Skip to content

Reject non-scalar UTXO transfer nonces#4793

Open
SimoneMariaRomeo wants to merge 1 commit into
Scottcjn:mainfrom
SimoneMariaRomeo:codex/utxo-scalar-nonce-replay
Open

Reject non-scalar UTXO transfer nonces#4793
SimoneMariaRomeo wants to merge 1 commit into
Scottcjn:mainfrom
SimoneMariaRomeo:codex/utxo-scalar-nonce-replay

Conversation

@SimoneMariaRomeo
Copy link
Copy Markdown

Summary

  • Reject boolean/object/array/null UTXO transfer nonces before signature or coin selection.
  • Normalize accepted integer/string nonces to one scalar replay key.
  • Add regression coverage for a same-signature object-nonce replay bypass caused by JSON key-order differences.

Bounty context

Targets Scottcjn/rustchain-bounties#2819. This is distinct from the earlier "no nonce protection" reports: replay protection exists, but it keyed str(nonce) while the signed transfer payload canonicalizes JSON with sort_keys=True. A caller could sign one object nonce, reorder that object in a second request, produce the same signed bytes, and get a different replay-table key.

Validation

  • python -m pytest node\test_utxo_endpoints.py tests\test_utxo_transfer_replay.py -q -> 21 passed
  • python -m py_compile node\utxo_endpoints.py tests\test_utxo_transfer_replay.py
  • git diff --check -- node\utxo_endpoints.py tests\test_utxo_transfer_replay.py
  • python tools\bcos_spdx_check.py --base-ref origin/main -> OK

Wallet/miner ID: SimoneMariaRomeo-codex-earner

@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) node Node server related tests Test suite changes size/M PR: 51-200 lines labels May 12, 2026
Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: LGTM! Thanks for contributing to RustChain. Approved.

Copy link
Copy Markdown

@saim256 saim256 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved current head a2e4d375c275a4ac036c816434215569c37dbb78.

This is a focused fix for a real replay-protection edge case in /utxo/transfer: JSON signing canonicalizes object keys with sort_keys=True, while the replay table previously keyed on str(nonce). Rejecting non-scalar nonces closes the key-order bypass, and normalizing accepted string/integer nonces gives the replay table one stable scalar key.

What I checked:

  • _normalize_transfer_nonce() rejects bool/object/array/null and empty strings before signature verification or UTXO selection.
  • The transfer endpoint now allows scalar nonce 0 by checking nonce is None instead of relying on truthiness.
  • Replay reservation stores nonce_key, so accepted integer/string nonces are handled consistently.
  • The regression test demonstrates that an object nonce is rejected before any balance movement or nonce-table insert, which is the safer behavior for the signed JSON canonicalization issue.

Validation run locally:

  • git diff --check origin/main...review-pr4793 -- node/utxo_endpoints.py tests/test_utxo_transfer_replay.py -> passed
  • python -m pytest node\test_utxo_endpoints.py tests\test_utxo_transfer_replay.py -q -> 21 passed
  • python -m py_compile node\utxo_endpoints.py tests\test_utxo_transfer_replay.py node\test_utxo_endpoints.py -> passed
  • python tools\bcos_spdx_check.py --base-ref origin/main -> BCOS SPDX check: OK

No production or live-target testing was performed.

Copy link
Copy Markdown
Contributor

@godd-ctrl godd-ctrl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved current head a2e4d37.

I reviewed the UTXO transfer nonce hardening. Rejecting non-scalar nonces closes the replay-key bypass caused by JSON signing canonicalizing object key order while the replay table stored str(nonce). The new normalization also keeps accepted integer/string nonces on one stable scalar key, and the endpoint now allows scalar nonce 0 by checking nonce is None instead of relying on truthiness.

Local verification passed:

  • uv run --no-project --with pytest --with flask python -m pytest node\test_utxo_endpoints.py tests\test_utxo_transfer_replay.py -q -> 21 passed
  • python -m py_compile node\utxo_endpoints.py tests\test_utxo_transfer_replay.py node\test_utxo_endpoints.py -> passed
  • uv run --no-project --with ruff ruff check --select E9,F821,F811 node\utxo_endpoints.py tests\test_utxo_transfer_replay.py node\test_utxo_endpoints.py --output-format=concise -> passed
  • git diff --check origin/main...HEAD -> passed
  • python tools\bcos_spdx_check.py --base-ref origin/main -> BCOS SPDX check: OK

The regression confirms object nonces are rejected before balance movement or nonce-table insertion. I did not find a blocker in this scope.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: LGTM! Thanks for contributing to RustChain. Approved.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: LGTM! Thanks for contributing to RustChain. Approved.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: LGTM! Thanks for contributing to RustChain. Approved.

Copy link
Copy Markdown
Contributor

@bolasse1234 bolasse1234 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the replay-key gap cleanly. Transfer nonces are now normalized before reservation, and non-scalar JSON values are rejected before signature verification / nonce table insertion. That closes the object-key-order bypass described in the regression while preserving integer and string nonce support.\n\nValidation I ran:\n- python -m pytest tests/test_utxo_transfer_replay.py -q -> 3 passed\n- python -m py_compile node\utxo_endpoints.py tests\test_utxo_transfer_replay.py -> passed\n- git diff --check origin/main...HEAD -- node/utxo_endpoints.py tests/test_utxo_transfer_replay.py -> passed\n- python tools\bcos_spdx_check.py --base-ref origin/main -> BCOS SPDX check: OK\n\nNo blocking findings from my review.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Reviewed for RustChain bounty. Wallet: AhqbFaPBPLMMiaLDzA9WhQcyvv4hMxiteLhPk3NhG1iG

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Reviewed for RustChain bounty. Wallet: AhqbFaPBPLMMiaLDzA9WhQcyvv4hMxiteLhPk3NhG1iG

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for contributing. Approved.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great contribution! LGTM. 🚀

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great contribution! LGTM. 🚀

@508704820
Copy link
Copy Markdown
Contributor

Reviewed as part of Bounty #2819 (UTXO Red Team Audit).

This is a solid security fix. Key observations:

  1. Root cause correct: Object/array nonce values with different JSON key orderings could bypass replay protection — e.g., {"a":1,"b":2} vs {"b":2,"a":1} serialize differently but are semantically identical.

  2. Normalization approach is sound: Forcing nonces to a single scalar representation (int or str) before signature verification eliminates the entire class of key-ordering replay attacks.

  3. Question: Does the normalization happen before or after signature verification? If after, the signature could still be valid for the original (non-normalized) nonce, creating a mismatch. The nonce should be normalized at parse time, before it enters the signature check.

  4. Suggestion: Consider also rejecting float nonces. nonce=1.0 vs nonce=1 are semantically identical in Python but would produce different signatures. isinstance(nonce, int) and not isinstance(nonce, bool) would be stricter.

Good addition of regression tests for the object-nonce replay bypass.

— Xeophon (wallet: RTC9d7caca3039130d3b26d41f7343d8f4ef4592360)

@iice257
Copy link
Copy Markdown

iice257 commented May 12, 2026

Review for Scottcjn/rustchain-bounties#73.

I reviewed the nonce normalization patch and the replay regression. This is a stronger fix than simply checking truthiness: it creates one replay key for accepted nonce values and rejects JSON containers before signature/reservation work.

Actionable notes:

  1. Please add an explicit numeric-zero test here too. The endpoint condition was changed from all(..., nonce) to nonce is None plus _normalize_transfer_nonce(), so nonce=0 should now be accepted and normalized to "0". That is an important non-regression and overlaps with Fix UTXO transfer nonce zero validation #4832.

  2. Please test string zero ("0") against numeric zero (0) as a replay pair. Because _normalize_transfer_nonce() turns both into the same key, the second request should be rejected as a duplicate even though the signed JSON payload can differ by type. If that type-collapsing is intentional, the test should document it.

  3. If this branch lands together with Fix UTXO transfer nonce zero validation #4832, the helpers will conflict conceptually (_missing_transfer_nonce vs _normalize_transfer_nonce). I would merge the stricter normalization helper and have the zero/blank tests live against that one path.

I think this is the right direction; the remaining risk is making the exact accepted nonce contract deliberate.

Copy link
Copy Markdown

@loganoe loganoe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. The transfer path now rejects non-scalar nonce values before signature verification or nonce reservation, and accepted integer/string nonces are normalized to a single replay key. That closes the object key-order replay path without reintroducing the falsy nonce=0 rejection.

Validation performed: git diff --check origin/main...HEAD, PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 /tmp/rustchain-flask-venv/bin/python -m pytest -q node/test_utxo_endpoints.py tests/test_utxo_transfer_replay.py, python3 -m py_compile node/utxo_endpoints.py tests/test_utxo_transfer_replay.py, and python3 tools/bcos_spdx_check.py --base-ref origin/main all pass with 21 tests. Manual normalizer smoke checks reject objects/arrays/null/bools and keep 0 as '0'.

Copy link
Copy Markdown
Contributor

@508704820 508704820 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Reject Non-Scalar UTXO Transfer Nonces

Summary

Adds _normalize_transfer_nonce() that validates nonce is a string or integer (not bool, list, or dict). Rejects empty nonces and non-scalar types. This is the original fix for the nonce=0 bug that #4830 and #4832 also address.

Verdict: Approve

Complements #4830/#4832 with proper type normalization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) node Node server related size/M PR: 51-200 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants