EVM Protocol Adapter

A thread for me and @xuyang to sync work on the protocol adapter. :thread:

1 Like
  • Verify Cairo proofs on EVM

    • evm-verifier-contract - Cryptographic STARK verifier elements
    • We need to further investigate the verifier-contract and proving system(lambda-cairo-prover) to ensure format and API compatibility. Ideally, we should create tests to verify the pipeline.
  • Verify Risc0 proofs on EVM

    • risc0-ethereum: RISC Zero’s Ethereum contracts, including the on-chain verifier for all RISC Zero Groth16 proofs
    • We will use risc0’s toolchain to construct tests verifying risc0 proof validation on EVM.
2 Likes

This is the Starknet docs page, where all deployments of the Starknet solidity verifier are listed Solidity verifier :: Starknet documentation.

The GpsStatementVerifier implementation behind this proxy and the verifyProofAndRegister insides seems to be the main entry point:

function verifyProofAndRegister(
    uint256[] calldata proofParams,
    uint256[] calldata proof,
    uint256[] calldata taskMetadata,
    uint256[] calldata cairoAuxInput,
    uint256 cairoVerifierId
) external;

The last input argument uint256 cairoVerifierId seems to indicate the verifier layout that should be used. Apparently, there are eight:

  1. “plain”
  2. “small”
  3. “dex”
  4. “recursive”
  5. “starknet”
  6. “recursive_large_output”
  7. “all_solidity”
  8. “starknet_with_keccak”

I guess the main challenge (mostly for you @xuyang) is figuring out what formats and which layout we have to use.
I can create a Solidity project in which we can play with the deployed verifier on mainnet locally (so that we don’t need to pay gas for trying things out).
It would be good to have an example STARK proof in the right format from your shielded RM implementation.

Probably it makes also sense to connect with the Starknet people @xuyang.

1 Like

Excalidraw from brainstorming 2024.12.16

1 Like

Protocol Adaptor meeting Notes Dec 16 , 2024

settlement only protocol adaptor

we have a transaction object which gets submitted to an EVM contract and EVM contract needs to

  1. run usual verification checking validity + balance
  2. if in fact valid then apply the state changes (commitments and nullifiers + potentially storage)

Is there a Cairo verifier we can just re-use. There are a bunch of contracts deployed and one that contains verifier proof and register function, but think this is the entry point, but unclear what the format is. There are different layouts, main difficulty is to figure out how the data has to go in.

First thing we are trying to build is settlement only Cairo Protocol adaptor. Taking tx object validating proofs and checking the state changes. Only proofs will be Cairo proofs, all proofs must align with Cairo RM. Poseidon Hash function. Did we find a Poseidon implementation on the EVM. Can look into solidity contract from Starkware.

Would hashing happen in the contract or locally?

  • PA contract needs to implement is valid and balanced as defined in resource machine specs
  • Implement verify

Do we need to compute hashes if we are just verifying a transaction? If we want to open the commitment from the commitment tree, we need the hash.

If we store the state off-chain, can build the Merkel tree off-chain.

We should just implement a Merkel tree in Solidity. What you would store in the mapping is previous Merkel tree roots and commitments.

Solidity storage

  • commitments
  • nullifiers
  • Previous Merkel tree roots (which can be used for proofs)
  • Store Binary data (which can put in mapping) if instructed by tx

In terms of storage, comprehensive?
Yes

Is it clear what to do for validity checking, balance checking, and state updates?

Yes

EVM interop

EVM state changes and resource state changes can be coupled so you can send 10 WETH to the PA contract mint 10 WETH resource, do stuff with the resource then someone can later withdraw a total of 10 WETH.

For ERC-20s

  • we want to be able to write a resource logic which says new resources can only be created when some ERC-20 tokens are transferred to the PA, and that ERC-20 tokens can be transferred from PA when a resource is consumed. (counting for the balance).

  • We want ERC-20 transfers to count as the balance delta. A simple way to do it is to special case ERC-20 transfers in the contract

  • We could also, hardcode the ERC-20 transfer logic in the PA, pass as input to resource logics what ERC-20 transfers have happened, they can accept or reject also PA needs to be careful about enforcing unauthorized erc-20 transfers

  • Wrapper contracts for each asset (need to call the PA and allow creation of a resource somehow, move the logic of the ERC-20 token becoming a resource move it out of the PA contract) - wrapper contracts for each mapping. Wrapper contract holds tokens and non PA, and somehow check the right resource is created.

  • Another way of going about this would be to somehow convert the EVM state changes into a delta and add the deltas to the tx s.t. the tx can only be balanced if certain resource state changes happen, might be compatible with both things. by default EVM doesn’t have concept of balance but if we know the transfers we can calculate the balance first through transfers then calculate the EVM delta add this. we would have to associate each ERC-20, 721, 1155, or w/e transfer with a specific kind to turn it into a delta. Maybe we make a sort of ERC-20, ERC-721, etc. wrapped resource logic and then tokens could be converted too and from these wrapper resources in the transaction.

    • somewhere we have logic which enforces the correspondence, kind of like putting this more in the resource machine, makes it more clear from the perspective of RM programs what exactly is going on
  • When executing a transaction PA also takes EVM action instructions (transfer ERC-20 from amount or transfer ERC-721 from amount, then at the start of executing the transaction the pa contract will execute the ERC-20 contract transfer from, and it will track an EVM delta corresponding to the total transfer amount where each contract is associated with a particular resource kind, this is an arbitrary decision we can enforce, which means we can have resource logic corresponding to resource kind do whatever we want it to - can enforce - allow themselves to be converted to native Anoma token resources or similar.

For example, we can have ERC-20 wrapped resource logic put the contract address in the resource label then derive the kind. So we need only one for each semantically unique Ethereum thing we are trying to wrap. Reasonably elegant. You could just transfer around ERC-20 wrapped tokens in the resource machine without doing anything else. We can also allow ERC-20 wrapped to be converted to and from some corresponding Anoma token.

If we limit kudos to ERC-20 resource logic and no, you say there is a wrapper contract, and it ensures that tokens being transferred create an equivalent. What I am really suggesting is the PA supports making arbitrary EVM state changes. You specify some EVM state changes you would like to happen.

Send to PA so it can track what is going on. If the PA is executing transfer from on ERC-20 one can imagine generalizing this to arbitrary call data (need to think about security properties) combine these to a delta. PA enforces the balance check, if you want to deposit 10 WETH in rm state in your tx before executed on EVM need to create 10 WETH ERC-20 resources so when its executed on EVM tx will be balances, using balance check to enforce state change correspondence.

This seems like a reasonably good place to start, with just ERC-20, and seeing if we can get the pattern to work.

Discussion

Do we expect or want to have this first version? I can’t work on the Cairo part, but can work on the interop part and Merkel tree part. When doing this, will understand all the details better. What part should we start with?

I would start just writing the state and data structures you need to process, writing validity check, and balance check, one EVM action for ERC-20 transfers and getting Cairo verifier to work. All of this is pretty well-defined just need to get stuff to work on the EVM. It would be cool to have an initial prototype by early January or something.

I think for Xuyang’s work it would be good to test different inputs, would like to set up a solidity project which allows us to fork Starknet to see if we can verify simple stark proofs. Having a solidity project in which you can easily input a proof and see if it works or reverts - Xuyang - I will figure out how to verify a Cairo proof in EVM. there are 8 different verifier layouts, I will take care of it.

Do you by chance know where to find Poseidon Hash implementation, or is there something you have in mind? There might be several, not sure of the formats of the concrete implementations are compatible, but we can figure it out, do some test for performance to see if large Merkel tree is available in EVM. For now, we also verify each proof separately and don’t combine into one. We don’t have the proof of aggregation. We can test the prototype locally first.

Yulia / Enrique would benefit from diagrams. Details for enforcing that some token has been transferred are not clear to me yet. Michael and I know the EVM. If you want to understand in detail, just read the EVM yellow paper.

How does something become a resource in the first place?

If here the idea to ensure some tokens have been transferred to a particular smart contract - contract has signature of user, couldn’t resource logic try to check the signature. It could be one way of implementing the RL. Are you proposing RL try to verify actual signature? problem with this is it does not cleanly interoperate with how the EVM works (multi-sig, abstract account with different signature scheme, just have to sue the fact that transfer from succeeded, and how it was authenticated we don’t know.)

2 Likes

Linking discussion from Hacker house where we discuss settlement only and full feature EVM contracts.

https://research.anoma.net/t/anoma-protocol-adaptor-discussion/878

2 Likes

I’ve pushed an initial draft for the EVM protocol adapter https://github.com/anoma/evm-protocol-adapter/blob/main/src/ProtocolAdapter.sol.

I’ll write some explanation tomorrow and we can discuss it in our next meeting.

2 Likes

EVM Protocol Adapter Architecture Draft


Transaction flow for the settlement-only EVM protocol adapter interacting with a resource wrapper contract and associated ERC20 contract.

Component Overview

The graphic shows 5 components

  1. Juvix App: Contains a wrapper resource definition and transaction functions producing transaction objects. The wrapper resource logic requires a signed authorization message by the owner on ephemeral and non-ephemeral consumption. The signature is stored in action.appData (as done in Kudos) and transaction functions are responsible to put them in place.
  2. Cairo Backend: Called by Juvix to compute proofs populating the transaction object.
  3. Protocol Adapter (PA) contract: A solidity contract containing
    • A Merkle tree implementation (to accumulate commitments)
    • A set implementation (to accumulate nullifiers)
    • A verify function and a reference to the Starknet Solidity verifier contract allowing Cario proof verification
    • An execute function that updates the commitments and nullifiers and can make external calls to wrapper contracts when processing wrapper resources
  4. Resource Wrapper contract
    • Contains a reference to the wrapper resource kind.
    • Owns wrapped ERC20 tokens
    • Can only be called by the protocol adapter contract
    • Contains a wrap and unwrap function calling the target contract (e.g., transferFrom and transfer on an ERC20 token)
  5. Target contract: E.g., a pre-existing ERC20 token containing the balances of owners.

Transaction Flow (ERC20 Wrap Example)

User Alice owns 100 ABC ERC-20 tokens and wants to wrap 20 of them in a resource.

  1. Alice calls approve on the ABC contract to allow the associated resource wrapper contract to spend 20 ABC on her behalf. Alternatively, Permit2 signatures can be used to eliminate this step.

  2. Alice calls the initialize transaction function in the ERC20 Juvix app populating a transaction object. This consumes an ephemeral ABC resource and creates a non-ephemeral ABC ERC20 resource, both with quantity 20 and Alice as the owner. The ERC20 resource contains the ABC ERC20 wrapper contract address in its label (thus influencing its kind) and requires the action.appData map to contain the consumed, ephemeral resource object with its nullifier as the lookup key.

  3. The transaction function calls the Juvix RM interface to prove the transaction object. In this example, proofs are computed using the Cairo backend since this is the only one we support in this prototype. In future versions, different proving systems can be selected through the information flow control predicate in the in transaction object.

  4. The Cairo backend computes the various proofs and returns them. For compliance proofs, it checks the EVM protocol adapter state, i.e., the on-chain commitment accumulator and nullifier set maintained inside.

  5. Alice (or any other Ethereum account) sends the balanced and valid initialize transaction object to the Ethereum mempool*.

  6. The protocol adapter verifies all proofs (i.e., delta, compliance, logic proofs) in the transaction object using the STARK verifier contract. If a proof is invalid, the transaction reverts.

  7. The PA updates its state by iterating over the tx.actions and adding the commitments and nullifiers and adding them to the commitment accumulator and nullifier set, respectively.

  8. For each nullifier and commitment, it checks if the corresponding resource object is part of the action.appData with the nullifer and commitment as the lookup key, respectively. On successful lookup, the PA checks

    • if the resource is ephemeral
    • if the resource reproduces the nullifier** or commitment
    • has a contract address stored in its label
    • if the contract is a wrapper contract and contains a reference to the resource’s kind.

      If the label doesn’t contain a contract address, the next step is skipped.
      If the kind referenced in the wrapper contract doesn’t match the resource, the call reverts.
      In this example, all criteria from above are met for the consumed, ephemeral ABC ERC20 resource being part of the initialize transaction object. The PA then calls the wrap function in the wrapper contract with the nullifier, the resource object, and the action.appData as inputs.
  9. The wrapper contract wrap function extracts the resource quantity and owner (stored in resource.value) of the passed resource object and calls transferFrom({from: owner, to: address(this), value: quantity}) on the ABC ERC20 token contract. This transfers 20 ABC tokens from Alice to the wrapper contract. After processing the remaining tx.actions (see step 6.) the call succeeds. In case of failure, the entire transaction reverts.

* Unbalanced transactions would be submitted to an intent pool to be matched by solvers. The solver can then send and execute the balanced transaction (and has to be cautious to not get frontrun for profitable intents).
** The universal nullifier key must be used to nullify ephemeral resources triggering EVM state changes. Since EVM state is visible anyway, no information is leaked here.

Below, the PA and the wrapper contract interfaces are shown:

interface IProtocolAdapter {
    function verify(Transaction calldata transaction) external;
    function execute(Transaction calldata transaction) external;
}

interface IResourceWrapper {
    event ResourceWrapped(bytes32 indexed nullifier, Resource resource);
    event ResourceUnwrapped(bytes32 indexed commitment, Resource resource);

    function kind() external view returns (bytes32);
    function wrap(bytes32 nullifier, Resource calldata resource, Map.KeyValuePair[] calldata appData) external;
    function unwrap(bytes32 commitment, Resource calldata resource, Map.KeyValuePair[] calldata appData) external;
}

Conclusion

This design decouples the PA and wrapper contract, supports intents, maintains information flow control (except for external EVM state changes), and is not limited to ERC20 tokens. Arbitrary logic and external contract calls can be executed in each deployed wrapper contract.

2 Likes

Could you elaborate on how the Cairo backend checks the on-chain state? I might overlook something obvious here but I stumbled over this step unsure how this is done exactly

Could you elaborate on how the Cairo backend checks the on-chain state?

Sure, compliance proofs require proving the (non-) existence of commitments:

All resource commitments are stored in an append-only data structure called a commitment accumulator. Every time a resource is created, its commitment is added to the commitment accumulator. The resource commitment accumulator is external to the resource machine, but the resource machine can read from it. A commitment accumulator is a cryptographic accumulator that allows to prove membership for elements accumulated in it, provided a witness and the accumulated value.

Accordingly, to prove membership of a leaf (a resource commitment in our case) in a Merkle tree, you need the witness (a list of all sibling nodes from the leaf up to the root) and have to check that it reproduces the expected root.
This requires maintaining a history of roots on-chain. In the draft example, I am storing the roots and emit

event CommitmentAdded(bytes32 indexed commitment, uint256 indexed index, bytes32 root);

Given the past commitments and leaf indices emitted in all previous events, one can re-construct the Merkle tree off-chain and obtain the witness (the list of siblings) allowing to prove (non-) existence of the commitment in the Merkle tree for a given root.

1 Like

Some thoughts and questions after our meeting yesterday.

The difference between the three design ideas proposed in the Excalidraw image seems to be that design 1. and 3. provide previously executed EVM calls/actions as arguments to wrapper resource (WR) logics, whereas design 2. uses the initialization and finalization of ephemeral WRs and an associated wrapper contract (WC) reading from them to conduct the EVM state change call in consequence.

The main challenge for all designs is to ensure the correspondence between WR properties such as kind, owner, or quantity and the ERC20 call that must be triggered on WR initialization/finalization.
In this context, there a some difficulties that I encountered and that influenced the choices that I’ve made that I want to share before asking some clarifying question on design 3.

Kind Correspondence

An ERC20<->WR kind correspondence check is necessary because otherwise anyone can create WRs with ERC20 addresses in their label to trigger transferFrom/transfer calls through the PA, thus draining it. If we want to make the external calls directly from the PA contract, we need a registry of ERC20<->WR kind correspondences, but this would make the system permissioned, which is not an option.

Furthermore, token or other contracts standards can require the receiving contracts to implement certain callback functions (e.g., onERC721Received or onERC1155Received).
To avoid the above and to separate concerns, I moved the external EVM call logic outside the PA into WCs. A WC has a state variable referencing the WR kind and the WR references the WC adress in the label.

It is worth mentioning that establishing the WC<->WR kind correspondence requires a trick: Since there is a circular dependency between the WR kind and the WC address, we must either initialize the WC post-deployment with the WR kind or pre-compute the WC address using CREATE2 and pass the resulting WR kind to the WC constructor on deployment.

Specific Correspondences

Besides the kind, we also must ensure the ERC20<->WR owner and quantity correspondences not influencing the kind.
Here, the WC (with the wrap(nullifier, resource, appData) and unwrap(nullifier, resource, appData) function inside) gives flexibility to

  • read and process data from the resource object (e.g., owner and quantity, or whatever other data the resource object contains) and
  • execute (multiple) external calls.

I can’t see how we can ensure such correspondences in the EVMAction + EVMDelta approach (design 3.) easily, since the delta includes the kind and quantity, but not other properties such as the owner. To ensure these properties as well, we would need to additionally

pass as input to resource logics what ERC20 transfers have happened

(as per design 1).

In this context, I didn’t understand the last part in the Excalidraw text (which seems to be part of design 3.) and forgot coming back to it after the holidays.

When executing a transaction, the protocol adapter also takes some “EVMAction” instructions EVMAction := TransferERC20 contract from amount | TransferERC721 contract from amount Protocol adapter contract will execute erc20contract.transferFrom, erc721contract.transferFrom, … and it will track a “EVMDelta” corresponding to the total transfer amounts where each contract is associated with a particular resource kind. The resource logics corresponding to these resource kinds can allow themselves to be converted to “native” Anoma token resources or similar.
“ERC20Wrapped” resource logic, put the contract address in the resource label, derive the kind
The resource logics corresponding to these resource kinds can allow themselves to be converted to “native” Anoma token resources or similar.
“ERC20Wrapped” resource logic, put the contract address in the resource label, derive the kind

I would like to understand better how native Anoma tokens and their conversion work and explore design 3 as well.

As a note (which we also discussed in the call), the PA contract will also need to support “regular storage”, i.e. some way to process the fields in application data which come associated with instructions to store them for a duration beyond transaction execution. A simple first-pass implementation of this would just be to keep a content-addressed storage map of hash to bytearray.

As a general note, it is my preference to execute all EVM state changes before calling resource logics (or checking associated proofs). This way, we can also execute EVM state reads and pass those results to resource logics if desired, and guarantee that the EVM state will not change after resource logics are checked. If we execute EVM writes after or during the resource logics checks, we would be unable to support reads in this way. We might potentially want to support “pre” and “post” - EVM write execution EVM state reads (even potentially segmented per-action) – but in any case we’ll need clear segmentation here.

Yes. In particular, I think we can say that we want a particular resource kind to correspond to a particular EVM wrapper contract, such that all resources of that kind “collectively” control access to EVM state (e.g. tokens) which is “owned” by the associated wrapper contract. This will work for ERC20 wrappers, and I also think that it will work well in general: we’d never want resources of another arbitrary kind to be able to control state owned by a given wrapper contract (otherwise state could be stolen), and we’d never want a wrapper contract to correspond only to a single resource (since resources are immutable).

This complexifies things somewhat, I can see more why you were going for “custom developer-built wrapper contracts” now.

Here, we put the “specific correspondence”-enforcing logic inside the wrapper contract. This could work, but all else remaining the same, I think it may be easier to have the specific correspondence enforcing logic live inside the resource machine – just for the general reason that it’d be nice to have as little EVM logic and code as possible.


I have an overall proposal to simplify this system, which I’ll try to describe here – let me know if it makes sense. Forgetting the details of ERC20/ERC721/etc. and such for now, how about:

  1. The basic correspondence which we’re trying to establish is between wrapper contracts (which own EVM state, execute EVM calls, etc.) and resources in the RM. Let’s further simplify and say that we represent a given wrapper contract by an “NFT” RM resource, such that only one such resource exists at any point. Let’s call this resource the “wrapper contract resource”. The correspondence which we want to enforce is that:
    a. When we create a wrapper contract, we create an associated wrapper contract resource.
    b. Any calls made by this wrapper contract can only happen in a transaction where the associated wrapper contract resource is consumed, and where the associated wrapper contract resource’s resource logic approves the calls made.
    • (in practice, we could enforce (a) with the balance check and (b) with some simple “FFICalls” field passed into resource logics, similar to app data, where we make any EVM calls from wrapper contracts requested in the transaction but enforce that the associated wrapper contracts resource logics were satisfied)
  2. All further enforcement of specific connections between EVM state changes and resource machine state changes happens in these wrapper contract resource logics. For example, a wrapper contract resource logic might enforce that a CALL with certain calldata to an ERC20 contract (to call transfer or transferFrom, in practice) can be made iff. a resource of the associated denomination and quantity is consumed or created.

To some extent, this simply “pushes the complexity around” – instead of dealing with resource data in the EVM, we deal with EVM calldata in the RM – but I think that the latter might be preferable, especially in the long-term. Does this proposal make any sense? What do you think?

1 Like

Thanks for reminding me. This is on the TODO list. What will the app data be used for after transaction execution?

Does this mean we only support the store forever deletion criterion in the first-pass implementation?

I see. I didn’t think about EVM state reads and followed the checks-effects-interaction pattern to avoid re-entrancy attacks by the external contract. I can put other guards in place.

Agreed.

Why is such an “NFT” wrapper contract resource that only exists once desirable? This limits the wrapper contract to do only one EVM state change per block (not only per transaction). If one transaction has consumed the wrapper resource, all subsequent transactions trying to consume it as well will revert.

Yes, I will do this.

1 Like

It would be stored and potentially retrieved later by a user. For example, the app data could be some state of a resource that the transaction author wants the Ethereum validators to store, or some encrypted data that the recipient should download.

Yes. That’s a good place to start with the first-pass implementation.

Aye, checks-effects-interaction makes sense in general for EVM programming, but I think we’ll need to think more about our specific constraints and intended correspondence here.

Such a wrapper contract resource that exists only once might be desirable since we can guarantee arbitrary state correspondence between the state of that wrapper contract resource and the state of the wrapper contract (since they are 1-1 and always either both involved in a transaction or neither involved at all).

I agree that only being able to do one EVM state change per block is undesirable. This limitation would be lifted with the regular (non-settlement-only) protocol adapter (which could always query for the latest wrapper contract resource). In the meantime, I think we have to either live with this limitation or allow for multiple wrapper contract resources and give up the guarantee of state correspondence (but retain the guarantee that the resource logic would always be called). The latter may be fine but we should note this carefully, and consider returning to a single wrapper contract resource in the future when we have a full protocol adapter, I think.

2 Likes

Yes, for the prototype and for devnets, we can live with it. In production, this would allow for griefing attacks. An attacker could frontrun a tx and consume the wrapper resource first, thus causing the other tx to fail.

1 Like

would this be for the purpose of specifying controllers; e.g. i want p2p and coinbase validators to store my resource data?

you might be able to get around this with sending Anoma related orderflow exclusively to one blockbuilder if there is only one EVM state change per block, no? you might even be able to get a preconf where 99% of builders will include by using something like MEV-commit or Commit-Boost. Preconfs will definitely take-off by mid 2025, the product development is tracking this (takeawy from Bangkok - Mev-commit is live.). clearly there are trade-offs to relying on trusted entities for front-running protection, but this is the status quo in Ethereum.

1 Like

That would be a further specialization - here I’m just talking about instructing all the Ethereum validators to store a piece of data (this would be implemented with SSTORE).

Yes, potentially, but this is only a temporary limitation anyways, which will go away once we implement a full protocol adapter capable of post-ordering execution. That said, it will often make sense anyways to compose transactions before sending them to the EVM, especially if they don’t rely on any EVM state changes (~ are more like zk-Rollups), so I expect that we’ll want some “sequencer affinity” for some applications.

2 Likes