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
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.
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
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.
Transactions expire, which is why the drop rate is highest
Transactions expire, which is why the drop rate is highest
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.
Fees are priced, not auctioned
Fees are priced, not auctioned
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.
Finality is a commitment level, not a block count
Finality is a commitment level, not a block count
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
The design decision that makes determinism actually hold is subtler than seeding the PRNG:
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.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 topending, 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:
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
Thesettle 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.