The origin tracer is paused. The library is open and still growing.What’s paused, and when it returns

← All articles

Anatomy of a Signed Transaction

Key takeaways

  • A transaction has two parts: a raw_data envelope (the contract, a reference block, an expiration, a timestamp, a fee limit) and a signature array over it.
  • The transaction ID is the SHA-256 hash of the raw_data alone — it is fixed before anyone signs, and the signatures never change it.
  • The signature field is a list, not a single value. Each entry is a 65-byte recoverable ECDSA signature, and the Permission_id on the contract decides which permission — and which weight threshold — those signatures have to satisfy.
  • A transaction references a recent block and expires: unless it is packed within its window (60 seconds after creation by default, 24 hours at most), it is dropped and never lands.

Every action on TRON — a transfer, a contract call, a vote, a permission change — travels as a single object called a transaction. Most of what an on-chain reader wants to know is written on its face: who authorized it, under what authority, against which block, and how long it was allowed to live.

The object has two halves. One is a structured envelope of intent, unsigned and complete on its own. The other is an array of signatures wrapped around it. The two are computed in that order, and much of the forensic signal sits right at the boundary between them.

This article works through the structure field by field, using the protocol’s own definition. It stays on the envelope and its signatures; the catalog of contract types and the mechanics of account permissions each have their own article, linked below where they meet the structure.

The two parts: raw_data and the signature array

In the protocol definition, the top-level Transaction message carries raw_data in field 1 and signature — a repeated bytes, meaning a list — in field 2.

TRANSACTION — ONE OBJECT, TWO HALVES raw_data FIELD 1 THE ENVELOPE OF INTENT contract — holds Permission_id ref_block_bytes · ref_block_hash expiration · timestamp fee_limit · data (memo) EVERY FIELD CHOSEN BY THE BUILDER signature[ ] FIELD 2 REPEATED signature[0] · 65 B signature[1] · 65 B ECDSA OVER THE txID ONE PER CO-SIGNER [ SHA-256 ] txID = SHA-256(raw_data) THE ID NAMES THE ENVELOPE ALONE — SIGNATURES NEVER CHANGE IT KEY
The signatures wrap the envelope, but the transaction ID is taken from the envelope alone.

A third field, ret, holds the execution result the network fills in after the fact; it is not part of what the sender builds or signs.

The raw_data envelope is where intent lives. Its fields:

FieldWhat it holds
contractThe action itself — a typed contract (a transfer, a TriggerSmartContract, a vote), carried as a protocol Any so one envelope shape can hold any of the roughly forty contract types.
ref_block_bytes, ref_block_hashA reference to a recent block — the anti-replay anchor (see TAPOS, below).
expirationThe time after which the transaction “will no longer be packed.”
timestampThe creation time set by the builder.
fee_limitThe maximum Energy cost the signer will allow a contract call to burn.
dataAn optional memo.

The contract field also holds the Permission_id: a small integer, sitting inside the contract rather than on the outer envelope, that names which of the account’s permissions the signatures must satisfy. It defaults to 0, the owner permission. More on what that integer buys in the signature section below.

Everything in raw_data is chosen by whoever builds the transaction, and none of it is a signature yet. Authorization comes from the second half.

Where the txID comes from

The transaction ID is the SHA-256 hash of the serialized raw_data, and of nothing else.

The protocol’s signing procedure is explicit about the order of operations: “Convert the transaction’s raw data to byte[]. Hash the raw data using sha256. This gives you the Transaction ID.” The hash is taken over the envelope. The signatures are computed from that hash and attached afterward; they are never folded back into it.

A few consequences follow for anyone reading the chain.

The ID exists before any signature does. A builder can compute a transaction’s ID, hand the unsigned envelope to a co-signer, and know the final ID in advance.

And because the hash covers only raw_data, signatures cannot change the ID. Adding a second signature for multi-sig, or replacing one signature with another valid one, leaves the txID untouched; a single envelope keeps one canonical identity no matter who ends up signing it.

The flip side is that any edit to intent produces a different transaction. Change the amount, the recipient, the fee limit, the expiration — anything inside raw_data — and the hash changes, which means the ID changes. There is no such thing as the same transaction with a different amount.

The txID is a fingerprint of intent, taken before the question of authority is ever asked.

The signature array and multi-signature

The signature field is a list because a transaction can require more than one signer.

Each entry is produced the same way: the signer runs ECDSA on the secp256k1 curve over the txID, and the result is “65 byte in size — r 32 bytes, s 32 bytes and v 1 byte.” The trailing v byte makes the signature recoverable: a verifier can take the txID and the 65 bytes, recover the public key that produced them, then derive that key’s address. If the recovered address is one the account’s permission authorizes, the signature counts.

So the array is legible after the fact. You do not have to be told who signed; the signatures themselves yield the signing addresses.

Whether one signature is enough depends on the permission named by Permission_id. Id 0 is the owner permission, the default and the account’s highest authority. Ids 2 and up are active permissions — up to eight custom permission groups, each scoped to a chosen set of operations. The id of an active permission “is automatically assigned by the network in sequence starting at 2.” Id 1, the witness permission, is reserved for a Super Representative’s block production and cannot be used to sign an ordinary transaction.

Each key inside a permission carries a weight, and the permission sets a threshold. The rule is a sum: the operation is allowed only when the combined weight of the participating signatures meets or exceeds the threshold. A single key whose weight meets the threshold signs alone. A permission that spreads weight across several keys needs several signatures, gathered one at a time — one signer signs the envelope and passes it on, the next adds a signature, until the collected weight clears the bar.

[ SIGNATURE ARRAY · WEIGHT vs THRESHOLD ] signature[0] 65 BYTES · r s v signature[1] 65 BYTES · r s v [ RECOVER ] [ RECOVER ] TJmuRk…9Yt3 RECOVERED KEY · WEIGHT 1 TKppXe…7Rn2 RECOVERED KEY · WEIGHT 1 [ Σ WEIGHT ] Σ weight 2 ≥ threshold 2 AUTHORIZED — PERMISSION SATISFIED KEY Permission_id names the permission the weights must satisfy OWNER 0 · ACTIVE 2+ · WITNESS 1 (BLOCK PRODUCTION ONLY)
The array clears the bar by weight, not by count — one heavy key or several light ones.

For a reader, the array and the Permission_id together answer more than the “from” address alone can: which account acted, under which of its permissions, and with how many keys. A transaction signed under an active permission looks different from one signed under the owner key, and that difference is on-chain. The account model covers how those permissions are structured and changed; here the point is narrower. A signed transaction is a permission being exercised, and the signature array records it.

TAPOS: the reference block and the expiration

Two fields tie a transaction to a specific, recent moment on the chain. Together they are TRON’s implementation of TAPOS — Transaction as Proof of Stake.

ref_block_bytes and ref_block_hash are a compact pointer to a recent block: a slice of that block’s height and a slice of its hash. Their job is anti-replay. As the protocol puts it, the reference block “is used in the TRON TAPOS mechanism, which can prevent a replay of a transaction on forks that do not include the referenced block.” A transaction built against a block that a given fork never saw is invalid on that fork — it cannot be lifted out of one history and replayed into another. The valid reference is “the latest 65536 blocks, and generally, the latest solidified block is used.”

expiration puts the transaction on a clock. Past that time, it “will no longer be packed” — block producers refuse it. When a transaction is built through a node’s API, the node sets the expiration “to the value of adding 60 seconds to the timestamp of the node’s latest block.” That window is configurable, but “the maximum value cannot exceed 24 hours.”

[ TAPOS · REFERENCE BLOCK + EXPIRATION ] PACKABLE WINDOW +60S DEFAULT · ≤ 24H MAX CREATED EXPIRATION ref_block ONE OF LAST 65,536 BLOCKS Packed into a block LANDS ON-CHAIN Past expiration DROPPED · NEVER LANDS KEY EXPIRED = REBUILD: NEW REF BLOCK · NEW EXPIRATION · NEW txID
A transaction lives inside a window anchored to a recent block; miss the window and it is gone, not delayed.

The practical result is that a TRON transaction is a perishable object. It is bound to a fork by its reference block and to a clock by its expiration. If it is not included before the window closes — network congestion, a fee limit set too low for the contract to complete, a co-signer who never signs — it does not queue for later. It expires, unbroadcast to the ledger, and the sender has to build a fresh one with a new reference block, a new expiration, and therefore a new txID.

One field on that clock is easy to misread: timestamp is the builder’s creation time, not the confirmation time. The protocol notes it “does not need to be set, because the timestamp of the block which the transaction is included in is the actual on-chain timestamp.”

What the structure tells a reader

Taken apart, the envelope and its signatures answer questions the summary view of a transfer never surfaces.

Start with who actually authorized it. The signature array recovers to real addresses, so a transaction can be from one account and yet reveal, in its signatures, the set of keys (one, or several) that satisfied a permission. The count and the recovered addresses are evidence about control, independent of the sender field.

A Permission_id other than 0 adds the authority the signers acted under: an active or custom permission rather than the owner key. That is a standing arrangement made visible in a single transaction — someone delegated signing authority, and this is it being used.

The reference block and expiration bracket the transaction in time and fork. They record when it was built and that it was built against a specific recent block, which helps when the confirmation timestamp and the creation timestamp disagree, or when placing an action relative to a chain event.

On a contract call, fee_limit shows how far the signer was willing to go. It is a ceiling the signer chose before broadcasting, visible even when the call reverts, and it reads as intent: how much Energy value the author was prepared to spend on the attempt.

None of this requires trusting a label or a block explorer’s rendering. It is all recoverable from the transaction’s own bytes: an envelope that fixes intent and a name for it, and an array of signatures that records exactly which authority was exercised to send it.

Sources

  • Transactions — TRON Developer Hub — official reference for the raw_data fields, TAPOS reference-block semantics, and the expiration window (default +60s, 24-hour maximum). The live page no longer states the 65,536-block validity range; the archived revision below is cited for that.
  • Transactions — TRON Developer Hub, archived 2025-06-20 — the revision carrying the TAPOS and expiration wording quoted in the body: the reference block “is used in the TRON TAPOS mechanism, which can prevent a replay of a transaction on forks that do not include the referenced block”, a transaction past its expiration “will no longer be packed”, the node setting that expiration “to the value of adding 60 seconds to the timestamp of the node’s latest block,” the valid reference being “the latest 65536 blocks, and generally, the latest solidified block is used”, and “the maximum value cannot exceed 24 hours.” TRON has since rewritten the live page in place, so the last archived revision carrying this wording is cited alongside it.
  • Tron.proto — tronprotocol/protocol — the protobuf definition of the Transaction message: raw_data (field 1), signature (field 2, repeated bytes), and the Contract sub-message with its type, parameter, and Permission_id; also the authoritative ContractType enumeration behind the “roughly forty” contract-type count.
  • Procedures of transaction signature generation — tronprotocol/documentation — the signing procedure: hash raw_data with SHA-256 to get the txID, sign with ECDSA/secp256k1, 65-byte (r, s, v) recoverable signature.
  • Multi-Signature — TRON Developer Hub — permission IDs (owner 0, witness 1, active 2+), the Permission_id field on a contract, and the weight-versus-threshold rule for validating a set of signatures.
  • smart_contract.proto — tronprotocol/protocol — the protocol definition of TriggerSmartContract (owner_address, contract_address, call_value, data, call_token_value, token_id) — the message type this chapter names. Protocol-authoritative: a contract type is either declared in core/contract/ or it does not exist.