Skip to content

fix(security): bind burn cipher nonce to nullifier#25

Open
Federico2014 wants to merge 1 commit into
developfrom
feature/burn-nonce-nf-binding
Open

fix(security): bind burn cipher nonce to nullifier#25
Federico2014 wants to merge 1 commit into
developfrom
feature/burn-nonce-nf-binding

Conversation

@Federico2014
Copy link
Copy Markdown
Owner

@Federico2014 Federico2014 commented May 9, 2026

What does this PR do?

Hardens the shielded TRC-20 burn encryption by binding the AEAD nonce to the spend nullifier, and embedding the nonce in the log record for backward-compatible scanner detection.

Why are these changes required?

The original implementation used a static (all-zero) nonce for every burn cipher under the same long-lived ovk. This PR makes each burn use a unique per-spend nonce derived from the nullifier, restoring the AEAD (key, nonce) uniqueness guarantee.

Design

nf-derived nonce with domain separation

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

The fixed domain tag prevents the derivation from colliding with any other SHA3-of-nf usage. Each burn gets a distinct (key, nonce) pair since the nullifier is globally unique per spend.

96-byte record layout

encryptBurnMessageByOvk returns a 96B record. The 16-byte trailing region of the legacy log already existed as zero padding, so total calldata length is unchanged:

record (96B) = cipher(80) || nonce(12) || reserved_zero(4)

Scanner backward compatibility

decryptBurnMessageByOvk(ovk, cipher, nonceFromLog, nf) reads the nonce from the log record:

  • All-zero nonce — legacy path (pre-upgrade burns whose 16B padding is naturally zero); decrypt with zero nonce.
  • Non-zero nonce + nf available — strict mode; verify nonce == SHA3(domain || nf)[:12] before decrypt.
  • Non-zero nonce + nf absent — relaxed mode; AEAD tag (keyed by ovk) provides authenticity. Used when the scanner cannot pair a NoteSpent log within the same tx.

Encryption moved into builder

Encryption now runs inside ShieldedTRC20ParametersBuilder.build() after generateSpendProof where the nullifier is available. Callers just setOvk(ovk); if not set, the builder falls back to spend.expsk.getOvk().

Compatibility

Dimension Impact
Consensus / hard fork None — wallet-side change only, no protocol-level state
On-chain log layout Unchanged 160 B (addr 32 + value 32 + cipher 80 + trailing 16); the trailing 16 B is now interpreted as nonce(12) + reserved(4) instead of pure padding
Contract ABI / triggerContractInput Unchanged 96 B hex passed to the burn contract
gRPC / HTTP / JSON-RPC No request/response shape change
Config keys / DB schema None
v1 legacy burns on chain Fully scannable — log nonce field is naturally all-zero, routed through the legacy zero-nonce path
v2 nf-bound burns New code; mainnet has none today
scanShieldedTRC20NotesByOvk API Same contract; allowShieldedTransactionApi defaults to false on mainnet
Third-party SDKs Not affected — deriveNonceFromNf is private and not exposed via any public surface

Residual note: historical burn ciphertexts encrypted under the old zero-nonce scheme remain on chain. Their keystream leakage is permanent and out of scope for this PR — only future burns benefit from the nf-binding.

Key changes

File Change
NoteEncryption.java encryptBurnMessageByOvk returns 96 B record; decryptBurnMessageByOvk takes 4 params with legacy/strict/relaxed paths; input length validation; private deriveNonceFromNf with Ztron_BurnNonce domain separation
ShieldedTRC20ParametersBuilder.java 96 B burnCiphertext; encryption inlined in BURN branch after spend proof; ovk falls back to spend.expsk.getOvk() when not set
Wallet.java NoteSpent(bytes32) log support (logType=5); scanner extracts nonce from log; pendingNf cursor for same-tx pairing; logData length guard; 80 B-only cipher rejected on the trigger-input path
BurnCipherTest.java 16 unit tests: round-trip, nonce determinism, wrong-nf rejection, tampered-nonce rejection, null-nf relaxed mode, record structure, input validation
NoteEncDecryTest.java 6 integration tests: legacy zero-nonce compat, Wallet 96 B / 80 B trigger input, logData too-short guard, end-to-end log scan round-trip, two-burns-in-one-tx cursor pairing

Testing

  • ./gradlew :framework:test --tests "org.tron.core.zen.note.BurnCipherTest" — 16/16 pass
  • ./gradlew :framework:test --tests "org.tron.core.zksnark.NoteEncDecryTest" — 8/8 pass (6 new + 2 pre-existing)
  • ./gradlew :framework:checkstyleMain :framework:checkstyleTest — pass
  • CI: all checks green (Build × 4 platforms, Coverage, CodeQL, SystemTest, check-math)

Summary by CodeRabbit

  • New Features

    • Enhanced shielded TRC-20 burn operations with improved OVK-based decoding and validation.
  • Bug Fixes

    • Improved burn cipher validation with stricter length and format checks; legacy 80-byte burn records now properly flagged as deprecated.
  • Tests

    • Added comprehensive test coverage for shielded TRC-20 burn encryption and decryption operations.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements v2 burn-message encryption for shielded TRC-20 using a nonce derived from the note nullifier (nf), updates encrypt/decrypt signatures to accept nf and a nonce-from-log, integrates OVK into the parameters builder, propagates pending nullifiers during Wallet log scanning, and adds comprehensive unit tests.

Changes

Shielded TRC-20 V2 Burn-Message Implementation

Layer / File(s) Summary
Data Contracts & Constants
framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java, framework/src/main/java/org/tron/core/Wallet.java
Adds BURN_CIPHER_LEN, BURN_NONCE_LEN, BURN_CIPHER_RECORD_SIZE; introduces SHIELDED_TRC20_LOG_TOPICS_NOTE_SPENT.
Core Encryption & Decryption
framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
Adds v2 encryptBurnMessageByOvk requiring nf and producing a 96-byte record (cipher+nonce+reserved); replaces decryptBurnMessageByOvk to accept nonceFromLog and nf with zero-nonce legacy fallback; introduces deriveNonceFromNf and isAllZero.
Builder Integration
framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java
Adds optional ovk field/setter; BURN build derives nf and calls encryptBurnMessageByOvk(..., nf), storing the returned record in burnCiphertext; serialization now embeds the actual record and adjusts padding.
Wallet Integration & Orchestration
framework/src/main/java/org/tron/core/Wallet.java
Sets OVK on builder; classifies NoteSpent as logType = 5; scan flow captures pendingNf from NoteSpent logs and passes it into subsequent burn-log decoding (logType = 4), validates decrypted address-prefix, clears pendingNf, and enforces record-length validation.
Test Coverage
framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java, framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java
Adds tests validating burn-cipher constants/formatting, encrypt→decrypt round-trips, nonce determinism and per-nf uniqueness, rejection on wrong nf or tampered nonce, v1 zero-nonce compatibility, record formatting expectations, input validation, and trigger/log handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through cipher's dance,
With nullifiers seeding nonce by chance,
V2 records tuck the nonce inside,
Old zero-nonce paths still glide—
Burn messages now stride in stride.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix(security): bind burn cipher nonce to nullifier' directly and clearly describes the main security improvement—binding the AEAD nonce to the nullifier. This is the primary change throughout the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/burn-nonce-nf-binding

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
framework/src/main/java/org/tron/core/Wallet.java (2)

3660-3677: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate ovk length before the burn AEAD path.

These burn branches only reject an empty ovk, but they now pass ovk straight into encryptBurnMessageByOvk(...). A non-32-byte value can still reach the AEAD call here, especially in the without-ask flow, which has no later length check.

Suggested fix
         byte[] ask = request.getAsk().toByteArray();
         byte[] nsk = request.getNsk().toByteArray();
         byte[] ovk = request.getOvk().toByteArray();
         if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) {
           throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk");
         }
+        if (ovk.length != 32) {
+          throw new ContractValidateException("ovk must be 32 bytes");
+        }
@@
         byte[] ak = request.getAk().toByteArray();
         byte[] nsk = request.getNsk().toByteArray();
         byte[] ovk = request.getOvk().toByteArray();
         if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) {
           throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk");
         }
+        if (ovk.length != 32) {
+          throw new ContractValidateException("ovk must be 32 bytes");
+        }

Also applies to: 3788-3802

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/Wallet.java` around lines 3660 - 3677,
The ovk byte array is only checked for emptiness but not length before being set
and passed into encryptBurnMessageByOvk; add a strict length check (ovk.length
== 32) where ovk is read and before builder.setOvk(...) and before any branch
that calls encryptBurnMessageByOvk(...) (the block around builder.setOvk and the
similar block at the other occurrence) and throw a ContractValidateException
with a clear message if the length is not 32 so a malformed ovk cannot reach the
AEAD call.

4317-4323: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep legacy 80-byte burn ciphertexts accepted here.

Line 4317 makes the trigger-input reconstruction path v2-only. If a client still carries the legacy 80-byte burn ciphertext, this now hard-fails even though the rest of the PR preserves v1 burn compatibility. Accepting BURN_CIPHER_LEN here and zero-extending it to the 96-byte record would avoid breaking older offline-signing flows.

Suggested fix
     if (parametersBuilder.getShieldedTRC20ParametersType() == ShieldedTRC20ParametersType.BURN) {
       byte[] burnCiper = ByteArray.fromHexString(shieldedTRC20Parameters.getTriggerContractInput());
-      if (!ArrayUtils.isEmpty(burnCiper)
-          && burnCiper.length == NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) {
-        parametersBuilder.setBurnCiphertext(burnCiper);
+      if (!ArrayUtils.isEmpty(burnCiper)) {
+        if (burnCiper.length == NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) {
+          parametersBuilder.setBurnCiphertext(burnCiper);
+        } else if (burnCiper.length == NoteEncryption.Encryption.BURN_CIPHER_LEN) {
+          byte[] legacyRecord = new byte[NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE];
+          System.arraycopy(
+              burnCiper, 0, legacyRecord, 0, NoteEncryption.Encryption.BURN_CIPHER_LEN);
+          parametersBuilder.setBurnCiphertext(legacyRecord);
+        } else {
+          throw new ZksnarkException(
+              "invalid shielded TRC-20 contract parameters for burn trigger input");
+        }
       } else {
         throw new ZksnarkException(
             "invalid shielded TRC-20 contract parameters for burn trigger input");
       }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/Wallet.java` around lines 4317 - 4323,
The current check only accepts NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE
(96) which rejects legacy 80-byte burnCiper; update the logic in Wallet.java
where burnCiper is handled so it also accepts
NoteEncryption.Encryption.BURN_CIPHER_LEN (80) and when burnCiper.length ==
BURN_CIPHER_LEN create a new 96-byte buffer, copy the 80 bytes into it and
zero-fill the remaining bytes, then call
parametersBuilder.setBurnCiphertext(...) with the 96-byte buffer; keep the
existing behavior when burnCiper.length == BURN_CIPHER_RECORD_SIZE by passing it
through unchanged and throw ZksnarkException for any other lengths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/src/main/java/org/tron/core/Wallet.java`:
- Around line 4043-4069: The burn-log branch (logType == 4) assumes logData has
the full fixed-size payload (Encryption.BURN_LOG_TO_ADDRESS_OFFSET ..
BURN_NONCE_LEN) before slicing and decrypting; add a defensive length check at
the start of that branch to ensure logData.length >=
(Encryption.BURN_LOG_RESERVED_OFFSET or compute 32+32+80+12+4 using the BURN_*
offsets/constants) and return Optional.empty() if too short, so subsequent
ByteArray.subArray calls and decryptBurnMessageByOvk/
decryptBurnMessageByOvk(ovk, cipher, nonceFromLog, nf) are never invoked on
truncated data; keep the existing behavior for pendingNf null check and preserve
use of logAmountArray, cipher, nonceFromLog, and decryptedText.

---

Outside diff comments:
In `@framework/src/main/java/org/tron/core/Wallet.java`:
- Around line 3660-3677: The ovk byte array is only checked for emptiness but
not length before being set and passed into encryptBurnMessageByOvk; add a
strict length check (ovk.length == 32) where ovk is read and before
builder.setOvk(...) and before any branch that calls
encryptBurnMessageByOvk(...) (the block around builder.setOvk and the similar
block at the other occurrence) and throw a ContractValidateException with a
clear message if the length is not 32 so a malformed ovk cannot reach the AEAD
call.
- Around line 4317-4323: The current check only accepts
NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE (96) which rejects legacy
80-byte burnCiper; update the logic in Wallet.java where burnCiper is handled so
it also accepts NoteEncryption.Encryption.BURN_CIPHER_LEN (80) and when
burnCiper.length == BURN_CIPHER_LEN create a new 96-byte buffer, copy the 80
bytes into it and zero-fill the remaining bytes, then call
parametersBuilder.setBurnCiphertext(...) with the 96-byte buffer; keep the
existing behavior when burnCiper.length == BURN_CIPHER_RECORD_SIZE by passing it
through unchanged and throw ZksnarkException for any other lengths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5dc7e685-5d4e-4b76-9757-4ec800c74b28

📥 Commits

Reviewing files that changed from the base of the PR and between 645bf20 and b203364.

📒 Files selected for processing (4)
  • framework/src/main/java/org/tron/core/Wallet.java
  • framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java
  • framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
  • framework/src/test/java/org/tron/core/zen/note/BurnCipherV2Test.java

Comment thread framework/src/main/java/org/tron/core/Wallet.java Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java">

<violation number="1" location="framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java:372">
P2: Treating `null` as all-zero silently falls back to insecure v1 decryption. If a caller fails to extract the nonce from the log and passes `null`, the nonce verification is bypassed entirely — undermining the replay protection this fix adds. Consider returning `false` for null (or throwing) so that a missing nonce is never silently accepted as a v1 record.</violation>
</file>

<file name="framework/src/test/java/org/tron/core/zen/note/BurnCipherV2Test.java">

<violation number="1" location="framework/src/test/java/org/tron/core/zen/note/BurnCipherV2Test.java:86">
P2: This test should assert rejection for the bad address prefix. As written, it asserts decryption succeeds and only checks the raw byte value, so it would still pass if invalid prefixes were accepted.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java Outdated
Comment thread framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java Outdated
@Federico2014 Federico2014 force-pushed the feature/burn-nonce-nf-binding branch 5 times, most recently from 460ea93 to a8442b9 Compare May 9, 2026 12:11
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java (1)

257-296: ⚡ Quick win

Deduplicate the two encryptBurnMessageByOvk overloads.

The two overloads share ~90% of their body (plaintext layout, AEAD call, error path). Either delegate the legacy overload to the new one, or extract a private helper that takes the nonce as a parameter. As-is, future changes to the layout (e.g., 32+21 packing) must be applied in two places.

♻️ Proposed refactor
   public static Optional<byte[]> encryptBurnMessageByOvk(byte[] ovk, BigInteger toAmount,
       byte[] transparentToAddress)
       throws ZksnarkException {
-    byte[] plaintext = new byte[64];
-    byte[] amountArray = ByteUtil.bigIntegerToBytes(toAmount, 32);
-    byte[] zeroNonce = new byte[BURN_NONCE_LEN];
-    byte[] cipher = new byte[BURN_CIPHER_LEN];
-    System.arraycopy(amountArray, 0, plaintext, 0, 32);
-    System.arraycopy(transparentToAddress, 0, plaintext, 32, 21);
-
-    if (JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt(new Chacha20Poly1305IetfEncryptParams(
-        cipher, null, plaintext,
-        64, null, 0, null, zeroNonce, ovk)) != 0) {
-      return Optional.empty();
-    }
-
-    return Optional.of(cipher);
+    return encryptBurnMessageByOvkWithNonce(ovk, toAmount, transparentToAddress,
+        new byte[BURN_NONCE_LEN]);
   }
 
   public static Optional<byte[]> encryptBurnMessageByOvk(byte[] ovk, BigInteger toAmount,
       byte[] transparentToAddress, byte[] nf)
       throws ZksnarkException {
-    byte[] plaintext = new byte[64];
-    byte[] amountArray = ByteUtil.bigIntegerToBytes(toAmount, 32);
-    byte[] nonce = deriveNonceFromNf(nf);
-    byte[] cipher = new byte[BURN_CIPHER_LEN];
-    System.arraycopy(amountArray, 0, plaintext, 0, 32);
-    System.arraycopy(transparentToAddress, 0, plaintext, 32, 21);
-
-    if (JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt(new Chacha20Poly1305IetfEncryptParams(
-        cipher, null, plaintext,
-        64, null, 0, null, nonce, ovk)) != 0) {
-      return Optional.empty();
-    }
-
-    return Optional.of(cipher);
+    return encryptBurnMessageByOvkWithNonce(ovk, toAmount, transparentToAddress,
+        deriveNonceFromNf(nf));
+  }
+
+  private static Optional<byte[]> encryptBurnMessageByOvkWithNonce(byte[] ovk, BigInteger toAmount,
+      byte[] transparentToAddress, byte[] nonce)
+      throws ZksnarkException {
+    byte[] plaintext = new byte[64];
+    byte[] amountArray = ByteUtil.bigIntegerToBytes(toAmount, 32);
+    byte[] cipher = new byte[BURN_CIPHER_LEN];
+    System.arraycopy(amountArray, 0, plaintext, 0, 32);
+    System.arraycopy(transparentToAddress, 0, plaintext, 32, 21);
+    if (JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt(new Chacha20Poly1305IetfEncryptParams(
+        cipher, null, plaintext,
+        64, null, 0, null, nonce, ovk)) != 0) {
+      return Optional.empty();
+    }
+    return Optional.of(cipher);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java` around
lines 257 - 296, The two overloads of encryptBurnMessageByOvk share almost
identical logic; extract the shared body into one private helper (e.g.,
encryptBurnMessageByOvkWithNonce(byte[] ovk, BigInteger toAmount, byte[]
transparentToAddress, byte[] nonce)) that builds the 64-byte plaintext (using
ByteUtil.bigIntegerToBytes for 32 bytes and copying the 21-byte
transparentToAddress), chooses a zero nonce when passed null (or accept the
explicit zeroNonce = new byte[BURN_NONCE_LEN]), and performs the
JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt call with BURN_CIPHER_LEN; then
have the current public overloads (encryptBurnMessageByOvk(...), and
encryptBurnMessageByOvk(..., byte[] nf)) delegate to that helper (for the nf
variant call deriveNonceFromNf(nf) and pass it).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java`:
- Around line 279-303: deriveNonceFromNf currently passes a null or malformed nf
into Hash.sha3 causing an NPE and the v2 encrypt path (encryptBurnMessageByOvk)
propagates that; add explicit input validation: in deriveNonceFromNf call
Objects.requireNonNull(nf, "nf must not be null") and validate nf.length > 0 (or
== expected length if you have a constant) and throw an IllegalArgumentException
with a clear message on bad length; additionally, in encryptBurnMessageByOvk
(and any v2 encrypt overload that calls deriveNonceFromNf) perform a
precondition check and convert invalid input into a ZksnarkException (or throw a
ZksnarkException with a descriptive message) before calling deriveNonceFromNf so
callers get a fast, meaningful domain error rather than an NPE.

In `@framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java`:
- Around line 131-154: The test name is misleading —
testDecryptBurnWrongAddressPrefixRejected actually checks that
Encryption.encryptBurnMessageByOvk and Encryption.decryptBurnMessageByOvk
round-trip an address prefix byte of 0x00, it does not exercise Wallet prefix
rejection; rename the test to something like
testDecryptBurnRoundTripsInvalidPrefix (or similar) to reflect it verifies
round-trip behavior of Encryption.encryptBurnMessageByOvk and
Encryption.decryptBurnMessageByOvk, and add a new sibling test that
constructs/validates an address with a 0x00 prefix and asserts the Wallet code
path that enforces prefix rules (the Wallet prefix validation/constructor code
where Wallet.getAddressPreFixByte() is referenced) actually rejects it.

---

Nitpick comments:
In `@framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java`:
- Around line 257-296: The two overloads of encryptBurnMessageByOvk share almost
identical logic; extract the shared body into one private helper (e.g.,
encryptBurnMessageByOvkWithNonce(byte[] ovk, BigInteger toAmount, byte[]
transparentToAddress, byte[] nonce)) that builds the 64-byte plaintext (using
ByteUtil.bigIntegerToBytes for 32 bytes and copying the 21-byte
transparentToAddress), chooses a zero nonce when passed null (or accept the
explicit zeroNonce = new byte[BURN_NONCE_LEN]), and performs the
JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt call with BURN_CIPHER_LEN; then
have the current public overloads (encryptBurnMessageByOvk(...), and
encryptBurnMessageByOvk(..., byte[] nf)) delegate to that helper (for the nf
variant call deriveNonceFromNf(nf) and pass it).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a17d0d0c-2322-4517-a522-eb24b461e74a

📥 Commits

Reviewing files that changed from the base of the PR and between b203364 and 62d1943.

📒 Files selected for processing (4)
  • framework/src/main/java/org/tron/core/Wallet.java
  • framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java
  • framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
  • framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • framework/src/main/java/org/tron/core/Wallet.java
  • framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java

Comment thread framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
Comment thread framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java Outdated
@Federico2014 Federico2014 force-pushed the feature/burn-nonce-nf-binding branch 3 times, most recently from 5de2e31 to 39f6829 Compare May 9, 2026 12:29
@Federico2014 Federico2014 changed the title fix(security): bind burn cipher nonce to nullifier to prevent AEAD replay fix(security): bind burn cipher nonce to nullifier May 9, 2026
@Federico2014 Federico2014 force-pushed the feature/burn-nonce-nf-binding branch 3 times, most recently from fa0bc20 to 9b2c9ae Compare May 9, 2026 13:22
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java`:
- Around line 298-310: Validate transparentToAddress before copying in the OVK
burn-encrypt path: inside the block guarded by ovk != null (the code that
creates addr21 and calls Encryption.encryptBurnMessageByOvk), check that
transparentToAddress is non-null and has length >= 20 (same guard as
ArrayUtils.isEmpty(transparentToAddress) used elsewhere); if it fails, throw a
clear ZksnarkException (or skip encryption explicitly) so a meaningful error is
raised instead of letting System.arraycopy throw
NullPointerException/ArrayIndexOutOfBoundsException. Ensure the check is placed
before creating addr21 or calling Encryption.encryptBurnMessageByOvk so the
error context remains tied to build()/ovk handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29c7f520-6cb7-4e57-a0ea-02eeddd7b8ae

📥 Commits

Reviewing files that changed from the base of the PR and between 62d1943 and 9b2c9ae.

📒 Files selected for processing (4)
  • framework/src/main/java/org/tron/core/Wallet.java
  • framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java
  • framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
  • framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • framework/src/main/java/org/tron/core/Wallet.java

@Federico2014 Federico2014 force-pushed the feature/burn-nonce-nf-binding branch 2 times, most recently from c9b5c16 to 7f7e289 Compare May 9, 2026 14:51
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java (1)

298-310: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate transparentToAddress before the arraycopy on line 303.

encryptBurnMessageByOvk does check transparentToAddress.length != 21, but that runs after System.arraycopy(transparentToAddress, 0, addr21, 1, 20) on line 303, so a null or < 20 byte input throws NullPointerException / ArrayIndexOutOfBoundsException first and the surrounding try/catch rewraps it into a generic "build the shielded TRC-20 parameters error: ...", masking the real misconfiguration. The sibling burnParamsToHexString already guards this at lines 496-498 with ArrayUtils.isEmpty(transparentToAddress).

🛡️ Suggested guard
           byte[] nf = spendDescription.getNullifier().toByteArray();
           if (ovk != null) {
+            if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length < 20) {
+              throw new ZksnarkException("invalid transparentToAddress for burn encryption");
+            }
             byte[] addr21 = new byte[21];
             addr21[0] = Wallet.getAddressPreFixByte();
             System.arraycopy(transparentToAddress, 0, addr21, 1, 20);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java`
around lines 298 - 310, In ShieldedTRC20ParametersBuilder (the method that
builds the shielded TRC-20 parameters surrounding the shown snippet), add an
input guard before the System.arraycopy that verifies transparentToAddress is
non-null and has at least 20 bytes (e.g., same check used in
burnParamsToHexString / ArrayUtils.isEmpty and length >= 20); if the check
fails, throw a ZksnarkException with a clear message like "invalid
transparentToAddress: null or too short" so we fail fast and avoid
NPE/ArrayIndexOutOfBounds before calling Encryption.encryptBurnMessageByOvk.
🧹 Nitpick comments (1)
framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java (1)

310-321: 💤 Low value

Confirm the v1 legacy path intentionally skips nf-binding verification even when nf is supplied.

When isAllZero(nonceFromLog) is true the code proceeds straight to AEAD decryption regardless of whether nf was provided, so a caller passing a real nf for a v1-record cannot detect tampering via the nonce-binding check (it has to rely solely on AEAD integrity over ovk). This is consistent with the PR's documented residual risk ("injection via direct calldata construction with zero-nonce remains until an activation-height gate is added in a separate PR"), but a one-line comment here pointing at the activation-gate follow-up would make the intent explicit for future readers and prevent the legacy branch from being mistakenly relied upon as a verified path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java` around
lines 310 - 321, The v1 legacy branch intentionally skips nf-binding
verification when isAllZero(nonceFromLog) is true; add a one-line comment above
that branch (near isAllZero(nonceFromLog) / nonceFromLog / nf /
deriveNonceFromNf) stating this is deliberate for v1 records and that nf-binding
will be enforced by the planned activation-height gate in a follow-up PR (so
callers should not rely on this branch for nf verification). Ensure the comment
mentions the residual injection risk and references the activation-height gate
follow-up to make intent explicit for future readers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java`:
- Around line 298-310: In ShieldedTRC20ParametersBuilder (the method that builds
the shielded TRC-20 parameters surrounding the shown snippet), add an input
guard before the System.arraycopy that verifies transparentToAddress is non-null
and has at least 20 bytes (e.g., same check used in burnParamsToHexString /
ArrayUtils.isEmpty and length >= 20); if the check fails, throw a
ZksnarkException with a clear message like "invalid transparentToAddress: null
or too short" so we fail fast and avoid NPE/ArrayIndexOutOfBounds before calling
Encryption.encryptBurnMessageByOvk.

---

Nitpick comments:
In `@framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java`:
- Around line 310-321: The v1 legacy branch intentionally skips nf-binding
verification when isAllZero(nonceFromLog) is true; add a one-line comment above
that branch (near isAllZero(nonceFromLog) / nonceFromLog / nf /
deriveNonceFromNf) stating this is deliberate for v1 records and that nf-binding
will be enforced by the planned activation-height gate in a follow-up PR (so
callers should not rely on this branch for nf verification). Ensure the comment
mentions the residual injection risk and references the activation-height gate
follow-up to make intent explicit for future readers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4137961-0538-4576-bd02-001c2de7b1be

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2c9ae and 7f7e289.

📒 Files selected for processing (4)
  • framework/src/main/java/org/tron/core/Wallet.java
  • framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java
  • framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
  • framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • framework/src/main/java/org/tron/core/Wallet.java
  • framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java

@Federico2014 Federico2014 force-pushed the feature/burn-nonce-nf-binding branch 2 times, most recently from f08121e to 2648e05 Compare May 9, 2026 16:24
@Federico2014 Federico2014 force-pushed the feature/burn-nonce-nf-binding branch from 2648e05 to 682d8d5 Compare May 10, 2026 03:46
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.

1 participant