Skip to content

fix(crypto): bind burn cipher nonce to nullifier#6775

Open
Federico2014 wants to merge 1 commit into
tronprotocol:release_v4.8.2from
Federico2014:feature/burn-nonce-nf-binding
Open

fix(crypto): bind burn cipher nonce to nullifier#6775
Federico2014 wants to merge 1 commit into
tronprotocol:release_v4.8.2from
Federico2014:feature/burn-nonce-nf-binding

Conversation

@Federico2014
Copy link
Copy Markdown
Collaborator

@Federico2014 Federico2014 commented May 18, 2026

What does this PR do?

Hardens shielded TRC-20 burn encryption by binding the burn AEAD nonce to the spend nullifier, storing the nonce in the burn record, and making the burn-record version explicit in the trailing reserved bytes.

Why are these changes required?

The original implementation encrypted every burn payload under the same long-lived ovk with a static all-zero ChaCha20-Poly1305 nonce. Reusing (key, nonce) pairs breaks the AEAD security assumptions and leaks keystream relationships across burns.

This PR gives each burn a nonce derived from its nullifier and records enough metadata on chain for scanners to distinguish legacy and upgraded burn records without relying on ambiguous zero-nonce heuristics.

Design

Nullifier-bound nonce with domain separation

nonce = SHA3("Ztron_BurnNonce" || nf)[:12]

The fixed domain tag prevents collisions with any other SHA3-of-nullifier use. Since the nullifier is unique per spend, the resulting (ovk, nonce) pair is also unique per burn.

96-byte burn record

encryptBurnMessageByOvk returns a 96-byte record that occupies the same calldata / log slot as the previous cipher(80) || zero-padding(16) layout:

record (96B) = cipher(80) || nonce(12) || reserved/version(4)

Current version marker:

v2 marker = 0x00000001

Explicit legacy/v2 discrimination for scanners

decryptBurnMessageByOvk(ovk, cipher, nonceFromLog, reservedFromLog, nf) now treats the trailing 4 reserved bytes as an explicit version marker:

  • reserved = 0x00000000 and nonce = 0x000...000 -> legacy v1 path
  • reserved = 0x00000001 -> v2 path; the scanner must verify nonce == SHA3(domain || nf)[:12]
  • any other reserved value -> reject

This removes the previous ambiguity where an all-zero nonce could be interpreted both as a valid new record and as the legacy sentinel.

Encryption moved into the builder

BURN encryption now runs inside ShieldedTRC20ParametersBuilder.build() after generateSpendProof(...), once the nullifier is available. Callers only need to provide ovk; if omitted, the builder falls back to spend.expsk.getOvk().

Compatibility

Dimension Impact
Consensus / hard fork None — wallet / scanner / trigger-input only, no protocol-state transition
On-chain burn log layout Still 160B total (toAddress 32 + value 32 + burn record 96)
New burn record semantics cipher(80) + nonce(12) + reserved/version(4) replaces cipher(80) + zero padding(16) for newly generated burns
Legacy burns already on chain Still scannable via the explicit legacy branch (reserved=0 && nonce=0)
getTriggerInputForShieldedTRC20Contract Breaking change for legacy callers: 80-byte burn ciphers are now deprecated and rejected; callers must send the new 96-byte burn record
gRPC / HTTP / JSON-RPC shape Field names are unchanged, but acceptable burn payload semantics are narrower because legacy 80-byte burn ciphers are rejected
Config / DB schema None
Third-party SDKs SDKs that still build or submit 80-byte burn ciphers must be updated to emit the 96-byte burn record
Old java-tron / old SDK scanners One-way compatibility: the new scanner reads both legacy and v2 records, but old scanners interpret a v2 record as cipher(80) + zero-padding(16) and attempt zero-nonce decryption, which fails. Scanners that need to read v2 burns must be upgraded in lockstep

Upgrade guidance

The deployed shielded TRC-20 contract accepts an opaque bytes32[3] burn payload and does not interpret its layout, so this PR cannot reject v1 calldata on-chain. The v1→v2 migration is therefore a coordinated wallet/SDK/scanner upgrade. To get the privacy guarantees of nf-bound nonces, all of the following must be upgraded:

  1. Node operators — upgrade java-tron to the release that includes this PR. Old nodes still serve getTriggerInputForShieldedTRC20Contract with v1 semantics and old scanners cannot read v2 burn records (they interpret them as cipher(80) + zero-padding(16) and fail decryption).
  2. Wallet / SDK developers who hand-build burn calldata — emit the 96-byte v2 record: cipher(80) + nf-bound nonce(12) + reserved=0x00000001(4). The on-chain contract does not validate this layout, so 80-byte calldata continues to confirm, but any burn assembled that way loses nf-binding and falls back to the legacy zero-nonce decryption path with the known (ovk, nonce=0) reuse weakness.
  3. End users — until their wallet/SDK is upgraded, burns generated through that wallet retain the original v1 weakness (static (ovk, nonce=0) reused across every burn under the same ovk). Privacy improves only after the wallet itself emits v2 records.

Fully disallowing v1 calldata on-chain is an application-layer change: it requires redeploying the shielded TRC-20 contract with explicit layout validation (e.g. reject burn payloads whose trailing 16 bytes are all zero). That is out of scope for this PR.

Key changes

File Change
NoteEncryption.java Adds BURN_CIPHER_RECORD_SIZE=96, explicit reserved/version marker handling, nonce derivation from nullifier, and strict v1/v2 burn-record decryption rules
ShieldedTRC20ParametersBuilder.java Moves BURN encryption into the builder after spend proof generation so the nullifier is available; emits the 96-byte burn record
Wallet.java Parses burn logs as cipher(80)+nonce(12)+reserved/version(4); scanner now uses explicit version markers; 80-byte trigger-input burn ciphers throw a clear deprecation/rejection error
BurnCipherTest.java Covers 96-byte record structure, reserved/version marker behavior, v2 nf-required decryption, unknown marker rejection, and malformed input handling
NoteEncDecryTest.java Covers legacy zero-nonce compatibility, 96-byte trigger-input acceptance, 80-byte trigger-input rejection, burn-log round trip, burn-log truncation, missing-nf rejection, and same-tx cursor pairing

Testing

  • ./gradlew framework:test --tests "org.tron.core.zen.note.BurnCipherTest"
  • ./gradlew framework:test --tests "org.tron.core.zksnark.NoteEncDecryTest"
  • ./gradlew framework:test --tests org.tron.core.zksnark.NoteEncDecryTest.testGetTriggerInputBurn80ByteCipherRejected

@github-actions github-actions Bot requested a review from 3for May 18, 2026 03:03
Comment thread framework/src/main/java/org/tron/core/Wallet.java
Comment thread framework/src/main/java/org/tron/core/Wallet.java
Comment thread framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
Comment thread framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
@3for
Copy link
Copy Markdown
Collaborator

3for commented May 18, 2026

[SHOULD] The PR states that the change is “wallet / scanner / trigger-input only”. However, from the perspective of the old scanner, the new record is interpreted as cipher(80) + padding(16), and the legacy logic attempts decryption with a zero nonce. Decryption of the v2 cipher will therefore fail, because encryption now uses a derived nonce.

So this is effectively a one-way compatibility upgrade:

  • the new scanner can read both new and legacy records,
  • while the old scanner can only read legacy records.

Suggested addition to the Compatibility table:

┌────────────────────────────────┬──────────────────────────────────────────────────┐
│ Dimension                      │ Impact                                           │
├────────────────────────────────┼──────────────────────────────────────────────────┤
│ Old java-tron / old SDK scanner│ Cannot parse upgraded v2 burn records;           │
│                                │ synchronized upgrade is required                 │
└────────────────────────────────┴──────────────────────────────────────────────────┘

Compatibility

Dimension Impact
Consensus / hard fork None — wallet / scanner / trigger-input only, no protocol-state transition

@Federico2014
Copy link
Copy Markdown
Collaborator Author

Federico2014 commented May 18, 2026

@3for Good point — the asymmetry deserves to be called out explicitly. Updated the Compatibility table with a row for old scanners:

| Old java-tron / old SDK scanners | One-way compatibility: the new scanner reads both legacy and v2 records, but old scanners interpret a v2 record as cipher(80) + zero-padding(16) and attempt zero-nonce decryption, which fails. Scanners that need to read v2 burns must be upgraded in lockstep |

@Federico2014 Federico2014 requested review from 3for and waynercheung May 18, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants