Skip to content

fix(crypto): harden shielded burn ovk encryption - #24

Closed
Federico2014 wants to merge 3 commits into
developfrom
fix/burn-ovk-nonce
Closed

Federico2014 wants to merge 3 commits into
developfrom
fix/burn-ovk-nonce

Conversation

@Federico2014

@Federico2014 Federico2014 commented May 8, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Hardens the out_ciphertext (ovk-encrypted note) used by shielded TRC-20 burn:

  • NoteEncryption.encryptBurnMessageByOvk generates a fresh CSPRNG nonce per call and emits a 96-byte record laid out as cipher(80) || nonce(12) || reserved(4). The 2^-96 all-zero draw is rerolled so the nonce field never collides with the pre-upgrade zero padding at the same offset.
  • decryptBurnMessageByOvk(ovk, ciphertext, nonce) requires a 12-byte nonce; null or wrong-length input is rejected via a fail-fast precondition. Pre-upgrade burn logs carry an all-zero byte field at the nonce offset and decrypt via the same path.
  • ShieldedTRC20ParametersBuilder emits the 96-byte burnCiphertext.
  • Wallet.getNoteTxFromLogListByOvk parses the nonce slice from on-chain logs and feeds it into the new decrypt API.
  • Wallet.getTriggerInputForShieldedTRC20Contract accepts only the 96-byte record. Stale clients passing an 80-byte cipher are rejected with a clear error message guiding them to rebuild against the new encoder; padding stale ciphertext to a zero-nonce record would otherwise extend the (ovk, zero-nonce) reuse surface forward in time.

Why are these changes required?

The previous burn AEAD reused a fixed all-zero nonce under the same ovk for every burn, violating RFC 8439 §4 (key, nonce) uniqueness. Two burns under the same ovk recover the keystream and the Poly1305 one-time key, which is sufficient to forge AEAD ciphertexts attributed to that ovk.

Compatibility matrix

Scenario Behavior
New create + new scan Works. Each burn uses a fresh CSPRNG nonce; (ovk, nonce) reuse is gone.
Pre-upgrade burn log + new scan Works. The nonce slice is all-zero on legacy records and decrypts via the same path.
New create + old scan Incompatible. Old scanners always pass a zero nonce to ChaCha20-Poly1305, so they cannot decrypt new burns whose nonce is random.
Pre-upgrade 80-byte burnCiphertext fed into the new getTriggerInputForShieldedTRC20Contract Rejected (Wallet.java:4291) with an explicit error directing the caller to rebuild against encryptBurnMessageByOvk. Padding stale ciphertext to a zero-nonce record would extend the (ovk, zero-nonce) reuse window forward in time.
New 96-byte record fed into an old getTriggerInputForShieldedTRC20Contract Rejected by old nodes (their length check accepts only 80 bytes).
Old node / old client keeps producing zero-nonce burns on-chain New scanners still decrypt them, since the scan path must remain compatible with historical records. The (ovk, zero-nonce) reuse surface persists for legacy producers until they upgrade — see Stage 2 follow-up below.

This PR has been tested by:

  • Unit Tests
    • NoteEncDecryTest: round-trip with random nonce, uniqueness across calls, pre-upgrade zero-nonce decode, malformed nonce rejection, short-log rejection, 80-byte trigger-input rejection, 96-byte trigger-input round-trip.
  • Manual Testing
    • Local node: regenerate burn parameters end-to-end and verify scan APIs decode pre-upgrade and new records.

Follow up

A scanner-side activation height (BURN_NONCE_ACTIVATION_HEIGHT, default Long.MAX_VALUE, switchable via a ChainParameters proposal) is planned as a separate change. Once activated, getNoteTxFromLogListByOvk will refuse to decrypt any zero-nonce burn whose log height is ≥ H₀, closing the residual phantom-burn injection path (Finding 1) that bypasses the encoder API.

Note: the gate is a hard reject and cannot distinguish injected phantom burns from legitimate burns produced by un-upgraded clients. Activating it therefore requires an ecosystem migration window: every producer that bypasses getTriggerInputForShieldedTRC20Contract (legacy SDKs / wallets / third-party scripts) must be on the 96-byte encoder before H₀, otherwise their post-H₀ burns become unscannable. Tracking in the design doc.

Extra details

  • No consensus / hard-fork impact: change is confined to off-chain encoding and parsing; zk-SNARK circuit and on-chain validation are unchanged.
  • No DB / config changes.

Summary by CodeRabbit

  • Bug Fixes

    • Stricter validation for shielded TRC-20 burn payloads: rejects legacy 80-byte burn ciphertexts and enforces exact record size, reducing ambiguous/invalid decrypts.
    • Nonce-aware burn-message decryption now validates decrypted amount and recipient before exposing transparent fields.
  • New Features

    • Burn records now include a per-burn nonce for improved encryption semantics.
  • Tests

    • Added tests for round-trip encryption/decryption, nonce uniqueness, legacy rejection, and edge-case log handling.

@coderabbitai

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a 96-byte burn record (80-byte AEAD cipher + 12-byte nonce + 4 reserved), updates NoteEncryption to emit/consume explicit nonces, changes calldata encoding to embed the 96-byte record, makes Wallet parse and require the nonce (trigger inputs reject legacy 80-byte ciphers), and adds tests for round-trip, uniqueness, legacy, and log handling.

Changes

Burn Message Nonce-Based Encryption

Layer / File(s) Summary
Data Contract and Constants
framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
Adds SecureRandom import and public constants: BURN_CIPHER_RECORD_SIZE (96), BURN_CIPHER_LEN (80), BURN_NONCE_OFFSET (80), BURN_NONCE_LEN (12); adds helper to avoid all-zero nonces.
Encrypt/Decrypt API Implementation
framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
encryptBurnMessageByOvk generates a non-zero 12-byte nonce and returns a 96-byte record (cipher
Burn Calldata Encoding
framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java
Changes burnCiphertext buffer from 80 to 96 bytes and replaces prior cipher(80)+zeros(16) tail with the 96-byte burnCiphertext in burnParamsToHexString; adjusts local zero-padding usage.
Wallet Parsing and Trigger Validation
framework/src/main/java/org/tron/core/Wallet.java
Burn log parsing (logType == 4) enforces minimum payload length, extracts cipher and explicit nonce, and calls decryptBurnMessageByOvk(ovk, cipher, nonce). Trigger-input generation accepts only 96-byte burn records and throws a ZksnarkException when given legacy 80-byte burn ciphertexts.
Test Coverage
framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java
Adds imports, helpers, and tests for encrypt/decrypt round-trip and record layout, nonce uniqueness across encryptions, legacy zero-nonce handling, trigger-input handling for 96/80-byte payloads (80-byte rejected), truncated-burn-log rejection, end-to-end burn log round-trip, and helper builders.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A dozen bright bytes tucked beneath the night,
Cipher sings eighty while the nonce holds twelve tight,
Old eighty’s turned away at the new gate,
Tests hop in circles — round-trip checks celebrate. 🥕🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% 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 title 'fix(crypto): harden shielded burn ovk encryption' directly and concisely describes the main change: hardening the shielded burn OVK encryption mechanism by adding per-call random nonces, which is the primary objective across all modified files.
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 fix/burn-ovk-nonce

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.

@Federico2014
Federico2014 marked this pull request as ready for review May 9, 2026 02:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
framework/src/main/java/org/tron/core/Wallet.java (1)

4048-4049: ⚡ Quick win

Derive the nonce offset from the shared burn layout constants.

Line 4048 hardcodes 144 even though NoteEncryption.Encryption now owns the burn record sizes. Recomputing this from BURN_CIPHER_LEN keeps the log parser aligned if the record layout changes again.

Suggested change
-        byte[] cipher = ByteArray.subArray(logData, 64, 144);
-        byte[] nonce = ByteArray.subArray(logData, 144,
-            144 + NoteEncryption.Encryption.BURN_NONCE_LEN);
+        final int burnCipherOffset = 64;
+        final int burnNonceOffset =
+            burnCipherOffset + NoteEncryption.Encryption.BURN_CIPHER_LEN;
+        byte[] cipher = ByteArray.subArray(logData, burnCipherOffset, burnNonceOffset);
+        byte[] nonce = ByteArray.subArray(logData, burnNonceOffset,
+            burnNonceOffset + NoteEncryption.Encryption.BURN_NONCE_LEN);
🤖 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 4048 - 4049,
The code in Wallet.java extracts the burn nonce using a hardcoded offset (144)
which can drift when NoteEncryption.Encryption record sizes change; replace the
magic number by deriving the start offset from the shared burn layout constants
(e.g., use NoteEncryption.Encryption.BURN_CIPHER_LEN to compute the offset) and
then call ByteArray.subArray(logData, computedOffset, computedOffset +
NoteEncryption.Encryption.BURN_NONCE_LEN) so the nonce extraction remains
correct if the record layout changes.
framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java (1)

203-327: ⚡ Quick win

Add Wallet-level regression coverage for the rollout compatibility paths.

These tests validate NoteEncryption well, but the risky behavior change is in Wallet: parsing a 96-byte burn log and padding a legacy 80-byte trigger input back to 96 bytes. A focused test around those two paths would catch the integration regressions this PR is trying to avoid.

🤖 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/test/java/org/tron/core/zksnark/NoteEncDecryTest.java` around
lines 203 - 327, Add Wallet-level regression tests that exercise the two
rollout-compatibility paths: parsing a full 96-byte burn log and handling an
80-byte legacy burn trigger by padding it to 96 bytes before processing. Write
tests that construct a 96-byte record and an 80-byte legacy record, call the
Wallet method(s) that parse/process burn logs (use the Wallet class methods that
accept burn records / trigger inputs), and assert the resulting parsed
amount/address match the original plaintext and that the 80-byte input is
correctly padded to 96 bytes (or produces the same parsed output as the 96-byte
record). Locate test targets based on Wallet and the wallet method names used to
parse burn records (e.g., any method that consumes a byte[] burn record or
trigger input) and add assertions for both successful parsing and failure cases
(wrong nonce) to mirror existing NoteEncDecryTest expectations.
🤖 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 296-299: The current decryptBurnMessageByOvk method silently
replaces any wrong-length nonce with an all-zero nonce; instead, in
NoteEncryption.decryptBurnMessageByOvk you should only accept a null or empty
nonce to trigger the legacy all-zero behavior and reject any non-null nonce
whose length != BURN_NONCE_LEN by returning Optional.empty(); update the nonce
validation logic to check nonce == null || nonce.length == 0 (legacy) or
nonce.length == BURN_NONCE_LEN (use provided), otherwise return Optional.empty()
so malformed/truncated nonces are not silently accepted.

---

Nitpick comments:
In `@framework/src/main/java/org/tron/core/Wallet.java`:
- Around line 4048-4049: The code in Wallet.java extracts the burn nonce using a
hardcoded offset (144) which can drift when NoteEncryption.Encryption record
sizes change; replace the magic number by deriving the start offset from the
shared burn layout constants (e.g., use
NoteEncryption.Encryption.BURN_CIPHER_LEN to compute the offset) and then call
ByteArray.subArray(logData, computedOffset, computedOffset +
NoteEncryption.Encryption.BURN_NONCE_LEN) so the nonce extraction remains
correct if the record layout changes.

In `@framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java`:
- Around line 203-327: Add Wallet-level regression tests that exercise the two
rollout-compatibility paths: parsing a full 96-byte burn log and handling an
80-byte legacy burn trigger by padding it to 96 bytes before processing. Write
tests that construct a 96-byte record and an 80-byte legacy record, call the
Wallet method(s) that parse/process burn logs (use the Wallet class methods that
accept burn records / trigger inputs), and assert the resulting parsed
amount/address match the original plaintext and that the 80-byte input is
correctly padded to 96 bytes (or produces the same parsed output as the 96-byte
record). Locate test targets based on Wallet and the wallet method names used to
parse burn records (e.g., any method that consumes a byte[] burn record or
trigger input) and add assertions for both successful parsing and failure cases
(wrong nonce) to mirror existing NoteEncDecryTest expectations.
🪄 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: 9f2aa1ed-d7b3-4c93-9482-ec97de2f94db

📥 Commits

Reviewing files that changed from the base of the PR and between 328480f and da67899.

📒 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/zksnark/NoteEncDecryTest.java

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

@Federico2014
Federico2014 force-pushed the fix/burn-ovk-nonce branch from da67899 to 8622a5e Compare May 9, 2026 03:24
@Federico2014
Federico2014 force-pushed the fix/burn-ovk-nonce branch from 8622a5e to 290cbd4 Compare May 9, 2026 04:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java (1)

290-309: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore the legacy 2-arg burn decrypt overload.

The new nonce-aware overload is here, but the public decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext) entry point is no longer in the class. That turns the rollout into a source-compatible break for any downstream caller still decoding historical 80-byte burn records via the old API.

Suggested fix
+    public static Optional<byte[]> decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext)
+        throws ZksnarkException {
+      return decryptBurnMessageByOvk(ovk, ciphertext, new byte[BURN_NONCE_LEN]);
+    }
+
     public static Optional<byte[]> decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext,
         byte[] nonce) throws ZksnarkException {
       byte[] outPlaintext = new byte[64];
       if (JLibsodium.cryptoAeadChacha20poly1305IetfDecrypt(new Chacha20poly1305IetfDecryptParams(
           outPlaintext, null,
           null,
           ciphertext, BURN_CIPHER_LEN,
           null,
           0,
           nonce, ovk)) != 0) {
         return Optional.empty();
       }
       return Optional.of(outPlaintext);
     }
🤖 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 290 - 309, Add back the legacy two-arg overload
decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext) that simply creates an
all-zero nonce of length BURN_NONCE_LEN and delegates to the existing
decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext, byte[] nonce), preserving
the throws ZksnarkException signature; this restores source compatibility for
callers decoding historical burn records (the underlying decrypt uses
BURN_CIPHER_LEN and JLibsodium.cryptoAeadChacha20poly1305IetfDecrypt).
🧹 Nitpick comments (2)
framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java (2)

326-350: ⚡ Quick win

Assert the burn ciphertext layout, not just notNull.

These two tests still pass if the wallet takes the wrong branch or serializes the burn record incorrectly. Please inspect the returned trigger input and verify the burn slot is 96 bytes, preserves the 96-byte input as-is, and zero-pads the nonce/reserved tail when the input is only 80 bytes.

🤖 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/test/java/org/tron/core/zksnark/NoteEncDecryTest.java` around
lines 326 - 350, Update the two tests (testGetTriggerInputBurn96ByteCipher and
testGetTriggerInputBurn80ByteCipher) to validate the actual burn ciphertext
layout instead of only asserting non-null: call
wallet.getTriggerInputForShieldedTRC20Contract(req), extract the trigger input
bytes from the returned GrpcAPI.BytesMessage, locate the burn slot bytes within
that serialized trigger input (the same slot produced by buildBurnTrc20Params),
and assert that the burn slot length equals Encryption.BURN_CIPHER_RECORD_SIZE
(96); for the 96-byte case assert the returned 96 bytes exactly match the
original burnCipher, and for the 80-byte legacy case assert the first
Encryption.BURN_CIPHER_LEN (80) bytes match legacyCipher and the trailing bytes
up to Encryption.BURN_CIPHER_RECORD_SIZE are zero (zero-padded nonce/reserved
tail).

380-413: ⚡ Quick win

Verify the decoded fields on the burn-log round trip.

isPresent() only proves decryption succeeded. It will miss regressions where getNoteTxFromLogListByOvk populates the wrong amount or transparent address after decrypting. Please assert the populated result/builder matches the injected amount and toAddress.

🤖 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/test/java/org/tron/core/zksnark/NoteEncDecryTest.java` around
lines 380 - 413, After confirming Optional is present, unwrap the Optional
returned from Wallet.getNoteTxFromLogListByOvk (or inspect the populated
GrpcAPI.DecryptNotesTRC20.NoteTx.Builder) and assert that the decoded amount and
transparent address equal the original amount and toAddress used to build
logData; specifically verify the NoteTx amount field matches BigInteger value
(or its byte/uint representation) and the transparent address bytes (or address
string) match the injected toAddress bytes (skip prefix byte if the function
stores the 20-byte payload) to catch incorrect population of those fields.
🤖 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.

Outside diff comments:
In `@framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java`:
- Around line 290-309: Add back the legacy two-arg overload
decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext) that simply creates an
all-zero nonce of length BURN_NONCE_LEN and delegates to the existing
decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext, byte[] nonce), preserving
the throws ZksnarkException signature; this restores source compatibility for
callers decoding historical burn records (the underlying decrypt uses
BURN_CIPHER_LEN and JLibsodium.cryptoAeadChacha20poly1305IetfDecrypt).

---

Nitpick comments:
In `@framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java`:
- Around line 326-350: Update the two tests (testGetTriggerInputBurn96ByteCipher
and testGetTriggerInputBurn80ByteCipher) to validate the actual burn ciphertext
layout instead of only asserting non-null: call
wallet.getTriggerInputForShieldedTRC20Contract(req), extract the trigger input
bytes from the returned GrpcAPI.BytesMessage, locate the burn slot bytes within
that serialized trigger input (the same slot produced by buildBurnTrc20Params),
and assert that the burn slot length equals Encryption.BURN_CIPHER_RECORD_SIZE
(96); for the 96-byte case assert the returned 96 bytes exactly match the
original burnCipher, and for the 80-byte legacy case assert the first
Encryption.BURN_CIPHER_LEN (80) bytes match legacyCipher and the trailing bytes
up to Encryption.BURN_CIPHER_RECORD_SIZE are zero (zero-padded nonce/reserved
tail).
- Around line 380-413: After confirming Optional is present, unwrap the Optional
returned from Wallet.getNoteTxFromLogListByOvk (or inspect the populated
GrpcAPI.DecryptNotesTRC20.NoteTx.Builder) and assert that the decoded amount and
transparent address equal the original amount and toAddress used to build
logData; specifically verify the NoteTx amount field matches BigInteger value
(or its byte/uint representation) and the transparent address bytes (or address
string) match the injected toAddress bytes (skip prefix byte if the function
stores the 20-byte payload) to catch incorrect population of those fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 758b6518-9354-40d6-b538-655bb3757cd3

📥 Commits

Reviewing files that changed from the base of the PR and between da67899 and 290cbd4.

📒 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/zksnark/NoteEncDecryTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java
  • framework/src/main/java/org/tron/core/Wallet.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

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

4057-4065: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the decrypted address prefix before returning it.

The integrity check only covers the last 20 bytes of the address. A burn record with the right raw TVM address but a wrong prefix byte still passes, and Line 4063 then returns that unvalidated prefix in transparentToAddress. Rebuild the 21-byte address from the node prefix + logToAddress, or reject the record if plaintext[32] does not match the expected prefix.

Suggested fix
           if (logAmount.equals(decryptedAmount) && Hex.toHexString(logToAddress)
               .equals(Hex.toHexString(decryptedAddress))) {
             byte[] addressWithPrefix = new byte[21];
-            System.arraycopy(plaintext, 32, addressWithPrefix, 0, 21);
+            addressWithPrefix[0] = Wallet.getAddressPreFixByte();
+            System.arraycopy(logToAddress, 0, addressWithPrefix, 1, 20);
+            if (plaintext[32] != addressWithPrefix[0]) {
+              return Optional.empty();
+            }
             builder.setToAmount(logAmount.toString(10))
                 .setTransparentToAddress(ByteString.copyFrom(addressWithPrefix));
             return Optional.of(builder.build());
           }
🤖 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 4057 - 4065,
The code currently accepts the 21-byte prefix from plaintext[32] without
validating it, allowing a mismatched prefix to be returned via
builder.setTransparentToAddress; modify the logic in Wallet (around the
plaintext/amountArray/decryptedAddress check) to either (a) reconstruct
addressWithPrefix by prepending the expected node prefix byte to logToAddress
(i.e., use the known prefix constant + logToAddress) before calling
builder.setTransparentToAddress, or (b) validate that plaintext[32] equals the
expected prefix byte and reject the record if it does not; update the branch
that compares logAmount and logToAddress (and uses addressWithPrefix) to perform
one of these fixes so the returned transparentToAddress cannot carry an
unvalidated prefix.
🧹 Nitpick comments (1)
framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java (1)

384-422: ⚡ Quick win

Add a wallet-level legacy burn-log compatibility case.

This block verifies the new 96-byte record path, but it never exercises Wallet.getNoteTxFromLogListByOvk with a pre-upgrade log payload (80B cipher + 16 zero bytes). Since the parser now slices the first 12 bytes of that tail as the nonce, a dedicated regression test here would protect the actual compatibility path, not just the lower-level decrypt helper.

🤖 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/test/java/org/tron/core/zksnark/NoteEncDecryTest.java` around
lines 384 - 422, Add a legacy-compatibility subcase in
testGetNoteTxFromLogListByOvkBurnRoundTrip that constructs a pre-upgrade tail
(80-byte cipher + 16 zero bytes) and verifies Wallet.getNoteTxFromLogListByOvk
still decodes it: after you build the existing 96-byte random-nonce record,
create a legacyTail byte[] of length Encryption.BURN_CIPHER_RECORD_SIZE, copy
the first 80 bytes from record into legacyTail (leaving the final 16 bytes as
zeros), copy legacyTail into logData at the same offset (System.arraycopy(...,
64, ...)), invoke the private method via reflection exactly as with the
random-nonce case (Wallet.getNoteTxFromLogListByOvk) and Assert that the
returned Optional isPresent(), ensuring the legacy path is exercised alongside
the new path.
🤖 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.

Outside diff comments:
In `@framework/src/main/java/org/tron/core/Wallet.java`:
- Around line 4057-4065: The code currently accepts the 21-byte prefix from
plaintext[32] without validating it, allowing a mismatched prefix to be returned
via builder.setTransparentToAddress; modify the logic in Wallet (around the
plaintext/amountArray/decryptedAddress check) to either (a) reconstruct
addressWithPrefix by prepending the expected node prefix byte to logToAddress
(i.e., use the known prefix constant + logToAddress) before calling
builder.setTransparentToAddress, or (b) validate that plaintext[32] equals the
expected prefix byte and reject the record if it does not; update the branch
that compares logAmount and logToAddress (and uses addressWithPrefix) to perform
one of these fixes so the returned transparentToAddress cannot carry an
unvalidated prefix.

---

Nitpick comments:
In `@framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java`:
- Around line 384-422: Add a legacy-compatibility subcase in
testGetNoteTxFromLogListByOvkBurnRoundTrip that constructs a pre-upgrade tail
(80-byte cipher + 16 zero bytes) and verifies Wallet.getNoteTxFromLogListByOvk
still decodes it: after you build the existing 96-byte random-nonce record,
create a legacyTail byte[] of length Encryption.BURN_CIPHER_RECORD_SIZE, copy
the first 80 bytes from record into legacyTail (leaving the final 16 bytes as
zeros), copy legacyTail into logData at the same offset (System.arraycopy(...,
64, ...)), invoke the private method via reflection exactly as with the
random-nonce case (Wallet.getNoteTxFromLogListByOvk) and Assert that the
returned Optional isPresent(), ensuring the legacy path is exercised alongside
the new path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 905f3074-16d0-41a3-9fed-05d90c811b19

📥 Commits

Reviewing files that changed from the base of the PR and between 290cbd4 and 59ec401.

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

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