Skip to main content
Architecture · the core Arc settles the middle leg of a corridor transfer on a blockchain. Which blockchain is a decision made per transfer, not wired into the system, and that is what “chain-agnostic” has to mean if it is to mean anything. This lives in packages/chain rather than in a service, because it is a shared primitive: the movement context needs it to settle, and the test suite needs it to inject failures.

The seam

Six methods. Everything upstream codes against this interface, so swapping the simulator for a real RPC client, or an Anvil node, or a testnet, is a matter of writing another implementation, not of touching the saga.
Nothing in the movement context knows it is talking to a simulator. That is the test of whether an abstraction is real: the code above it cannot tell what is below it.

Five chains that behave differently

This is where chain-agnosticism earns its keep. If all chains behaved the same, the abstraction would be decoration. Polygon has blocks six times faster than Ethereum and settles nearly twice as slowly. Fast blocks that are individually less secure need more of them to reach equivalent confidence.
This is the single most useful thing the chain layer teaches, and it is the subject of The fastest chain that settled last. A system that models chains as “block time” gets this exactly backwards, and would route high-value transfers to the slowest-settling chain in the set while believing it had picked the fastest.
Failure rates are per-chain too, not uniform noise: Solana has the highest drop rate, Polygon the deepest reorgs.

Solana is the one that breaks the EVM assumptions

Four of the five chains are close enough to each other that a single mental model covers them: a global mempool, gas auctions, probabilistic finality by block depth. Solana is not, and it is the most useful chain in the set precisely because it forces the abstraction to be genuine rather than an EVM adapter with the names changed.

There is no global mempool to be evicted from

Ethereum-family chains hold pending transactions in a mempool that every node gossips. A transaction sits there until it is included, replaced, or evicted under pressure.Solana forwards transactions directly to the current and upcoming slot leaders rather than pooling them. Under congestion a leader drops what it cannot schedule instead of queueing it, and there is no shared buffer that later reconsiders it.The practical consequence for a payments business: a Solana transaction that does not land is simply gone, and it is gone quickly. There is no “still pending, might confirm later” limbo of the kind an EVM chain produces.
A Solana transaction carries a recent blockhash and is only valid for a bounded window after it, on the order of a minute. Miss that window and the transaction is permanently invalid, not delayed.This is a genuinely different failure mode from EVM eviction, and it is a better one:That “possibly still out there” column is what makes EVM retries dangerous and idempotency keys mandatory. Solana converts an ambiguous timeout into a definite answer, which is worth a great deal to a saga that has to decide whether to compensate.Arc models this by giving Solana the highest drop rate in the set. The frequency is worse and the ambiguity is better, and those are different things.
An EVM chain runs a blind auction: bid too low and you wait, bid too high and you overpay, and the clearing price moves block to block.Solana charges a small base fee per signature plus an optional priority fee, expressed as a price per unit of compute. Fees are lower and far more predictable, which matters more than the headline cost for a business quoting a price to a customer before it has broadcast anything.Arc’s quote engine takes a chain fee estimate at quote time. On Ethereum that estimate can be materially stale thirty seconds later. On Solana it usually is not, and that difference is worth more to the quote than the absolute fee level.
Solana exposes progressive commitment: a transaction is processed, then confirmed once a supermajority has voted on its block, then finalized once enough confirmed blocks are rooted on top.Roughly 32 slots at 400ms gets you to finalized, hence the ~13 second window in the table. The intermediate confirmed level arrives far sooner and is what most consumer applications actually use.
Arc waits for the finalized equivalent and does not currently expose the intermediate level. Using confirmed would cut the Solana settlement window to roughly a second, and it would mean crediting a beneficiary against a state that is very unlikely, but not certain, to persist. That is a credit decision, and Arc’s position is that a credit decision should be modelled as one rather than absorbed silently by a lower commitment level.
This is why the driver interface has getConfirmations rather than getBlockDepth, and why finality depth is configuration per chain rather than a constant. “Confirmations” is an abstraction over a vote count on Solana and a block count on Ethereum, and the saga above it does not need to know which.

Transaction states, and the distinction that matters

failedMined, executed, reverted. Consumed gas. The fee is spent and irrecoverable.
droppedNever made it into a block. Cost nothing.
These are different terminal states and the difference is load-bearing. Compensating for a failed transaction must not reverse the gas expense: the gas was really spent, and reversing it produces a balanced ledger that misstates reality. Arc’s saga deliberately does not track the network-fee journal for exactly this reason.
The ChainEvent union covers block | included | confirmed | failed | dropped | reorg. The reorg event carries revertedTransactions, so a consumer knows precisely what to undo without diffing state itself.

Determinism

The whole simulator rests on one small file: mulberry32 with an FNV-1a seed hash. Tiny, dependency-free, and identical across runs and platforms.
This is the only file in the repository permitted float arithmetic, with a scoped eslint-disable and a written justification. It is bit-mixing on a 32-bit state, not money, and nothing it produces becomes an amount without conversion to bigint minor units first.
The design decision that makes determinism actually hold is subtler than seeding the PRNG:
1

The outcome is decided at broadcast time, not at inclusion

Whether a transaction will be dropped, will revert, and how long it will be delayed are all rolled from the seeded RNG when it enters the mempool.If those rolls happened at inclusion, the results would depend on the order in which transactions were later queried, and a seeded run would stop being reproducible the moment a test read state in a different order.
2

Broadcast is idempotent by key

Rebroadcasting with the same idempotency key returns the original hash. This matches real client behaviour and matches Arc’s own retry semantics: a retry that produced a second on-chain transaction would be a duplicate payment.
3

Block production drains the mempool with a bounded loop

Delayed transactions are pushed to the back rather than dropped, modelling a low-fee transaction waiting its turn. A considered counter bounds the loop so a mempool full of delayed transactions cannot spin forever.

Reorgs, two ways

Rollback splices blocks off the chain, clears each affected transaction’s block height, resets its status to pending, and unshifts it to the front of the mempool so it is re-mined promptly. It returns the reverted hashes. It can be triggered two ways, and having both is the point:
Probabilistically, during advance()Before each block, a roll against the chain’s reorg rate: guarded so it cannot reorg past genesis. Realistic, and reproducible under a seed.
Deterministically, via forceReorg(depth)The chaos suite needs a reorg at a precise moment. Probability-tuning a test until it happens to reorg is flaky and proves nothing.
A seeded run reproduces the same blocks, reorgs and transaction outcomes every time, and a reorg rolls a mined transaction back to pending before it is re-mined. Both are asserted by test.

Chain selection

selectChain scores each available chain on settlement time and fee, weighted by a 0–100 speedPreference:
High speed preference picks Base. Low speed preference picks Tron. Both terms are normalised into comparable ranges before weighting, because adding seconds to gas units is meaningless otherwise.
Deliberately simple. Real routing would also weigh liquidity depth on the destination pair and current congestion, both of which move faster than the static characteristics here. The interface has room for both; the implementation does not have them yet.
This is what chain-agnostic settlement means concretely: the corridor evaluates per transfer instead of being wired to one chain, and swapping in a better scoring function does not touch the saga.

How the saga uses it

The settle step does three things in a specific order, and the third is the one people skip:
1

Broadcast

With an idempotency key derived from the transfer, so a retry cannot double-send.
2

Advance the chain

Via an injected advanceChain function: the simulator has no wall clock, so time is something the caller supplies. This is also what makes a full settlement testable in milliseconds.
3

Re-read the transaction and require `final`

Not “no error was thrown”. Checking final is the difference between confirming settlement and merely confirming submission, and code that does the latter looks completely correct until the day a transaction is dropped.
The settle step’s compensation is a ledger-level unwind representing funds recovered from the settlement partner. Nothing un-sends a confirmed on-chain transaction. That limitation is real, and it is stated here rather than papered over: an architecture that implies otherwise is lying about what a blockchain is.

Next: the settlement saga

Where the chain layer, the rails and the ledger are assembled into a transfer.

Settlement and finality

The domain background, if the finality-depth table above raised questions.