tronprotocol / tronprotocol/tips
TIP-935: Harden ECDSA Signature Validation
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 334
- Forks
- 339
- Avg merge
- 11m
- Merged PRs (30d)
- 2
Description
tip: 935
title: Harden ECDSA Signature Validation
author: federico.zhen@tron.network
discussions-to: https://github.com/tronprotocol/tips/issues/935
status: Draft
type: Standards Track
category: Core
created: 2026-09-15
Simple Summary
This TIP introduces a governance-activated strict validation mode for recoverable secp256k1 ECDSA signatures used by TRON transactions, blocks, and TVM recovery. It requires exact signature length and component bounds, hardens modular inversion, and rejects recovery to the point at infinity while preserving historical and high-S compatibility.
Abstract
TRON transaction and block protobufs encode recoverable ECDSA signatures as 32-byte unsigned r and s values followed by a recovery byte. Historical java-tron validation accepts encodings of at least 65 bytes and ignores trailing data, does not consistently enforce 1 <= r, s < n at the lowest recovery boundary, and can encode a recovered point at infinity as {0x00} before deriving an address.
This TIP introduces ALLOW_STRICT_ECDSA_VALIDATION (proposal code: TBD), gated by the next java-tron block version. After activation, transaction and block signatures must be exactly 65 bytes, scalars and recovery identifiers must be in range, and recovery must fail when the result is the point at infinity. The strict java-tron reference implementation uses Bouncy Castle BigIntegers.modOddInverse(n, r); other implementations may use semantically equivalent algorithms that preserve the input acceptance range, recovery results, and failure behavior required by this TIP. The point-at-infinity rule also applies to TVM ECDSA recovery without changing its ABI, energy cost, or high-S behavior. Historical consensus and pre-activation VM execution retain legacy behavior. Fresh transaction admission and the getTransactionSignWeight and getTransactionApprovedList queries use the exact-length rule independently of activation.
Motivation
The same untrusted signature data reaches transaction permission checks, block witness verification, and TVM recovery. Four inconsistencies should be addressed at this boundary.
1. Component bounds
ECDSA requires r and s in [1, n), where the secp256k1 group order is:
n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
Explicit bounds reject invalid inputs before curve operations and align transaction and block recovery with TVM validation. This TIP does not require low-S verification because signatures are excluded from the TRON transaction ID and existing tools may produce valid high-S signatures.
2. Unbounded encodings
Only the first 65 signature bytes are interpreted. Trailing bytes therefore consume bandwidth, memory, and storage without affecting the recovered signer. Historical padded signatures require legacy validation, but newly admitted and post-activation consensus signatures should use a fixed-width 65-byte encoding.
3. Modular-inversion robustness
Public-key recovery computes an inverse of attacker-controlled r. The strict path should use an implementation with resource behavior bounded by operand size without narrowing the valid scalar domain or changing the mathematical result.
4. Point-at-infinity recovery
Recovery computes Q = r^-1 * (sR - eG). Valid-range inputs can still make Q the point at infinity, which has no ordinary public-key coordinates. Encoding it as {0x00} and hashing an empty public-key payload produces a deterministic address derived from an invalid public-key recovery result. The invalidity is in the recovered public key, not the address's byte format. Strict recovery must reject the point at infinity before address derivation.
Specification
1. Activation
The proposal code for ALLOW_STRICT_ECDSA_VALIDATION is TBD.
- It defaults to
0and may be set to1only after the next java-tron block version passes. Once enabled, this parameter cannot be disabled by a subsequent governance proposal. - The maintenance block that changes the value from
0to1uses legacy rules. Strict consensus rules begin with the next block. - Consensus validation must use the proposal state associated with the block being validated, not the current head state applied retroactively.
- A positive signature-verification result may be reused only when the verified input and applicable rule set still match. The verified input includes the message digest, complete signature bytes, and crypto engine. Changes to signature bytes or
raw_datamust invalidate any verified state that no longer matches the input. At activation, implementations must version or invalidate entries that could otherwise bypass strict validation. - Asynchronous verification results must remain associated with the input and rule set used by the task. A task started under legacy rules must not, upon completion, create a result that can be incorrectly reused under strict rules, including when it completes after cache invalidation. Implementations may satisfy these requirements through versioning, invalidation, or task coordination.
Implementations must enforce the activation boundary during normal block processing and prevent verification results obtained under legacy rules from bypassing strict validation after activation.
The fresh-input admission rules and auxiliary-query signature-length checks in Section 4 are independent of this proposal.
2. Encoding and component validation
Transaction and block protobuf signatures use:
sig[0..31] r, unsigned big-endian
sig[32..63] s, unsigned big-endian
sig[64] recovery byte
java-tron converts this to the compact form [header || r || s]. After activation, transaction and block signatures must use a fixed-width 65-byte encoding. This requirement fixes the encoded length; it does not impose low-S verification or remove the supported recovery-byte aliases. Every other length is invalid. The check must occur before wire conversion, Base64 conversion, scalar construction, curve operations, modular inversion, or signature-task submission.
Let n be the secp256k1 group order. Strict validation requires:
1 <= r < n
1 <= s < n
For transaction and block protobuf signatures, recovery values retain the existing java-tron convention:
| Wire value | 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 |
All other transaction and block wire recovery values are invalid. Headers 31..34 are normalized by subtracting 4; every strict direct recovery entry must reject a normalized recId outside [0, 3]. These checks must precede point construction and inversion. High-S signatures remain valid.
TVM precompiles retain their existing input-specific recovery-value rules:
| TVM precompile | Accepted input v |
Conversion and normalized recId |
|---|---|---|
ECRecover |
27 or 28 in the last byte of its 32-byte v word; the preceding 31 bytes must all be zero |
No wire-byte compatibility conversion; recId = v - 27 |
ValidateMultiSign and BatchValidateSign |
0, 1, 27, or 28 in the signature's recovery byte |
Convert 0/1 to 27/28, retain 27/28, then set recId = v - 27 |
All other recovery values at these TVM inputs remain invalid before and after activation. In particular, the transaction/block compatibility mappings for 2..7 and 29..34 must not be applied to expand TVM acceptance. Each TVM entry point must enforce its own input rules before invoking shared strict recovery; the shared recovery boundary's [0, 3] range does not override the TVM entry points' restriction to normalized recId values 0/1.
3. Recovery
Every strict public-key recovery entry point must reject a null message digest or a digest whose length is not exactly 32 bytes before curve operations or modular inversion. This check applies to the digest passed to the recovery primitive and preserves the existing TVM input layouts and legacy recovery behavior.
For valid r, the strict java-tron reference implementation computes r^-1 mod n using Bouncy Castle BigIntegers.modOddInverse(n, r). Other implementations may use semantically equivalent algorithms, provided they preserve the input acceptance range, recovery results, and failure behavior required by this TIP. The inverse and recovered public key must match legacy recovery for every valid signature. The java-tron legacy path retains BigInteger.modInverse.
After candidate-point and subgroup checks, recover:
Q = r^-1 * (sR - eG)
If Q is the point at infinity, recovery must fail before public-key encoding, address derivation, or permission evaluation. A successful result must be an ordinary secp256k1 public key.
Malformed signatures must be rejected by the recovery primitive, which must not mutate on-chain state. A failed TVM recovery returns the precompile-specific failure result defined in Section 4; it does not by itself require the enclosing transaction to revert. The calling contract may handle that result and continue execution, including state changes, according to existing TVM semantics. Exception types and messages are implementation-specific. Validation proceeds from activation and encoding checks to component bounds, point construction, subgroup validation, inversion, final point validation, and only then address or permission evaluation.
4. Affected paths
After activation, Sections 2 and 3 apply, with the encoding and recovery-value rules specific to each entry point as defined in Section 2, to recoverable ECDSA validation that can influence consensus:
- transaction signatures evaluated for account permission weight;
- block witness signatures;
- TVM
ECRecoverand signature-recovery precompiles.
The exact 65-byte wire rule applies to transaction and block protobuf fields. TVM precompiles retain their fixed input layouts, ABI, and energy costs.
After activation, recovery to the point at infinity must be handled using each TVM precompile's existing signature-validation failure semantics:
ECRecoverreturns empty byte data.ValidateMultiSignreturns a 32-byte zero word representingfalse.BatchValidateSignreturns its existing 32-byte result array, with the failed signature's corresponding byte set to0. Other signatures are validated independently and retain their own results; a point-at-infinity recovery must not by itself clear the entire batch result or produce empty return data.
A point-at-infinity recovery is a signature-validation failure, not a precompile call failure; the precompile call-success flag remains unchanged. ABI errors, execution timeouts, and energy accounting retain their existing semantics; these signature-recovery failure rules do not override them.
Upon deployment of a java-tron version implementing this admission policy, upgraded nodes immediately enforce exact 65-byte signatures at RPC broadcast, P2P transaction ingress, and relay handshakes, independently of governance activation. Relay handshakes also use strict public-key recovery. These are admission policies: padded historical consensus encodings remain valid where legacy rules apply, but the same encoding cannot be newly admitted.
Upon deployment of the same version, getTransactionSignWeight and getTransactionApprovedList must check the length of every supplied signature and return SIGNATURE_FORMAT_ERROR in their query result if any signature is not exactly 65 bytes, independently of governance activation. This check applies to both crypto engines and must occur before wire or Base64 conversion, scalar construction, public-key recovery, or signature-task submission. These APIs must not truncate, normalize, or otherwise rewrite supplied signature bytes. Transactions with no signatures retain their existing query behavior; an empty signature list is distinct from a supplied zero-length signature, which must be rejected. Signatures of exactly 65 bytes proceed to the existing permission, weight, and address checks.
A successful auxiliary query does not establish that the transaction satisfies all broadcast admission or consensus requirements. Any transaction subsequently submitted for broadcast must independently satisfy the admission rules in this TIP, using the exact signature bytes being serialized for broadcast.
This TIP does not change PBFT message-signature validation or SM2 consensus recovery. The shared fresh-input length policy requires exactly 65 bytes for both crypto engines once the admission-policy version is deployed.
Rationale
One activation covers length, component bounds, inversion, and final-point validation because they protect the same consensus input boundary. A single transition avoids partially strict rule combinations and makes cache invalidation unambiguous.
The fixed encoded size of two 32-byte scalars plus one recovery byte is 65 bytes. Applying the rule to new admission and post-activation consensus preserves legacy validation of historical padded data. The two auxiliary queries also reject non-65-byte signatures upon deployment, so they no longer silently repair inputs that fresh admission would reject. This API behavior change does not alter historical consensus validation. Existing transaction and block recovery values 0..7 and 27..34 are retained to avoid an unrelated compatibility change. TVM precompiles preserve their narrower recovery-value rules; sharing strict scalar and public-key recovery checks must not broaden their accepted inputs.
Low-S signing remains recommended, but verification-time low-S enforcement is excluded. Because signature bytes are not part of the transaction ID, transforming (r, s, v) into (r, n - s, flip(v)) does not create a new transaction ID or bypass replay protection.
High-S transformations do change the serialized transaction and its Merkle leaf hash, which includes signature bytes. Validators must not rewrite signatures in received blocks through normalization or truncation; validation must preserve the signature bytes committed by the block.
BigIntegers.modOddInverse(n, r) is selected for the strict java-tron reference path because its work is bounded by operand size and it preserves the complete valid ECDSA domain. Consensus conformance depends on the input acceptance range, recovery results, and specified failure behavior, rather than a particular library call. Native acceleration and other semantically equivalent implementations must preserve all of these properties.
Scalar, candidate-point, and subgroup checks do not prove that the final Q has ordinary coordinates. The explicit Q.isInfinity() check is therefore independent and must occur at the recovery boundary so no caller can derive an address from {0x00}.
Backwards Compatibility
This TIP is a coordinated consensus change.
| Scenario | Required behavior |
|---|---|
| Historical or pre-activation consensus validation | Retain legacy rules |
| Fresh RPC, P2P, or relay admission | Require exactly 65 bytes regardless of proposal state |
getTransactionSignWeight and getTransactionApprovedList |
Upon deployment, reject any supplied non-65-byte signature with SIGNATURE_FORMAT_ERROR regardless of proposal state; preserve signature bytes and retain existing behavior for transactions with no signatures |
| Activating maintenance block | Use legacy rules; strict rules start with the next block |
| Cached legacy verification result | Version or invalidate before post-activation reuse |
| Valid high-S signature | Remain accepted |
| Transaction and block recovery values | Preserve wire values 0..7 and 27..34 and their existing mappings |
| TVM recovery values | Preserve ECRecover's zero-padded 27/28 word and ValidateMultiSign/BatchValidateSign's 0/1/27/28 signature byte; reject all other values |
| Pre-activation TVM point-at-infinity result | Retain the legacy result |
Post-activation ECRecover point-at-infinity result |
Return empty byte data |
Post-activation ValidateMultiSign point-at-infinity result |
Return a 32-byte zero word (false) |
Post-activation BatchValidateSign point-at-infinity result |
Set the corresponding result byte to 0 in the 32-byte result array; preserve independently validated results for other signatures |
| PBFT and SM2 consensus recovery | Unchanged |
Wallets, SDKs, hardware signers, exchanges, and signing services must be ready to emit signatures of exactly 65 bytes before nodes serving or relaying their transactions deploy the version implementing the strict admission policy. Clients that currently emit 66–68-byte signatures must remove trailing padding before that deployment; waiting until governance activation is too late because upgraded nodes already reject those inputs while the proposal remains disabled. Clients already emitting exactly 65-byte signatures need no change for the length requirement.
The auxiliary-query length policy is an API compatibility change: clients that rely on getTransactionSignWeight or getTransactionApprovedList to truncate padded signatures must instead supply exactly 65 bytes per signature before upgrading the nodes they query. These APIs no longer provide automatic signature repair, including when querying a historical transaction with padded signatures. Historical consensus validation remains unchanged where legacy rules apply.
Client signatures must also satisfy the strict component and recovery-value requirements before governance activation. Normal java-tron signatures already satisfy this format. The two enforcement milestones are distinct: node deployment enables strict fresh-input length admission and the two auxiliary-query length checks, while strict consensus validation begins with the block after the activating maintenance block. Nodes must preserve legacy consensus validation before that boundary.
Test Cases
Activation and caching
Required tests for normal activation:
- Process the activating proposal and verify that its maintenance block uses legacy rules while the next block uses strict rules.
- Validate sequential blocks across the activation boundary using the state associated with each block.
- Seed positive verification results before activation and verify that none bypasses strict validation afterward, including pending, re-push, popped, and pushing queues.
- Verify that transactions admitted before activation and included in blocks after activation satisfy strict validation, including blocks produced by the local node.
- Do not reuse a matching pending transaction whose cached verified state is absent or false.
- Verify that changing signature bytes or
raw_dataprevents reuse of a positive result for the previous input. Matching transaction IDs alone must not permit reuse for different signature bytes. - Start a verification task under legacy rules, then activate strict rules and perform the implementation's cache invalidation or version transition. Verify that task coordination prevents a late legacy result from being published as a strict result, or that any late result remains identified as legacy and cannot bypass strict revalidation.
Additional regression tests should cover fork switching and speculative signature verification across the activation boundary.
Encoding and components
- Before activation, verify legacy-compatible signatures of at least 65 bytes; after activation, reject lengths below or above 65.
- At fresh RPC, P2P, and relay admission, accept 65 bytes for further validation and reject every other length independently of proposal state.
- Reject
rorsequal to0,n, orn + 1; allow boundary values1andn - 1to proceed to recovery. - For transaction and block signatures, allow wire recovery values
0..7and27..34through recovery-byte validation with the specified mappings; reject all other byte values. Passing this check does not by itself guarantee successful public-key recovery. Reject normalizedrecIdvalues outside[0, 3]at strict direct recovery entries. - For
ECRecover, test before and after activation that only a zero-padded 32-bytevword ending in27/28passes recovery-value validation. Reject words ending in0..7or29..34, and words with a nonzero byte in the preceding 31 positions. - For
ValidateMultiSignandBatchValidateSign, test before and after activation that only signature recovery bytes0/1/27/28pass recovery-value validation, with0/1converted to27/28. Reject all other byte values, including2..7and29..34. Cover both constant-call and worker-pool execution forBatchValidateSign. - Use otherwise valid signatures to verify that the
0/27and1/28aliases recover identical signers at entry points that support those aliases. Verify that transaction/block aliases such as4/31remain invalid at TVM entry points. - Verify that a valid high-S transformation recovers the same signer.
Auxiliary queries and broadcast
- For both
getTransactionSignWeightandgetTransactionApprovedList, test withALLOW_STRICT_ECDSA_VALIDATIONdisabled and enabled. Reject every supplied signature whose length is not 65 bytes withSIGNATURE_FORMAT_ERROR; include lengths0,64,66, and68, and a list containing both valid-length and invalid-length signatures. Verify rejection before conversion, recovery, or signature-task submission. Cover both crypto engines. - Verify that an empty signature list retains existing query behavior, while a list containing a zero-length signature is rejected. Exactly 65-byte signatures must proceed to the remaining checks; correct length alone must not imply sufficient permission or successful validation.
- Construct two transactions with identical
raw_data: one with an otherwise valid 65-byte signature and the other with the same signature plus one trailing byte. Record the exact request and response transaction objects for both APIs. Despite matching transaction IDs, both queries and fresh admission must reject the padded input with the governance flag disabled or enabled. The 65-byte input must pass the length check and proceed to the remaining validation. - Verify that neither query rewrites supplied signature bytes. SDK integration tests should check the exact transaction being serialized for broadcast, reject invalid lengths without silently repairing them, and never transfer a successful query result to a different signature encoding based solely on a matching transaction ID.
Recovery
- For a deterministic scalar corpus, verify
r * inverse(r) mod n = 1and identical public keys and addresses across legacy, strict, independent conformant, and enabled native implementations. - Construct
R = G,r = x(G),s = 1, ande = 1. Verify that legacy recovery returns{0x00}and derives41dcc703c0e500b653ca82273b7bfad8045d85a470, while strict recovery fails before address derivation. - Reject null signature objects and null
rorscomponents before field access or curve work. - At every strict public-key recovery entry point, reject a null digest and digests of any length other than 32 bytes before curve operations or modular inversion. Include lengths
0,31, and33as boundary cases, and verify that a 32-byte digest passes the digest-length check and proceeds to the remaining validation. Preserve legacy recovery behavior and existing TVM input layouts. - Execute point-at-infinity recovery cases through each affected TVM precompile before and after activation. Preserve pre-activation legacy behavior; after activation, assert empty return data for
ECRecover, a 32-byte zero word forValidateMultiSign, and a0byte at the corresponding position inBatchValidateSign's 32-byte result array. Verify that the precompile call-success flag is preserved. - For
BatchValidateSign, test a batch containing a valid signature, a point-at-infinity signature, and another valid signature, with matching expected addresses. After activation, assert a 32-byte result beginning with01 00 01and zeros in the remaining positions. Run this case through both constant-call and worker-pool execution paths. - Verify that ordinary valid signatures recover identical signers before and after activation.
Implementation
The java-tron implementation should:
- add the one-way
ALLOW_STRICT_ECDSA_VALIDATIONproposal (proposal code: TBD), gated by the next java-tron block version, and expose its chain parameter; - select the proposal state associated with block and VM validation, with the activating maintenance block using legacy rules;
- centralize exact-length admission checks, replace signature truncation in
getTransactionSignWeightandgetTransactionApprovedListwith an exact-65-byte check returningSIGNATURE_FORMAT_ERRORindependently of governance activation, preserve existing query behavior for transactions with no signatures, and perform strict component checks before signature-task submission or curve work; - use
BigIntegers.modOddInverse(n, r)in the strict java-tron reference path, permit semantically equivalent implementations as specified in Section 3, and reject a final point at infinity while preserving the legacy path; - apply strict recovery to affected transaction, block, VM, and relay paths without changing high-S behavior, and preserve each TVM precompile's existing recovery-value validation before calling shared recovery;
- reject null signature objects and components at the public recovery boundary, and require a non-null, exactly 32-byte message digest at every strict public-key recovery entry point before curve operations or modular inversion;
- require an explicit positive verification result whose input and rule set match before reuse, invalidate results when their verified inputs change, and prevent late legacy-task results from bypassing strict validation after activation.
Implementation references:
Copyright
Copyright and related rights waived via CC0.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the java-tron transaction, block-witness, TVM recovery, admission, relay, and query paths listed in TIP-935, then inspect their existing signature validation and recovery behavior. Done means implementing the activation boundary, exact-length and component checks, strict recovery failure handling, and the specified precompile results while preserving legacy behavior before activation; the TIP names no files or tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- blockchain, cryptography, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100