Skip to main content

eth_sendRawTransactionConditional

eth_sendRawTransactionConditional submits a signed transaction together with a set of preconditions. Nitro nodes evaluate those preconditions before the sequencer includes the transaction. If any precondition fails, the node rejects the transaction and never includes it, so you pay nothing.

The method extends eth_sendRawTransaction. It takes the same signed transaction payload and adds a second parameter that describes the state your transaction depends on.

Info

This method is not part of the standard Ethereum JSON-RPC API. Nitro implements it, and so do several other stacks, but the semantics differ between them. If you port a client from another stack, read Differences from other implementations first.

When to use it

Use eth_sendRawTransactionConditional when your transaction makes sense only against a specific view of chain state, and you would rather send nothing than send a transaction that reverts.

ERC-4337 account abstraction is the original motivation. A bundler packs many user operations into one transaction. Between the moment the bundler simulates the bundle and the moment the sequencer executes it, another transaction can change a nonce or a balance that the bundle depends on. The bundle then reverts, and the bundler pays the gas. When you list the storage slots the bundle depends on, that failure moves from "executed and reverted" to "rejected before execution".

The same pattern helps any submitter that pays gas on behalf of other people:

  • Relays that sponsor meta-transactions.
  • Liquidation and arbitrage bots that must not act on a stale price.
  • Batch payout services that read balances before they spend them.

Endpoint

Send conditional transactions to an endpoint that reaches the sequencer.

ChainSequencer endpoint
Arbitrum Onehttps://arb1-sequencer.arbitrum.io/rpc
Arbitrum Novahttps://nova-sequencer.arbitrum.io/rpc
Arbitrum Sepolia (Testnet)https://sepolia-rollup-sequencer.arbitrum.io/rpc

You can also send the transaction to any Nitro full node that forwards write traffic. The node carries your conditions to the sequencer unchanged. A node that rejects write traffic returns publishing transactions not supported by this endpoint.

Caution

The endpoints above are best-effort public endpoints with no service level agreement. If your application depends on availability, use a node provider or run your own node.

Parameters

PositionTypeDescription
1stringThe signed transaction, RLP-encoded and hex-prefixed. Same as eth_sendRawTransaction.
2objectThe conditions. Every field is optional, and the node evaluates only the fields you send.

The method returns the transaction hash, exactly as eth_sendRawTransaction does.

Three transaction kinds never reach the condition checks. When you send a blob transaction, an Arbitrum internal transaction type, or a transaction without EIP-155 replay protection, Nitro rejects it first.

Conditions

Nitro reads three different sources when it evaluates your conditions: the parent chain block number, the child chain block timestamp, and child chain contract storage.

FieldTypeYour transaction stays valid only while…
knownAccountsobjectEach listed account still holds the storage you describe.
blockNumberMin and blockNumberMaxquantityThe parent chain block number sits inside the range.
timestampMin and timestampMaxquantityThe child chain block timestamp sits inside the range.
Three boxes. blockNumberMin and blockNumberMax read the parent chain block number. timestampMin and timestampMax read the child chain block timestamp. knownAccounts reads child chain contract storage.
Each conditional option reads a different source.

Note the asymmetry. The block bounds read the parent chain block number that ArbOS records in the child chain block header, not the child chain block number. The timestamp bounds read the child chain block timestamp. To learn why the two chains report different numbers, see Block gas limit, numbers and time.

Express both bounds as quantities. Nitro accepts the hex-prefixed form and the decimal form.

knownAccounts

knownAccounts maps an account address to the storage you expect that account to hold. Each entry takes one of two forms.

Storage root form. The value is a single 32-byte hash. The condition holds only while the account's whole storage root matches that hash. This form is strict: a write to any slot of that account fails the condition.

{
"knownAccounts": {
"0x9c8fF314C9Bc7F6e59A9d9225Fb22946427eDC03": "0x1d5d9f6a3c8b2e4f7a9d1c3b5e7f9a2c4d6b8e1f3a5c7d9b2e4f6a8c1d3b5e7f"
}
}

Slot form. The value maps a storage slot to the value you expect. The condition holds while every listed slot still holds its listed value, and writes to other slots of the same account leave it alone. Prefer this form. It is far less likely to fail for a reason unrelated to your transaction.

{
"knownAccounts": {
"0x9c8fF314C9Bc7F6e59A9d9225Fb22946427eDC03": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000000000000000000000000000000000000000000a"
}
}
}

An entry that maps an address to an empty object carries no condition, and Nitro evaluates the remaining conditions as usual.

Where Nitro evaluates your conditions

Nitro evaluates your conditions three times. Knowing where each check happens tells you which failure you are looking at.

A vertical flow. Your app signs the transaction and attaches the conditions, then sends it to a prechecker node. The prechecker runs check 1 against current state and check 2 against state about two seconds old. The sequencer runs check 3 against the block it is building, then includes the transaction in a child chain block. If any check fails, the node rejects the transaction with JSON-RPC error -32003 and never includes it.
Nitro evaluates conditional transaction conditions at three points before inclusion.
  1. The prechecker node evaluates current state. The node you submit to evaluates your conditions against its latest block before it forwards anything.

  2. The prechecker evaluates older state. The node then walks back to a block that is at least required-state-age seconds old, which defaults to 2 seconds, and evaluates the same conditions against that older state. A failure here returns the original message behind the prefix conditions check failed for old state.

    This second check stops a caller from exploiting a condition that held for only a moment. It has a side effect worth planning for: Nitro rejects a condition that became true in the last 2 seconds, even though it holds right now. If your conditions track state that changes that recently, expect this rejection and retry.

  3. The sequencer evaluates the block it is building. At sequencing time the sequencer evaluates your conditions once more against the in-progress block, immediately before it executes your transaction. This check is the one that guarantees your conditions hold at execution.

A node running with strictness below 20 skips both prechecker checks. The sequencer check always runs, so the guarantee holds whatever any intermediate node does.

Errors

When a condition fails, the node returns JSON-RPC error code -32003 and names the condition in the message.

MessageCause
BlockNumberMin condition not metThe parent chain block number sits below blockNumberMin.
BlockNumberMax condition not metThe parent chain block number sits above blockNumberMax.
TimestampMin condition not metThe child chain block timestamp sits below timestampMin.
TimestampMax condition not metThe child chain block timestamp sits above timestampMax.
Storage root hash condition not metAn account's storage root no longer matches the hash you gave.
Storage slot value condition not metA listed slot no longer holds the value you gave.
conditions check failed for old state:…Your conditions hold now but did not hold about 2 seconds ago.

A rejection means the sequencer never included your transaction, so you can safely resubmit it with updated conditions. Errors that are not condition failures, such as nonce too low or a fee cap below the base fee, behave as they do for eth_sendRawTransaction.

Example

The request below submits a transaction that stays valid only while slot 0 of one contract still holds 10, and only until parent chain block 20260011.

curl -X POST https://arb1-sequencer.arbitrum.io/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_sendRawTransactionConditional",
"params": [
"0x02f8710a808459682f008459682f0e82520894a0b8...c080a0",
{
"knownAccounts": {
"0x9c8fF314C9Bc7F6e59A9d9225Fb22946427eDC03": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000000000000000000000000000000000000000000a"
}
},
"blockNumberMax": "0x13524ab"
}
]
}'

A successful call returns the transaction hash:

{
"jsonrpc": "2.0",
"id": 1,
"result": "0x8f7d1c0b3a2e5f4c9d6b8a1e3f7c2d5a9b4e6f8c1d3a7b5e9f2c4d6a8b1e3f5c"
}

A failed condition returns an error instead:

{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32003,
"message": "Storage slot value condition not met"
}
}

Most libraries do not wrap this method, because it is not part of the standard API. Call it through your provider's raw request interface:

const txHash = await provider.send('eth_sendRawTransactionConditional', [
signedTx,
{
knownAccounts: {
[contractAddress]: { [slot]: expectedValue },
},
blockNumberMax: '0x13524ab',
},
]);

Node configuration

Node operators control the prechecker behavior with the flags below. The sequencer check takes no configuration.

FlagDefaultEffect
execution.tx-pre-checker.required-state-age2How many seconds old the older state must be. 0 turns the second check off.
execution.tx-pre-checker.required-state-max-blocks4How many blocks the node walks back to find that older state. 0 removes the limit.
execution.tx-pre-checker.strictness20Below 20, the node skips the condition checks and forwards your transaction unverified.

For the full list, see the Nitro CLI flags reference.

Nodes export one accepted counter and one rejected counter for each of the three checks. Compare them to tell a client that sends stale conditions apart from a node that rejects on old state.

CheckMetric prefix
Prechecker, current statearb/txprechecker/conditionaltx/currentstate/
Prechecker, state about 2 seconds oldarb/txprechecker/conditionaltx/oldstate/
Sequencer, block being builtarb/sequencer/conditionaltx/

Limits

Nitro sets no cap on the number of accounts or storage slots you list in knownAccounts. Every listed slot costs the node a state read on each of the three checks, so a large knownAccounts map stays cheap for you and grows expensive for the node.

Warning

Nitro enforces no built-in cap, so operators who expose this method publicly should apply their own request-size and rate limits at the RPC layer. Treat an unlimited endpoint as a denial-of-service surface.

List only the slots your transaction depends on. For an ERC-4337 bundle, that usually means the nonce and deposit slots of each sender, rather than the storage root of each account.

Differences from other implementations

Other stacks expose a method with the same name and a different contract. If you port a bundler or relay to an Arbitrum chain, check these four points.

BehaviorOn Arbitrum chains
blockNumberMin and blockNumberMaxRead the parent chain block number. Other stacks read their own chain's block number.
Old-state re-checkNitro also evaluates your conditions against state about 2 seconds old. Other stacks evaluate current state only.
knownAccounts size capNitro enforces none. Other stacks cap the number of slots and reject the request with -32005.
AuthenticationNitro requires none. Each operator chooses their own access control.

Nitro reserves JSON-RPC error code -32005 for a limit-exceeded condition, but the current implementation never returns it.

See also