Pixa
GitHub

Transaction Lifecycle

Status: Live

Describes hived 1.30.0 and the public nodes' gateway at commit 4a271e8. Checked against api.pixagram.com on 2026-10-08.

A transaction is a signed list of operations. This page follows one from the program that builds it to the index that serves it back: nine stages, and at each one the checks the chain applies and the error a program sees when a check fails. It is written for developers who sign transactions themselves or debug a library that does; Developer Quickstart shows the same path with a library doing the work. What each operation requires is on Operations Reference; what the chain stores is on Data Model.

The nine stages

1 construct

2 serialize

3 sign

4 broadcast

5 node checks

6 pending, then in a block

7 applied

8 irreversible

9 indexed

Scroll sideways to see the whole diagram.Open full size
Stage Who Can still fail?
1-3 Your program, offline Only your own mistakes
4-5 The public node you send to Yes: every rule below, as an error in the JSON-RPC reply
6 The next witness, and every node re-applying pending transactions Yes, silently: a transaction accepted at stage 5 can still be left out
7-8 Every node A block can be replaced until it is irreversible
9 Indexers No, but each index lags by a different amount

1. Construction

A transaction has five fields:

Field Value Rule
ref_block_num the low 16 bits of a recent block's number The block must be one of the last 65,536 (TaPoS)
ref_block_prefix bytes 4 to 7 of that block's id, read as a little-endian 32-bit integer Must match the block the chain has at that number
expiration a UTC time, YYYY-MM-DDTHH:MM:SS After the head block's time; at most 24 hours ahead
operations one or more operations, applied in order Every operation must pass its own validate() (Operations Reference)
extensions [] Must be empty

Programs read the head block from condenser_api.get_dynamic_global_properties (head_block_number, head_block_id, time) and build the first three fields from it. dpixa sets the expiration 60 seconds after the head block's time (broadcast.ts:317-335). A transaction whose expiration is far ahead is still only valid for one hour from the moment the node first sees it, because the node applies an effective expiry of the earlier of the two (database.cpp:2602-2622).

Size. The serialized transaction must fit in maximum_block_size - 256 bytes: 2,096,896 bytes at the live block size of 2,097,152 (database.cpp:577-583). An artwork post of several hundred kilobytes fits; the public nodes' gateway, not the chain, is what limits a single HTTP request to about 1 MiB (stage 4).

Operations in one transaction. Operations are applied in order and the whole transaction succeeds or fails together. Since Hive's hardfork 28, which Pixa carries from block 1, one transaction may combine operations that need the posting authority with ones that need the active authority (stage 3).

2. Serialization

The chain signs and hashes bytes, not JSON. The bytes are the transaction's fields packed in order, little-endian, with variable-length integers for lengths and each operation preceded by its id, the index in the operation list (Operations Reference).

There are two encodings, and a signature is valid for exactly one of them:

Encoding Assets are written as Operations in JSON look like Who uses it
Legacy the symbol as text: PIXA, PXS, VESTS ["vote", {"voter": …}] dpixa, the app, cli_wallet, most Hive libraries
HF26 the numeric asset identifier (NAI), such as @@000000021 {"type": "vote_operation", "value": {"voter": …}} Hive's newer libraries

The legacy encoding is where Pixa and Hive part: a library that writes HIVE or HBD into the bytes produces a signature the Pixa chain rejects (Differences from Hive). The HF26 encoding carries no symbol text, so a library using it needs only the chain id and the PIX key prefix.

Two hashes come from the serialized bytes:

  • The transaction id is the first 20 bytes of the SHA-256 of the transaction without its signatures (transaction.cpp:21-64). It is what get_transaction and the account history key on, and it is the same for both encodings only when the two produce the same bytes, which for amounts they do not.
  • The signing digest is the SHA-256 of the chain id followed by the same bytes (transaction.cpp:29-35). The chain id, 706978616772616d followed by 48 zeros, is in every signature, so a transaction signed for Pixa cannot be replayed on Hive or on any test chain.

3. Signing

Each signature is a 65-byte recoverable secp256k1 signature of the signing digest, in canonical form (crypto.ts:191-200). The node recovers the public key from the signature and checks it against the authorities the transaction's operations require.

Which key. Every operation names the authority level it needs: posting (votes, posts, custom_json with posting auths, profile metadata), active (anything that moves tokens, witness and proposal votes, key changes below owner) or owner (changing the owner key, account recovery). Operations Reference lists the level for each. The rules the node applies (transaction_util.cpp:60-135):

  • Strict levels. An operation that needs the posting authority must be signed by a key in the posting authority. Signing a vote with the active key fails with Missing Posting Authority <account>. Before Hive's hardfork 28 a higher key stood in for a lower one; Pixa has never had that behaviour.
  • Thresholds. An authority is a weight threshold and a list of keys and accounts with weights. The signatures present must reach the threshold. Account authorities are followed at most 2 levels deep, across at most 125 accounts.
  • Extra signatures are allowed. A signature that no operation needs is ignored, not rejected.
  • At most 1,000 signatures per transaction, checked before any key is recovered, on the API path (chain_plugin.cpp:2095-2102) and on the P2P path (core_messages.cpp:189-195), and again when the transaction is pushed (database.cpp:585-595). The limit is enforced where the node receives a transaction, not in block validation, and it is new in 1.30.0.
Failure Error text
Wrong key, or wrong encoding so that the recovered key is not the signer's missing required posting authority: Missing Posting Authority <account> (or Active, Owner)
Signatures valid for neither encoding Transaction failed to validate using both new (hf26) and legacy serialization (chain_plugin.cpp:2092-2124)
More than 1,000 signatures Too many signatures - count = <n>, limit 1000

4. Broadcast

The signed transaction travels as JSON in a JSON-RPC call over HTTPS to a public node:

JSON{"jsonrpc":"2.0","method":"condenser_api.broadcast_transaction","params":[{ "ref_block_num": 2770, "ref_block_prefix": 2254617426, "expiration": "2026-10-08T12:00:00", "operations": [["vote", {"voter": "alice", "author": "bob", "permlink": "horus-portrait-1790597187843", "weight": 10000}]], "extensions": [], "signatures": ["20…"] }],"id":1}
Method What it returns
condenser_api.broadcast_transaction {} once the node has accepted the transaction into its pending list. It says nothing about inclusion in a block.
condenser_api.broadcast_transaction_synchronous The block number and position once the transaction is in a block, or an error. The public nodes answer it, but their gateway cuts any request at 30 seconds, so a transaction that waits longer returns HTTP 504 while still being processed.
network_broadcast_api.broadcast_transaction The same as the first, in the HF26 JSON form.

What the gateway does on the way in (Chain Architecture, nginx.conf:160-200):

  • It rejects a request body above about 1 MiB with HTTP 413. A transaction larger than that must go to a node without the gateway, such as your own (Run an API Node).
  • For a request that fits its buffer, about 10 KiB, it renames 19 Pixa field names to Hive's inside the body, including inside signed text, which is a known defect (Differences from Hive). A larger request is passed through untouched.
  • It routes by the method name. Broadcast methods go to hived.

What hived does on arrival. It parses the JSON, accepting either operation form, packs the transaction in the HF26 encoding and verifies the signatures; if that fails, it packs it in the legacy encoding and tries again (chain_plugin.cpp:2092-2124). Then it runs the checks of stage 5.

5. The node's checks

The node applies the transaction to a private copy of its state, in this order. The first failure is returned as the JSON-RPC error; nothing is kept.

# Check Rule Error contains
1 Size ≤ maximum_block_size - 256 bytes Transaction too large
2 Signature count ≤ 1,000 Too many signatures
3 Not a duplicate The id is not in a block or in the pending list Duplicate transaction check failed
4 Custom-operation rate At most 5 custom, custom_json or custom_binary operations touching one account in the pending list at a time (database.cpp:602-652) already submitted 5 custom json operation(s) this block
5 Expiration expiration after the head block's time and at most 24 hours ahead (TaPoS and expiration) transaction_expiration_exception
6 TaPoS ref_block_prefix matches the block at ref_block_num transaction_tapos_exception
7 Operation validity Each operation's validate(): field formats, limits, symbols The operation's own message, such as Title size limit exceeded.
8 Authority Stage 3's rules Missing Posting Authority …
9 State rules Each operation's evaluator: balances, intervals, existence of accounts and posts The evaluator's message, such as You may only post once every 5 minutes.
10 Resource Credits The paying account holds the credits the transaction costs (Resource Credits) Account: <name> has <n> RC, needs <m> RC. Please wait to transact or power up HIVE.

Resource Credits are the one check that is not part of consensus. A node enforces it on transactions it receives from clients; a transaction that reaches a witness by another route and is put in a block is valid even if the payer lacked credits (rc_utility.cpp:539-575). The error text still says HIVE: hived's messages are Hive's.

TaPoS and expiration

Transactions as proof of stake (TaPoS) ties a transaction to a block it has seen. ref_block_num holds the low 16 bits of that block's number and ref_block_prefix 4 bytes of its id, and the node looks the number up in a ring of the last 65,536 blocks, about 54 hours (database.cpp:2627-2639). Two consequences:

  • A transaction built against a block that is later replaced in a fork fails the check on the winning fork and is dropped, which is the point: it cannot be replayed on a chain the signer did not see.
  • ref_block_num is 16 bits, so a transaction built against a block more than 65,536 blocks old matches a different block and fails. Build against a recent block, which get_dynamic_global_properties gives.

Expiration is checked as now < expiration with the node's effective expiry: the earlier of the transaction's own expiration and one hour after the node first validated it (database.cpp:2602-2622). A transaction may state an expiration up to 24 hours ahead, but the node drops it from its pending list after one hour at most.

6. Pending, then in a block

An accepted transaction sits in the node's pending list and is sent to its peers over P2P. The next witness to produce a block takes pending transactions in order until the block is full and produces the block, normally within 3 seconds. Between blocks, every node re-applies its pending transactions on top of each new block.

Three things can happen to an accepted transaction before it reaches a block, none of which produces an error for the sender:

What Why How you see it
It is included The usual case It appears in a block; database_api.is_known_transaction returns true from the moment the node has it
It is dropped Re-application failed because the state changed: the post was deleted, the balance was spent by another transaction, the vote became identical to an existing one It never appears; is_known_transaction turns false once it expires
It waits The payer ran short of Resource Credits after acceptance: the node keeps retrying it until it expires (rc_utility.cpp:562-575) It appears late, or not at all

broadcast_transaction returning {} therefore means "accepted", not "done". A program that needs to know polls condenser_api.get_transaction with the id, or, within the gateway's 30-second limit, uses broadcast_transaction_synchronous.

7. Applied in a block

Every node validates the block and applies its transactions in order, running the same checks as stage 5 except the Resource Credit check. One exception: a node that does not produce blocks, unless started with p2p-force-validate, skips the signature and authority checks for transactions in blocks it receives over P2P, and relies on the witnesses who checked them (Consensus and Block Production). The operations' effects become the node's state: the vote's rshares, the transfer's balances, the post's place in its thread and its reward figures until payout. The post's text itself is never in the state; it stays in the block log, where indexers read it (Data Model).

The chain also writes virtual operations into the block's record: the rshares a vote produced (effective_comment_vote), a block's producer reward (producer_reward), a payout's parts (author_reward, curation_reward, comment_reward), a conversion filled, a power-down instalment paid (Operations Reference). Nobody signs them; they are the chain's own account of what a rule did, and account_history_api returns them alongside the signed operations.

8. Irreversible

A block is reversible until enough of the scheduled witnesses have built on it, then irreversible. While reversible it can be replaced by a competing block from another witness, and a transaction in it then goes back to pending and is re-applied, or dropped if it no longer passes. condenser_api.get_dynamic_global_properties reports both head_block_number and last_irreversible_block_num; the difference is how many blocks are still open. A program that pays out or ships something on the strength of a transaction waits until the block that holds it is at or below last_irreversible_block_num.

The API reflects this split:

Call Sees pending Sees reversible blocks Sees irreversible blocks
State reads: get_accounts, get_content, balances Yes, on the node that holds them Yes Yes
database_api.is_known_transaction Yes Yes Yes, until the transaction's own expiration has passed; after that, false (database.cpp:339-346, 3228-3236)
condenser_api.get_transaction, account_history_api.get_account_history, get_ops_in_block No No Yes; for a transaction not yet irreversible the first returns Unknown Transaction <id> (account_history_api.cpp:211)
block_api.get_block No Yes Yes

A read of an account's balance can therefore show a transfer that get_transaction does not yet know.

9. Indexed

Index What it indexes When Served through
Account history (account_history_rocksdb plugin on API nodes) Every operation and virtual operation, by account and by transaction id On irreversibility account_history_api, condenser_api.get_account_history, get_transaction, get_ops_in_block
HAF (a second hived writing into PostgreSQL) Every block, operation and account As blocks arrive; reversible blocks are kept in a separate set and promoted or discarded Not directly
Hivemind (reads HAF) Posts, votes, follows, reblogs, communities, notifications: the social view After HAF, as a forking application: it follows a replaced block back bridge.*, follow_api.*, tags_api.*, 23 condenser_api calls

Two details matter for programs. Hivemind reads custom_json operations it understands, such as follow and community, and ignores the rest, so a follow is a chain fact the moment it is in a block but a feed fact only once Hivemind has processed that block, usually a few seconds later. And Hivemind keeps notifications for 90 days; older interactions remain in the account history but leave bridge.account_notifications (Notifications).

Reading a transaction back

Shellrpc() { curl -s -X POST https://api.pixagram.com -H 'Content-Type: application/json' \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"$1\",\"params\":$2,\"id\":1}"; }

rpc condenser_api.get_dynamic_global_properties '[]'            # head and last irreversible block
rpc database_api.is_known_transaction '{"id":"<txid>"}'         # pending or recent: true / false
rpc condenser_api.get_transaction '["<txid>"]'                  # irreversible only; else "Unknown Transaction"
rpc condenser_api.get_ops_in_block '[949330, true]'             # only_virtual=true: the chain's own operations in a block

Compared with Hive

Aspect Hive Pixa
Transaction size hived enforces maximum_block_size - 256; the voted block size is 64 KiB the same rule; the voted block size is 2 MiB, so about 2,096,896 bytes
Signatures per transaction bounded only by the size, about 1,000 1,000, checked before key recovery (1.30.0)
Chain id beeab0de… 706978616772616d…
Encodings legacy and HF26 the same, with PIXA, PXS as the legacy symbols
Gateway Jussi (Python) in front of hived and Hivemind an OpenResty configuration that also renames fields; 1 MiB body limit

Sources