Skip to content

fix(crypto): strengthen signature length validation#6776

Open
Federico2014 wants to merge 9 commits into
tronprotocol:release_v4.8.2from
Federico2014:fix/sig_length_limit
Open

fix(crypto): strengthen signature length validation#6776
Federico2014 wants to merge 9 commits into
tronprotocol:release_v4.8.2from
Federico2014:fix/sig_length_limit

Conversation

@Federico2014
Copy link
Copy Markdown
Collaborator

@Federico2014 Federico2014 commented May 18, 2026

Summary

  • Tighten signature-length validation across transaction, block, relay, and PBFT verification paths.
  • Centralize the bounds in ChainConstant.MIN_SIGNATURE_SIZE (65) and ChainConstant.MAX_SIGNATURE_SIZE (68), and centralize the predicate in SignUtils.isValidLength(int size, boolean checkMaxSignatureSize).
  • Gate the consensus/PBFT upper-bound checks with ForkController.signatureMaxSizeChecked(), which passes only after VERSION_4_8_2 fork stats meet the required threshold.
  • Unconditionally enforce the upper bound on admission paths that ingest fresh, non-historical signatures (Wallet.broadcastTransaction, TransactionsMsgHandler.check, RelayService.checkHelloMessage).
  • Preserve compatibility for read-only transaction introspection by keeping Wallet.getTransactionApprovedList on a lower-bound-only check, so historical transactions with padded signatures remain inspectable.

Design Notes

Why use [65, 68] instead of strict 65?

After the fork gate is active, the accepted signature-length window is [65, 68] bytes rather than strict == 65.

This is a deliberate compatibility trade-off:

  • A canonical ECDSA signature is exactly 65 bytes (r || s || v).
  • Historical on-chain data contains signatures with a small amount of trailing padding that ECDSA recovery silently ignores.
  • MAX_SIGNATURE_SIZE = 68 closes the previously unbounded encoding window while remaining compatible with historical data already on chain.
  • The long-term goal is to tighten the rule to strict == 65 once the compatibility window is no longer needed.

Because the bounds are centralized in ChainConstant and the predicate is centralized in SignUtils.isValidLength, tightening the rule later will be straightforward.

Why gate consensus paths with ForkController.signatureMaxSizeChecked()?

Consensus and block-validation paths must be able to replay history. The upper bound on these paths is enforced only after VERSION_4_8_2 passes through the existing fork-stats mechanism:

  • SignUtils.isValidLength(size, false) enforces only the lower bound.
  • SignUtils.isValidLength(size, true) enforces both the lower and upper bounds.
  • Block witness validation, transaction permission checks, and PBFT signature analysis use ForkController.signatureMaxSizeChecked().
  • ForkController.signatureMaxSizeChecked() delegates to ForkController.instance().pass(ForkBlockVersionEnum.VERSION_4_8_2).
  • The read-only lookup path in Wallet.getTransactionApprovedList intentionally passes false so historical padded signatures remain readable even after the fork is active.

Why enforce both bounds unconditionally on admission paths?

Admission paths see freshly created signatures, not historical chain state, so there is no need to keep a permissive window for replay:

  • Wallet.broadcastTransaction - local RPC entry; clients producing oversized signatures are misbehaving regardless of fork state.
  • TransactionsMsgHandler.check - P2P transaction ingress; an oversized signature here cannot come from already-on-chain data.
  • RelayService.checkHelloMessage - peer handshake signature, generated on demand from a fresh timestamp.

Enforcing [65, 68] unconditionally on these paths blocks malformed traffic at the edge without affecting historical chain replay.

Compatibility

This PR tightens validation behavior, but it does not apply the new upper bound uniformly to every path:

  • Before VERSION_4_8_2 passes, consensus/block/PBFT validation paths enforce only the lower bound (>= 65).
  • After VERSION_4_8_2 passes, consensus/block/PBFT validation paths enforce both bounds ([65, 68]).
  • Admission paths (Wallet.broadcastTransaction, TransactionsMsgHandler.check, RelayService.checkHelloMessage) enforce both bounds ([65, 68]) unconditionally.
  • Wallet.getTransactionApprovedList remains intentionally more permissive and continues to skip the upper bound so historical transactions can still be inspected.

This means admission policy can be stricter than pre-fork consensus replay. That is intentional because admission only handles fresh inbound data, while consensus replay must preserve historical compatibility.

Breaking Changes

The following public method signatures changed:

Method Before After
TransactionCapsule.checkWeight (Permission, List<ByteString>, byte[], List<ByteString>) (Permission, List<ByteString>, byte[], List<ByteString>, boolean)

All in-tree callers were updated.

External JVM consumers of the chainbase artifact, such as SDKs, signing services, and third-party tools, must update these call sites. Existing callers will fail at compile time rather than silently misbehave at runtime.

Main Code Changes

File Change
common/.../config/Parameter.java Add MIN_SIGNATURE_SIZE, MAX_SIGNATURE_SIZE, and VERSION_4_8_2/BLOCK_VERSION = 35
crypto/.../SignUtils.java Add isValidLength(int, boolean)
chainbase/.../ForkController.java Add signatureMaxSizeChecked() as the VERSION_4_8_2 fork gate
chainbase/.../DynamicPropertiesStore.java Remove the temporary ALLOW_TVM_OSAKA-based signatureMaxSizeChecked() helper
chainbase/.../TransactionCapsule.java Thread max-size checking through checkWeight; validateSignature uses the fork gate, while addSign validates existing signatures with an unconditional max-size check
chainbase/.../BlockCapsule.java Apply fork-aware signature-size validation to witness signatures in validateSignature
framework/.../Wallet.java Add unconditional sig-length check in broadcastTransaction; keep the read-only approved-list path lower-bound-only
framework/.../TransactionsMsgHandler.java Add unconditional sig-length check in check() for the P2P transaction ingress path
framework/.../RelayService.java Apply unconditional sig-length validation in checkHelloMessage
framework/.../PbftDataSyncHandler.java Apply fork-aware signature-size validation in ValidPbftSignTask
framework/.../PbftMsgHandler.java Use the shared fork-aware PBFT signature check
consensus/.../PbftBaseMessage.java Apply signature-size validation in analyzeSignature
consensus/.../PbftMessageHandle.java Use the shared fork-aware PBFT signature check
actuator/.../TransactionUtil.java Keep read-only checkWeight usage lower-bound-only

Test Coverage Added or Updated

  • ForkControllerTest
    • Verify ForkController.signatureMaxSizeChecked() uses real VERSION_4_8_2 fork stats: false below threshold, true once 80% stats pass.
  • TransactionCapsuleTest
    • Reject short signatures.
    • Reject padded signatures when max-size checking is enabled.
    • Preserve padded-signature acceptance when max-size checking is disabled.
  • BlockCapsuleTest
    • Reject short witness signatures.
    • Reject padded witness signatures when max-size checking is enabled.
    • Preserve padded witness-signature acceptance when max-size checking is disabled.
  • TransactionExpireTest
    • Keep padded signatures readable through getTransactionApprovedList after Osaka.
    • Reject short signatures on the read-only path.
    • Preserve padded-signature readability before Osaka.
  • TransactionUtilTest
    • Reject short signatures in getTransactionSignWeight.
    • Accept padded signatures on the read-only weight path after Osaka.
  • WalletMockTest
    • Reject short, oversized, and empty signatures on the broadcastTransaction admission path; accept [65, 68].
  • TransactionsMsgHandlerTest
    • Reject short and oversized signatures on the P2P ingress path; accept [65, 68].
  • RelayServiceTest
    • Reject oversized hello-message signatures unconditionally.
  • PbftDataSyncHandlerTest
    • Reject padded PBFT signatures when max-size checking is enabled.
  • PbftMsgHandlerTest
    • Reject padded PBFT message signatures when max-size checking is enabled.

Verification

  • ./gradlew framework:test --tests org.tron.core.ForkControllerTest
  • ./gradlew framework:test --tests org.tron.core.capsule.BlockCapsuleTest --tests org.tron.core.capsule.TransactionCapsuleTest --tests org.tron.core.net.messagehandler.PbftDataSyncHandlerTest --tests org.tron.core.net.messagehandler.PbftMsgHandlerTest
  • ./gradlew framework:checkstyleMain framework:checkstyleTest
  • git diff --check

@github-actions github-actions Bot requested a review from 3for May 18, 2026 03:05
Comment thread chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java
Comment thread framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java Outdated
Comment thread framework/src/main/java/org/tron/core/Wallet.java Outdated
@Federico2014 Federico2014 force-pushed the fix/sig_length_limit branch from 6185c26 to 3fa9b53 Compare May 18, 2026 07:01
@Federico2014 Federico2014 requested review from 3for and Sunny6889 May 18, 2026 07:03
Comment thread chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java Outdated
Comment thread chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java Outdated
Comment thread crypto/src/main/java/org/tron/common/crypto/SignUtils.java Outdated
Comment thread framework/src/main/java/org/tron/core/net/service/relay/RelayService.java Outdated
Comment thread framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java Outdated
Comment thread framework/src/main/java/org/tron/core/Wallet.java Outdated
Comment thread framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java
Comment thread framework/src/test/java/org/tron/core/net/messagehandler/PbftMsgHandlerTest.java Outdated
Comment thread actuator/src/main/java/org/tron/core/utils/TransactionUtil.java Outdated
Comment thread framework/src/main/java/org/tron/core/Wallet.java Outdated
@Federico2014 Federico2014 force-pushed the fix/sig_length_limit branch from 2a687f2 to ac7889a Compare May 19, 2026 06:05
@Federico2014 Federico2014 force-pushed the fix/sig_length_limit branch from 5897571 to 15bc00f Compare May 20, 2026 10:40
Comment on lines +508 to +512
for (ByteString sig : signedTransaction.getSignatureList()) {
if (!SignUtils.isValidLength(sig.size(), true)) {
String info = "Signature size is " + sig.size();
logger.warn("Broadcast transaction {} has failed, {}.", txID, info);
return builder.setResult(false).setCode(response_code.SIGERROR)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[DISCUSS] Now, if a node upgrades before VERSION_4_8_2 is activated, the networking layer will immediately and unconditionally reject oversized-signature transactions, while the consensus layer will only start rejecting them after VERSION_4_8_2 is activated. Was this inconsistency between the networking-layer and consensus-layer rejection conditions intentional by design?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes — this is intentional. The asymmetry follows from what each path actually sees:

  • Consensus / block / PBFT validation must be able to replay history. Some historical on-chain signatures carry trailing padding within [66, 68] bytes that ECDSA recovery silently ignores. If we unconditionally enforced [65, 68] on these paths, a node that upgraded before activation would reject otherwise-valid historical blocks and stall. That's why the upper bound is gated by ForkController.signatureMaxSizeChecked() (i.e. VERSION_4_8_2 fork stats), so the network reaches the new rule together.
  • Admission paths (Wallet.broadcastTransaction, TransactionsMsgHandler.check, RelayService.checkHelloMessage) only see freshly-produced signatures from clients and peers — never historical chain state. A client emitting a 69-byte signature today is misbehaving regardless of fork state, so there is no replay risk in rejecting it at the edge.

Net effect on an upgraded-but-not-yet-activated node: it stops accepting freshly-broadcast oversized signatures, but still replays historical blocks correctly. This is documented in the "Compatibility" section of the PR description; happy to fold a shorter version into the code comment near the admission check if that would make the intent more discoverable.

…ength_limit

# Conflicts:
#	framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants