fix(crypto): bind burn cipher nonce to nullifier#6775
Conversation
|
[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 So this is effectively a one-way compatibility upgrade:
Suggested addition to the Compatibility table:
|
|
@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 |
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
ovkwith 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
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
encryptBurnMessageByOvkreturns a 96-byte record that occupies the same calldata / log slot as the previouscipher(80) || zero-padding(16)layout:Current version marker:
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 = 0x00000000andnonce = 0x000...000-> legacy v1 pathreserved = 0x00000001-> v2 path; the scanner must verifynonce == SHA3(domain || nf)[:12]reservedvalue -> rejectThis 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()aftergenerateSpendProof(...), once the nullifier is available. Callers only need to provideovk; if omitted, the builder falls back tospend.expsk.getOvk().Compatibility
toAddress 32 + value 32 + burn record 96)cipher(80) + nonce(12) + reserved/version(4)replacescipher(80) + zero padding(16)for newly generated burnsreserved=0 && nonce=0)getTriggerInputForShieldedTRC20Contractcipher(80) + zero-padding(16)and attempt zero-nonce decryption, which fails. Scanners that need to read v2 burns must be upgraded in lockstepUpgrade 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:getTriggerInputForShieldedTRC20Contractwith v1 semantics and old scanners cannot read v2 burn records (they interpret them ascipher(80) + zero-padding(16)and fail decryption).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.(ovk, nonce=0)reused across every burn under the sameovk). 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
NoteEncryption.javaBURN_CIPHER_RECORD_SIZE=96, explicit reserved/version marker handling, nonce derivation from nullifier, and strict v1/v2 burn-record decryption rulesShieldedTRC20ParametersBuilder.javaWallet.javacipher(80)+nonce(12)+reserved/version(4); scanner now uses explicit version markers; 80-byte trigger-input burn ciphers throw a clear deprecation/rejection errorBurnCipherTest.javaNoteEncDecryTest.javaTesting
./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