Skip to content

TIP: Harden ECDSA Signature Validation #2

Description

@Federico2014
tip: <to be assigned>
title: Harden ECDSA Signature Validation
author: federico.zhen@tron.network
discussions-to: https://github.com/Federico2014/tips/issues/2
status: Draft
type: Standards Track
category: Core
created: 2026-08-04

Simple Summary

This TIP introduces one governance-activated rule set for recoverable secp256k1 ECDSA signatures used by TRON transactions and blocks. It bounds signature length, validates the r, s, and recovery-header components before elliptic-curve work, and requires the reference client to use a robust modular-inverse implementation. The change rejects malformed signatures consistently while preserving the ability to validate historical blocks and maintaining compatibility with high-S signatures.

Abstract

TRON transaction and block protobufs encode a recoverable ECDSA signature as a 32-byte unsigned r scalar, a 32-byte unsigned s scalar, and a one-byte recovery value. java-tron converts this wire representation into an internal compact representation containing a recovery header followed by r and s. Some historical signatures carry a small trailing padding. The current validation paths are inconsistent. Fresh transaction admission limits signature length, but consensus validation of historical blocks still accepts any length of at least 65 bytes and ignores all bytes after the first 65. The main transaction and block recovery path also lacks explicit 1 <= r, s < n checks at its lowest-level entry, while the TVM ECRecover path already checks these scalar bounds.

This TIP activates a unified consensus rule through proposal code 99, gated by java-tron block version 37 (VERSION_4_8_3). After activation, recoverable ECDSA signatures must be exactly 65 bytes, r and s must be non-zero scalars below the secp256k1 group order, and the normalized recovery identifier must be in [0, 3]. High-S signatures remain valid because TRON transaction IDs exclude signatures. Historical blocks retain their original validation rules. The strict java-tron recovery path uses Bouncy Castle BigIntegers.modOddInverse, while the legacy path retains BigInteger.modInverse.

Motivation

The main transaction-signature verification chain is shared by transaction permission checks and block witness verification:

signature bytes
  -> parse v, r, s
  -> recover public key
  -> derive signer address
  -> evaluate permission or witness identity

Three issues should be resolved together because they apply to the same untrusted signature input and affect consensus behavior.

Incomplete component validation

ECDSA requires r and s to be integers in [1, n), where n is the secp256k1 group order:

n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

The TVM ECRecover path already enforces these bounds, but the main public-key recovery entry historically checked only that the values were non-negative. Explicit bounds make the accepted domain unambiguous, reject invalid values before point decompression or modular inversion, and align all non-VM transaction and block verification paths.

java-tron signing already emits low-S signatures. This TIP does not require low-S during verification. Transforming (r, s, v) into (r, n - s, flip(v)) does not change the TRON transaction ID because signature bytes are not part of the ID. Enforcing low-S during verification could reject signatures accepted under existing rules or produced by third-party tools, while providing no additional protection against transaction-ID malleability or duplicate execution because signatures are excluded from the transaction ID.

Unbounded signature encodings

Only the first 65 bytes of an ECDSA signature are interpreted. Without an upper bound, an attacker can attach arbitrary trailing bytes that are carried through networking, protobuf parsing, memory allocation, and block storage even though they do not affect verification.

Historical chain data contains signatures with trailing padding bytes. A strict 65-byte rule would make those encodings impossible to validate if applied without block-height context. Height-aware governance activation preserves their validity during historical block validation while making the post-activation wire encoding canonical.

Modular-inversion robustness

Public-key recovery performs modular inversion on signature components supplied by untrusted peers. Consensus verification should not depend on input-specific performance characteristics of a particular runtime implementation. The reference client therefore needs a mathematically equivalent inverse implementation whose resource behavior is bounded by operand size without narrowing the valid ECDSA scalar domain.

Specification

1. Activation

A new on-chain governance parameter named ALLOW_STRICT_ECDSA_VALIDATION is introduced with numeric proposal code 99.

  • The parameter defaults to 0.
  • A proposal may set it to 1 only after java-tron block version 37 (VERSION_4_8_3) passes.
  • Once activated, it must not return to 0.
  • Before activation, consensus validation behavior remains unchanged.
  • The maintenance block that processes the approved proposal is validated using the pre-transition rule set. If processing that block changes the parameter from 0 to 1, Sections 2 through 4 apply beginning with the next block.

Implementations must evaluate the activation state from the chain state associated with the block being validated. They must not apply the current head-state rule retroactively while validating a pre-activation block.

A successful signature-verification cache entry is valid only for the rule set under which it was computed. When the parameter transitions from 0 to 1, implementations must either version cache entries by validation rule or invalidate all cached positive verification results that may be reused for block processing. Matching transaction identifiers or signature bytes alone is not sufficient to reuse an entry whose verification state is absent, false, or associated with the legacy rules.

2. Recoverable signature encoding

For transaction and block protobuf fields, the 65-byte wire representation is:

sig[0..31]   r, unsigned big-endian
sig[32..63]  s, unsigned big-endian
sig[64]      recovery byte

java-tron converts the wire representation through Rsv.fromSignature into the internal compact representation used by ECKey recovery:

compact[0]       recovery header
compact[1..32]   r, unsigned big-endian
compact[33..64]  s, unsigned big-endian

A wire recovery byte in 0..7 is converted to an internal header in 27..34 by adding 27. A wire value already in 27..34 is retained for legacy compatibility.

After activation, consensus validation must require:

L = 65

Every other length is invalid. No trailing bytes are permitted after activation.

The length check must occur before wire-to-compact conversion, Base64 conversion, scalar construction, curve-point decompression, modular inversion, or signature-task submission.

3. Scalar and recovery-header validation

Let n be the secp256k1 group order. Parse r and s as unsigned big-endian integers and require:

1 <= r < n
1 <= s < n

The wire recovery byte and internal compact header use the existing java-tron convention:

Wire sig[64] Internal compact header Normalized recId
0..3 27..30 0..3
4..7 31..34 0..3
27..30 27..30 0..3
31..34 31..34 0..3

Every other wire value or internal header is invalid. Internal headers 31..34 are normalized by subtracting the legacy compressed-key marker before calculating recId. After normalization, recId must be in [0, 3] at every public recovery entry, including direct internal callers.

These checks must complete before x = r + floor(recId / 2) * n, curve decompression, or r^-1 mod n is evaluated.

This TIP deliberately does not require s <= n / 2. High-S signatures remain accepted. Signers should continue emitting low-S signatures.

4. Affected verification paths

After activation, Sections 2 and 3 apply to every recoverable ECDSA signature that can influence consensus, including:

  • transaction signatures evaluated for account permission weight;
  • block witness signatures.

The java-tron reference implementation enforces the exact 65-byte rule at fresh-input admission independently of governance activation, including RPC transaction broadcast, P2P transaction ingress, and relay handshakes. This is a local admission policy rather than a consensus rule: an upgraded node rejects a newly submitted padded signature before activation but must still accept the same encoding when it occurs in a valid pre-activation historical block.

Read-only APIs must continue to return historical transactions without revalidating their signatures under the post-activation rules.

This TIP does not change SM2 consensus verification or scalar and recovery semantics. The java-tron admission entry points use a shared signature-length policy, so the exact 65-byte admission rule applies to both crypto engines. TVM ECRecover is unchanged and retains its existing Ethereum-compatible input and high-S behavior.

5. Modular-inverse hardening

For valid r, public-key recovery computes r^-1 mod n. This computation must be mathematically identical before and after activation: the same valid signature and message hash must recover the same public key and address.

The strict java-tron recovery path uses Bouncy Castle BigIntegers.modOddInverse. Its fixed-divsteps structure is selected as the conservative default for a consensus-critical verification path. This choice does not claim that execution on the JVM is strictly constant-time.

BigIntegers.modOddInverseVar produces the same mathematical result and may be faster for public inputs, but the number of computation steps varies with r. It is not selected for the initial implementation. A future optimization may adopt it after conformance and resource-behavior testing.

The pre-activation and historical legacy path retains BigInteger.modInverse, while the strict path selects BigIntegers.modOddInverse only after the scalar-range checks succeed. The inverse transition therefore occurs together with the governance-activated strict rule set.

The reference implementation uses the pure-Java Bouncy Castle path for portability. A native implementation may be used as an optimization only if the conformance tests in this TIP produce the same recovered public keys and addresses.

6. Validation order and failure behavior

Implementations must validate in this order:

  1. activation state for the block being validated;
  2. encoded wire length;
  3. wire recovery-byte conversion, internal header validation, and normalized recId;
  4. r and s scalar ranges;
  5. curve-point construction and subgroup checks;
  6. modular inverse and public-key recovery;
  7. address and permission evaluation.

Malformed signatures fail validation without mutating state. Error text and exception classes are implementation-specific and are not consensus fields.

Rationale

One activation for one verification boundary

Length, scalar bounds, recovery-header bounds, and safe recovery all protect the same untrusted input boundary. Activating the consensus-visible checks together avoids combinations where one node rejects an encoding based on length while another reaches a different component or recovery path.

Why exactly 65 bytes

A recoverable TRON ECDSA wire signature contains exactly two 32-byte scalars and one recovery-value byte. Accepting trailing bytes creates multiple wire encodings for the same signature and consumes bandwidth and storage without affecting verification. Known historical padded signatures remain valid because the strict rule is selected using the proposal state at the block being validated, not the current head state.

Why high-S remains valid

Low-S signing is good hygiene and remains the default. Verification-time low-S enforcement is not included because TRON transaction IDs exclude signatures. Signature malleation therefore does not create a second transaction ID, bypass replay protection, or enable a double spend. Rejecting high-S would nevertheless invalidate some signatures produced by third-party tools and would add a larger migration burden than the security benefit justifies.

Why reject malformed inputs before elliptic-curve work

Length and integer comparisons are cheap and deterministic. Performing them first avoids memory, curve decompression, subgroup multiplication, and modular inversion for inputs that can never be valid. It also prevents direct internal callers from bypassing validation performed by a higher layer.

Why harden the modular inverse without narrowing valid scalars

Arbitrary restrictions on otherwise valid scalars would violate ECDSA and create signing interoperability failures. The strict path replaces the inverse implementation without narrowing the valid signature domain and provides predictable resource behavior for inputs that pass the scalar checks. Keeping BigInteger.modInverse on the legacy path preserves pre-activation and historical behavior. Pure Java is selected for the reference strict path to avoid making native library availability a consensus prerequisite; native secp256k1 remains a permitted conformant acceleration.

Why verification caches are invalidated at activation

A cached TransactionCapsule.isVerified result depends on the validation rules active when it was computed. A transaction accepted under legacy rules must not bypass strict validation merely because the same transaction identifier and signature bytes appear after activation. The java-tron implementation clears positive verification state across pending, re-push, popped, and pushing transaction queues when the proposal changes from 0 to 1, and defensively reuses a pending result only when the cached capsule is explicitly verified and its signatures match.

Why read-only signature APIs retain legacy validation

GetTransactionApprovedList and GetTransactionSignWeight are introspection APIs that may be used with transactions confirmed before activation. Applying the current head strictness flag would retroactively classify some historically valid padded or non-canonical signatures as invalid. These APIs therefore retain legacy-compatible signature parsing and weight calculation instead of acting as a guarantee that the same encoding can be newly broadcast after activation. If clients need current-rule preflight validation, it should be exposed separately without changing historical lookup behavior.

Why the existing 27 through 34 internal header range is preserved

This TIP preserves the existing java-tron recovery-header behavior rather than expanding the accepted domain. The legacy ECKey.signatureToKeyBytes implementation has long rejected only header < 27 || header > 34, while internal headers 31..34 are normalized by subtracting the legacy compressed-key marker before calculating recId. In the transaction and block wire representation, Rsv.fromSignature converts recovery bytes 0..7 into internal headers 27..34 by adding 27 and retains wire values already in 27..34.

Accordingly, internal headers 27..34 are all part of the pre-existing accepted format. Strict validation retains this internal header range and verifies that the normalized recId is in [0, 3]; it does not introduce 31..34 as new values or narrow compatibility to 27..30. Restricting post-activation transactions to wire v=0..3 or internal headers 27..30 would be a separate consensus and migration decision.

Backwards Compatibility

This TIP is a consensus change and requires coordinated governance activation.

Scenario Required behavior
Pre-activation historical block validation Legacy consensus rules remain unchanged
Fresh RPC, P2P, or relay admission on an upgraded java-tron node Exactly 65 bytes are required regardless of proposal state
Maintenance block that changes the parameter from 0 to 1 The block itself is validated under the pre-transition rules
First block after the activating maintenance block Strict consensus rules apply
Cached positive verification result created before activation Must be invalidated or bound to the legacy rule version before reuse
Post-activation signature of exactly 65 bytes with valid components Accepted if public-key recovery and permission checks succeed
Post-activation signature of any other length Rejected
Post-activation r or s outside [1, n) Rejected
High-S signature with otherwise valid components Remains accepted
Historical read-only lookup after activation Must remain inspectable
TVM ECRecover Unchanged
SM2 consensus verification Unchanged; the shared fresh-input length policy still requires exactly 65 bytes

Wallets, SDKs, hardware signers, exchanges, and signing services must emit exactly 65 bytes, valid r and s, and a supported recovery value before activation. java-tron already emits low-S, 65-byte signatures, so normal node-generated transactions require no migration. Upgraded java-tron nodes reject newly submitted padded signatures through admission policy even before the consensus proposal activates.

Nodes that apply the post-activation consensus rules before governance activation can disagree with unupgraded nodes on block validity. Implementations must therefore gate the new consensus rules and advertise the minimum compatible release before the proposal vote.

Test Cases

Length boundaries

Consensus validation must use the rule associated with the block being validated:

Length Pre-activation consensus Post-activation consensus
64 Reject Reject
65 Accept Accept
66 Legacy-compatible Reject
67 Legacy-compatible Reject
68 Legacy-compatible Reject
69 Legacy-compatible Reject
1 MiB Legacy-compatible only when permitted by the surrounding historical block and transaction size rules Reject before allocation-heavy recovery work

The java-tron fresh-input admission policy is independent of the proposal state:

Admission path 65 bytes Any other length
RPC transaction broadcast Accept for further validation Reject
P2P transaction ingress Accept for further validation Reject
Relay handshake Accept for further validation Reject

The 66-to-69-byte consensus vectors must contain valid first 65 bytes and trailing bytes, proving that padded signatures remain valid before activation but are rejected after activation. Separate admission tests must prove that the same padded inputs are rejected by upgraded nodes before and after proposal activation.

Component boundaries

For both transaction and block-signature paths, cover:

  • r = 0, r = n, and r = n + 1: reject after activation;
  • s = 0, s = n, and s = n + 1: reject after activation;
  • r = 1 and r = n - 1: proceed to normal recovery checks;
  • s = 1 and s = n - 1: proceed to normal recovery checks;
  • valid high-S (r, n - s, flip(v)): recover the same signer and remain accepted;
  • wire recovery bytes 0..7 and 27..34: convert to internal headers 27..34, normalize to recId in [0, 3], and proceed;
  • invalid wire recovery bytes such as 8, 26, and 35: reject;
  • direct compact-header calls with header 26 or 35: reject;
  • direct recovery calls with recId = -1 or recId = 4: reject before curve operations.

Modular-inverse conformance

For a deterministic corpus of boundary and generated valid scalars, the replacement inverse must satisfy r * inverse(r) mod n = 1 and recover exactly the same public key and address as a conformant independent secp256k1 implementation. Resource-behavior tests are non-consensus tests and must not be used to decide signature validity.

Consensus and compatibility paths

  • Process the activating proposal in a maintenance block and verify that the maintenance block uses legacy rules while the following block uses strict rules.
  • Validate blocks across the activation boundary and verify that each height uses the proposal state associated with that block.
  • Reject post-activation transactions and blocks containing any non-65-byte signatures.
  • Seed verified transactions in the pending, re-push, popped, and pushing queues before activation; after the 0 to 1 transition, verify that none retains a reusable positive verification result.
  • Verify that block processing does not reuse a pending transaction when its cached isVerified state is false, even if the transaction identifier and signatures match.
  • Keep historical padded transactions available through read-only approval and weight APIs.
  • Verify that upgraded RPC, P2P, and relay admission rejects padded signatures independently of the governance parameter.
  • Verify identical recovered addresses across the legacy inverse, the strict pure-Java inverse, and any enabled native implementation for a corpus of valid random signatures.

Implementation

The java-tron implementation should:

  1. assign proposal code 99 to the one-way ALLOW_STRICT_ECDSA_VALIDATION parameter and gate proposals on block version 37 (VERSION_4_8_3);
  2. expose the state through the getAllowStrictEcdsaValidation chain parameter;
  3. centralize the exact 65-byte admission rule while keeping consensus validation fork-aware;
  4. read the proposal state associated with block validation for transaction and block consensus paths;
  5. apply wire length, recovery-header, normalized recId, and scalar checks before public-key recovery;
  6. keep read-only historical introspection independent of current-head strictness;
  7. select Bouncy Castle BigIntegers.modOddInverse in the strict recovery path while retaining BigInteger.modInverse on the legacy path;
  8. invalidate cached positive transaction-verification state across pending, re-push, popped, and pushing queues when activation changes from 0 to 1, and require an explicit verified state before cache reuse;
  9. add activation, boundary, cache-transition, historical-block validation, and modular-inverse conformance tests.

Implementation references:

Copyright

Copyright and related rights waived via CC0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions