Skip to content

fix(sdk-coin-trx): emit canonical AccountCreate raw_data_hex#8836

Closed
bhavidhingra wants to merge 1 commit into
masterfrom
chalo-trx-account-create-canonical-encoding
Closed

fix(sdk-coin-trx): emit canonical AccountCreate raw_data_hex#8836
bhavidhingra wants to merge 1 commit into
masterfrom
chalo-trx-account-create-canonical-encoding

Conversation

@bhavidhingra
Copy link
Copy Markdown
Contributor

Summary

  • AccountCreateContract's enum value is 0 — the proto3 default for the outer Transaction.Contract.type field. The SDK explicitly encoded it, so raw_data_hex carried a stray 2-byte 0800 tag inside the contract envelope.

  • TRON's node re-serializes raw_data from broadcast JSON under strict proto3 semantics, which omits default-valued fields. Its canonical raw_data_hex is 2 bytes shorter, with a different sha256 (= txID).

  • For TSS wallets the signature is computed over sha256(SDK_raw_data_hex) but TRON validates against sha256(canonical_raw_data_hex). ECDSA recovery against the mismatched digest returns an unrelated pubkey, whose TRON address is not in the wallet's permission set, and broadcast fails with:

    SIGERROR Validate signature error: <sig> is signed by <random T…> but it is not contained of permission
    

    Hot-wallet flows sign-and-recover locally against the same buggy bytes and so don't trip this.

Why only AccountCreate

Builder type enum value Affected?
AccountCreateContract 0 (proto3 default) yes
TransferContract 1 no
FreezeBalanceV2Contract 11 no
VoteWitnessContract 4 no
DelegateResourceContract 57 no

Every other commonly-used TRON contract type has a non-default enum value, so the SDK's encoding happens to match TRON's canonical re-serialization. The analogous proto3-default guard for the inner resource: BANDWIDTH=0 field is already present in freezeBalanceTxBuilder.ts:175-181 with an explanatory comment — this PR applies the same idea to the outer type field for AccountCreate.

Fix

In getAccountCreateTxRawDataHex, omit the explicit type field from the txContract literal so protobufjs doesn't emit the default-valued tag. The decoded protobuf still reports type = AccountCreateContract because that's the field's default value.

Also removes the now-unused ContractType import.

Verification

Re-encoding the failure-case inputs (real production broadcast bytes) with the fix produces the canonical 132-byte raw_data_hex, and ECDSA-recovering the production signature against sha256(canonical) returns exactly the address TRON's error reported (TMAFWDcE5hpDvcQVn89gGB5oACwGXZKZqV) — confirming the diagnosis is precisely the failure mechanism.

Test plan

  • npm test -w modules/sdk-coin-trx -- --grep "proto3 default" — new regression test asserts raw_data_hex does not contain 5a68080012 (buggy framing with the default-valued enum tag) and does contain 5a661264 (canonical framing).
  • Existing AccountCreate builder tests pass unchanged (build, sign, round-trip, extendValidTo, multi-sign, txID stability).
  • End-to-end: TSS AccountCreate broadcast on testnet for an MPC wallet should now succeed (previously failed with the SIGERROR above).

🤖 Generated with Claude Code

AccountCreateContract's enum value is 0 — the proto3 default for the
outer Transaction.Contract.type field. The SDK's protobuf encoder
included this default-valued tag in the wire format (2-byte `0800`
inside the contract envelope), but TRON's node re-serializes raw_data
from broadcast JSON following strict proto3 semantics and omits
default-valued fields. The canonical raw_data_hex is therefore 2 bytes
shorter, with a different sha256 (txID).

For TSS wallets this manifested as a SIGERROR on broadcast:

  Validate signature error: <sig> is signed by <random T...> but it is
  not contained of permission

The TSS signature was valid for sha256(SDK_raw_data_hex), but TRON
validated it against sha256(canonical_raw_data_hex). With a mismatched
digest, ECDSA recovery returns an unrelated pubkey whose TRON address
is not in the wallet's permission set. Hot-wallet flows didn't trip
this because they sign-then-recover locally against the same buggy
bytes the SDK emits.

Other contract builders (Transfer=1, FreezeBalanceV2=11, VoteWitness=4,
DelegateResource=57, ...) all have non-default enum values, so their
SDK output matches TRON's canonical encoding by accident. AccountCreate
was uniquely affected. See the analogous proto3-default guard already
present in freezeBalanceTxBuilder.ts for the inner `resource: BANDWIDTH=0`
field.

Fix: drop the explicit `type` field from the txContract object in
getAccountCreateTxRawDataHex so the encoder omits it, matching the
canonical wire format TRON computes from the broadcast JSON. Includes
a regression test that asserts raw_data_hex does not include the
proto3-default tag and uses the canonical contract framing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bhavidhingra bhavidhingra requested a review from a team as a code owner May 22, 2026 15:20
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