The life of a transaction
For platform engineers.
In short
A transaction in CryptoRail is a persisted state machine. Your system submits an intent (what to call, on which network, from which identity, with an idempotency key); CryptoRail answers at once and then takes the operation through policy, approval, preparation, signing, broadcast and confirmation, recording every step. Nonces, fees, stuck transactions, dropped transactions and reorganisations are handled inside that machine, and every failure ends with a report that says where, why and what to do.
This page explains each stage and what it guarantees. The developer guide has the calls and the configuration.
The state machine, as data
Every legal change of state is one row of a transition table, with a guard and a named cause. A change is persisted before anything that depends on it happens; a change the table does not contain is refused. There are no state changes scattered through the code, so the diagram below is the behaviour, not an approximation of it.
Every operation follows one state machine written as a transition table: each change of state is persisted with its cause and time before anything depends on it, and a transition the table does not allow is refused.
stateDiagram-v2
[*] --> RECEIVED
RECEIVED --> REJECTED_BY_POLICY
RECEIVED --> PENDING_APPROVAL
PENDING_APPROVAL --> PREPARED
RECEIVED --> PREPARED
RECEIVED --> PREPARATION_FAILED
PREPARED --> PENDING_SIGNATURE
PENDING_SIGNATURE --> SIGNED
PENDING_SIGNATURE --> SIGNING_REJECTED
SIGNED --> BROADCAST
SIGNED --> BROADCAST_FAILED
BROADCAST --> MINED
BROADCAST --> STUCK
BROADCAST --> DROPPED
STUCK --> PREPARED
DROPPED --> SIGNED
MINED --> CONFIRMED
MINED --> REVERTED
MINED --> BROADCAST
CONFIRMED --> [*]
The diagram shows the main paths; the full table adds approval rejection and expiry, signing expiry, cancellation from every state before and after broadcast, and the re-nonce of a displaced transaction. Three properties matter most:
- Waiting for people costs nothing. An operation waiting for approval holds no nonce and no fee. The nonce is taken when the operation is prepared, just before it is signed.
- What is on the network is never withdrawn. A broadcast transaction is replaced at its nonce or
followed to its receipt; a reorganisation that removes its block sends it back to
BROADCAST. CONFIRMEDis final. It is reached only when the network’s finality rule is met, and it is never revisited.
Every change of state of your own operations arrives through the same channel as the contract events.
The full history of every operation, every transition and every attempt with its time and cause, can be queried.
Your business operation travels with its transactions: its type, its id and its step appear in queries, events and traces.
Intake and idempotency
Submission is asynchronous: the intent is persisted, the operation gets its id, and the answer comes back without waiting for policy, signature or chain. The idempotency key is what makes a retry safe: the same key with the same intent returns the same operation; the same key with a different intent is refused as a conflict. Through the service the key is a required header on every call that changes something, and the stored answer is replayed.
A submission is answered at once with the operation's id; progress is read through queries and events, and the API never waits for the chain.
Submitting the same intent twice with the same idempotency key returns the same transaction: a retry never becomes a second payment.
Before anything is prepared, the policy engine decides: allow, deny (a terminal state with no node call), or require approval. Approvals are covered in the signing deep dive.
Declarative policies decide before anything happens: limits per operation, payer and period, held even when several workers decide at once.
Preparation: the checks before any signature
Preparation runs in a fixed order: the call is encoded against the contract’s ABI, the gas is quoted
by simulating the call, the payer’s balance is compared with value plus worst-case gas, and the nonce
is taken last. A failure in any step ends the operation in PREPARATION_FAILED with nothing signed
and no hole left in the nonce sequence.
Call a function by the contract's name in your registry: arguments checked against the ABI before anything is signed, and the contract's own errors decoded when it refuses.
Each operation is simulated before it is signed: a call that would revert, a fee above the cap or a payer without enough funds fails with the reason before any signature, and the nonce is taken last.
Nonces and gap repair
On one sending address the nonce sequence is the order of execution, and a single missing nonce blocks everything behind it. CryptoRail allocates nonces per network and address under an exclusive lock in the database, so two workers, or two processes, never hand out the same one. On startup and periodically it reconciles its own count with the node’s, adopting the node’s when the key was used outside CryptoRail.
flowchart TD
R[reserve: lock network and address] --> P[persist next nonce before returning]
P --> S{transaction ends without broadcast?}
S -->|signing refused or expired, cancelled| G[nonce given back]
S -->|broadcast| OK[nonce consumed on chain]
REC[periodic reconciliation with the node] --> GAP{a reserved nonce nobody holds?}
GAP -->|yes| AL[gap alert to the operator]
AL --> F[release it, or fill it with a zero-value self-transfer]
Every path that ends before broadcast gives its nonce back on its own. What is left, a reservation whose process died at the wrong moment, is raised as an alert and repaired by an operator: released, or filled with a zero-value transfer. Filling is deliberately manual, because it spends gas to undo something that should not have happened and doing it silently would hide the defect.
Nonces strictly increasing per sending address, gaps detected and filled, and never two transactions mined for one operation.
Several workers on the same database without sending anything twice.
Ordered batches from one identity, from the library: consecutive nonces, and the steps behind a failed one cancelled when you ask for it.
Gas policies and priority tiers
Fees come from the network’s gas policy and from the sending identity’s priority tier, never from a caller’s guess. Each network declares one policy; each identity is bound to one tier of its network.
| Gas policy | Fees | Replacement of a stuck transaction |
|---|---|---|
| EIP-1559 market | From recent blocks, with the tier’s multiplier and percentile | Same nonce, fee raised by the tier’s bump |
| Legacy market | Gas price from the node | Same nonce, price raised by the tier’s bump |
| Fixed price | A constant gas price | Same nonce, price raised by the tier’s bump |
| Free (consortium networks) | Zero; no fee-market query, no gas balance needed | Rebroadcast, or a fresh zero-fee attempt at the same nonce |
A tier (low, normal, high, urgent) sets how aggressive the fee is, the cap it may never
exceed, when a transaction counts as stuck, how much each replacement raises the fee, how many
attempts it gets, and what happens when they run out. Urgency belongs to the identity, not to the
single operation, because nonces are mined in order per address: mixing tiers in one address would
let a cheap transaction block an urgent one. A team that needs several urgency levels uses several
identities. When a third-party contract authorises exactly one sender, an operation may instead ask
to preempt: it takes the lowest nonce of the queue, and the transactions it displaces are
re-signed one nonce higher, keeping their identity and their tier. Preemption needs an explicit
policy allowance.
Fees follow the network's gas policy (EIP-1559 market, legacy market, fixed price or free) and the sending identity's tier, never the caller's guess; an override is allowed only under the cap.
Priority tiers per sending identity with fee caps you set; an urgent operation can go ahead of a queue already on the network.
Permissioned consortium networks with free gas (Hyperledger Besu with QBFT): no fee market, no gas balance to keep topped up.
The actual cost of every operation is recorded, and a payer running low on funds raises an alert before operations fail.
Broadcast, stuck, dropped and cancelled
A node can refuse a transaction, ignore it, or forget it. Each case has one defined answer.
flowchart TD
B[BROADCAST] --> M{mined within the tier's threshold?}
M -->|yes| MI[MINED]
M -->|no| S[STUCK]
S --> P{market fees, attempts and cap left?}
P -->|yes| R[new attempt, same nonce, higher fee]
P -->|no, keep bumping| W[stay at the cap and rebroadcast]
P -->|no, abandon| C[cancel: zero-value transfer at the same nonce]
R --> B
B --> D{node forgot it?}
D -->|yes| DR[DROPPED: rebroadcast the same signed bytes]
DR --> B
- Refused at broadcast. The refusal is classified: recoverable ones are rebuilt and retried, unrecoverable ones end the operation with the reason and give the nonce back.
- Stuck. Replaced at the same nonce with a higher fee, within the cap you set. Every attempt is tracked until one is mined; that one becomes the transaction’s hash and the others are marked superseded, so a replacement never executes twice.
- Abandoned. A tier that gives up does not stop tracking: an unmined attempt keeps its nonce and would block everything behind it, so abandoning sends a zero-value transfer at the same nonce.
- Dropped. Rebroadcast with the same signed bytes, then rebuilt at the same nonce.
- Cancelled. Before broadcast nothing reaches the network; after it, cancelling is a race between a zero-value replacement and the original, and the final state says which one won.
A node's refusal is classified: a recoverable one (underpriced, stale nonce) is rebuilt and retried, an unrecoverable one (insufficient funds, intrinsic gas too low) fails with the reason and gives the nonce back.
A transaction the network does not pick up is replaced with a higher fee within the caps you set, or cancelled, never silently abandoned.
A transaction the node forgets is rebroadcast with the same signed bytes, then rebuilt at the same nonce: never a second payment.
Cancelling before broadcast means nothing reaches the network; after broadcast it sends a zero-value replacement at the same nonce, and the final state says which of the two landed.
An identity can be paused: new operations are accepted and wait without taking a nonce until it is resumed.
Several node endpoints per network: calls go to the first healthy one, an operation stays on the endpoint it started on, and a dead or lagging node is left aside without losing anything.
Finality per network
Being mined is not being done. Each network declares its finality rule, and MINED becomes
CONFIRMED only when that rule is met. A reorganisation before that point moves the operation back
and is reported; a reorganisation deeper than the rule is raised as an alert, never turned into a
silent change of a confirmed operation.
Each network declares its finality rule: a number of blocks on top, the node's finalized block, or instant on networks with deterministic finality, with coherent defaults for public and permissioned networks.
An operation is reported confirmed only when the network's finality rule is met; a reorganisation before that is followed, not hidden.
A deeper confirmation can be asked per operation or chosen by a policy rule (large amounts wait longer); the strictest applies, never one looser than the network's.
The failure report
Every terminal failure carries a report, and the state machine refuses to enter a failure state without one. It answers the questions an operator asks, in the order they are asked.
| Field | Answers | Example |
|---|---|---|
| Stage | Where it failed | policy, approval, signing, preparation, broadcast, execution, confirmation |
| Cause | Why, as a stable code | preparation.insufficient-funds |
| Retryable | Whether a retry can work | yes, no, or conditional: after-funding-payer |
| Suggested action | What to do | A sentence for the operator |
| Detail and trace | How to dig | Decoded revert or node message, attempt hash, trace id |
The report is part of the operation’s state, of the state-change event and of the API response, so your systems can route a failure without parsing a message.
Every failure comes with a report: the stage, a stable cause, whether a retry can work and on what condition, and the suggested action.
Traces, metrics and structured logs through OpenTelemetry, into the monitoring tools you already run.
What it does not do
- It does not orchestrate your business process. It groups the transactions of one business operation, traces them together and reports on them; it does not decide the next step or compensate a failed one.
- An ordered batch is not atomic on chain. It guarantees the order; a batch whose second step fails leaves the first one mined and paid for.
- Several calls that succeed or fail together, in one UserOperation of the customer's smart account: the policy reads every call and their sum, and a call that cannot succeed leaves none of them done.
- It does not fill nonce gaps on its own. Gaps are detected and alerted; repairing one is an explicit operator action.
- It does not exceed your caps. An operation whose fee would pass the tier’s cap is not sent;
a
keep-bumpingtier waits at the cap rather than raising it. - It does not withdraw a broadcast transaction. Once on the network a transaction can only be replaced at its nonce or followed to its receipt.
- It does not revisit a confirmed operation. A reorganisation deeper than the finality rule is an alert for an operator, not a change of state.
Evidence
Every claim above links to its evidence on the evidence page. The main ones:
| Subject | Requirements | Journeys |
|---|---|---|
| State machine, events, history | FR-TX-04, FR-TX-05, FR-TX-17, FR-TX-18 | J07, J13, J14 |
| Idempotency | FR-TX-03, FR-API-03 | J07, J26 |
| Preparation checks | FR-TX-06, FR-TX-16 | J02, J03, J08 |
| Nonces, gaps, several workers | FR-TX-09, FR-TX-10, FR-TX-11, FR-OPS-05 | J09, J29 |
| Gas policies, tiers, preemption | FR-CHN-12, FR-CHN-16, FR-TX-07, FR-TX-08, FR-TX-26 | J08, J21 |
| Rejections, stuck, dropped, cancelled | FR-TX-12, FR-TX-13, FR-TX-14, FR-TX-15, FR-TX-25, FR-FIN-06 | J04, J10, J11, J17, J24 |
| Finality and reorganisations | FR-FIN-01, FR-FIN-02, FR-FIN-04, FR-FIN-07, FR-FIN-08, FR-FIN-09 | J05, J11, J22, J28 |
| Failure report | FR-TX-29 | J02, J03 |