Repository navigation
feat(contracts): add executeWithCallOrder - #740
Closed
jonaprieto wants to merge 16 commits into
Closed
jonaprieto wants to merge 16 commits into
jonaprieto wants to merge 16 commits into
Conversation
A transaction can carry the order of its call lists in callOrder, signed by the delta proof. The adapter checks it instead of sorting the graph of the ordering constraints, so an application can state an order that the constraints of the generic call alone cannot.
The carried order already keeps the actions in transaction order, so it could live in each action. Record what that gains (numbers that survive composition, an order that the user proves) and what it costs.
ForwarderCallOrdering.checkCallOrder checks an order of the call lists instead of computing one, as ADR-0005 proposes: it names each call list once, keeps the actions in the order of the transaction, and meets every ordering constraint. It builds no graph, so it has no limit of 256 call lists. Nothing calls it yet.
Transaction gets a callOrder field. When it is not empty, the delta proof signs keccak256(abi.encode(transactionId, callOrder)) instead of the transaction ID, and execute checks the order with checkCallOrder and runs the call lists in it, as ADR-0005 proposes. An empty callOrder keeps the current order and delta message. The field changes the ABI and the selector of execute, so the bindings and the gas report are regenerated.
Add the carried call order to the design of the external call ordering and to the glossary: the delta proof signs it, the protocol adapter checks it in three steps instead of sorting, and the actions keep the order of the transaction.
jonaprieto
marked this pull request as ready for review
October 23, 2026 15:47
Moves the call order out of `Transaction` into a parameter of a new entry point, `executeWithCallOrder(Transaction, uint256[])`. `execute`, its selector and `Transaction` stay as on `next`, so existing clients, bindings and indexers keep working, and only a transaction that comes with a call order pays for it in calldata. The check and the delta signing do not change.
Updates the ADR, the design of the external call ordering and the glossary for the separate entry point. The ADR now states that the composer, not the provers of an action, chooses the order inside it, that an extra generic call reaches the same orders today, and that the ARM must sign the new delta message before a client can carry an order.
The design of the external call ordering already describes the entry point and its check, and the team has not decided on the call order yet, so the ADR can wait until it does.
Adds `sign_call_order`, which replaces the delta proof of a converted transaction with one over `abi.encode(transactionId, callOrder)`, the message that `executeWithCallOrder` checks. It signs with the delta witness through ARM's `delta_proof::prove`, so clients can carry an order without a change to the ARM.
The ordering constraints are the rules of the applications, which the logic proofs bind through the app data, and the call order is the choice of whoever holds the delta witness within them. Also points to the bindings helper that signs the order.
The Rust test pins the delta proof that sign_call_order makes for a fixed witness and order, and the Forge test verifies those bytes with DeltaProof.verify against computeVerifyingKeyWithCallOrder. ECDSA in k256 is deterministic, so a change on either side breaks one of the two tests.
executeWithCallOrder rejects a non-empty order for a transaction without call lists with CallOrderNotAPermutation, as it rejects an order of the wrong length. The NatSpec of the error now says so.
checkCallOrder required the action indices to never decrease along the order, so a transaction whose constraints run a later action first could use execute but not executeWithCallOrder. It now requires only what order does: the call lists of an action run together. Every order that execute computes is then a valid call order, and CallOrderSplitsAction replaces CallOrderNotInActionOrder.
…ll order A fuzz test builds up to 8 call lists over up to 8 actions with up to 8 afterTags and beforeTags constraints, and checks that checkCallOrder accepts the order that order computes, unless order finds a cycle. With the earlier rule that the action indices never decrease, it finds a counterexample.
checkCallOrder finds the call list of each tag by a linear search, so with no limit on the call lists its cost grew with the product of tags and call lists. Once a call carries ordering constraints, it now limits the call lists to DirectedGraph.MAX_NODE_COUNT with TooManyCallCarryingResources, as order does, so each tag costs at most 256 comparisons. Without constraints there is no search and no limit, as in execute.
Contributor
Author
|
closing in favor of #741. the order here is the pick of whoever holds the delta witness, which in anomapay is the backend, so the user's signature still doesn't fix it. if #741 stalls on the vk change, the user-bound route is the logics checking an owner signature over their tags with the action tree root; that needs no pa change and makes |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
adds
executeWithCallOrder(Transaction, uint256[]). a transaction can carry the order of its call lists, and the pa checks that order instead of sorting: each call list once, the calls of each action together, and every ordering constraint of #708. the delta proof signskeccak256(abi.encode(transactionId, callOrder)), so whoever relays the transaction cannot reorder or drop calls.execute, the journal and the existing abi don't change, so there are no new arm-risc0 keys and the deployed adapters gain the function by an upgrade. calls still run after all state changes, as in rc.12.the order is the choice of whoever holds the delta witness, within the tags each logic proof binds. that is the user only when the user also proves compliance; in anomapay today the backend's prover receives the delta randomness, so it picks whatever the tags leave open. this is the gap #741 closes by tying the order to the action tree root. its two objections are about committing constraints in
value_ref; tags in app data avoid both, since they are not in the nonce derivation and a persistent resource takes them at spend time, but only the logic proof covers them, not the user's signature.closing that gap on top of this needs two changes outside the pa. the logics that want a user-fixed order, anomapay-erc20-resource and generic-call-resource after #20, check the owner's signature over their tags together with the action tree root, so the prover cannot choose them, and the tags can pin a full order if the user wants one. and the sdk signs the order with
sign_call_orderwhen the user composes the transaction. the order across actions stays with the composer, as it does with #741.