|
| 1 | +--- |
| 2 | +icon: material/devices |
| 3 | +search: |
| 4 | + exclude: false |
| 5 | + boost: 2 |
| 6 | +tags: |
| 7 | + - work-in-progress |
| 8 | + - evm |
| 9 | + - resource-machine |
| 10 | + - protocol-adapter |
| 11 | +--- |
| 12 | + |
| 13 | +# Ethereum Virtual Machine Protocol Adapter |
| 14 | + |
| 15 | +The Ethereum Virtual Machine (EVM) protocol adapter is a smart contract written in [Solidity](https://soliditylang.org/) that can be deployed to EVM compatible chains and roll-ups to connect them to the Anoma protocol. In general, the aim of the protocol adapter is to allow Anoma applications to be run on existing EVM-compatible chains (similar to how drivers allow an operating system to be run on different pieces of physical hardware). |
| 16 | + |
| 17 | +The current prototype is a **settlement-only** protocol adapter, i.e., it is only capable of processing fully-evaluated transaction functions and therefore does not implement the full [[Executor Engine|executor engine]] behaviour. |
| 18 | + |
| 19 | +The implementation can be found in the [`anoma/evm-protocol-adapter` GH repo](https://github.com/anoma/evm-protocol-adapter). |
| 20 | + |
| 21 | +## Supported Networks |
| 22 | + |
| 23 | +For the upcoming product version v0.3, only the [Sepolia network](https://ethereum.org/en/developers/docs/networks/#sepolia) will be supported. |
| 24 | + |
| 25 | +## Storage |
| 26 | + |
| 27 | +The protocol adapter contract inherits the following storage components as Solidity contracts: |
| 28 | + |
| 29 | +- [[Commitment accumulator|Commitment Accumulator]] |
| 30 | +- [[Nullifier set|Nullifier Set]] |
| 31 | +- [[Stored data format#Data blob storage|Blob Storage]] |
| 32 | + |
| 33 | +Only the protocol adapter can call [non-view functions](https://docs.soliditylang.org/en/latest/contracts.html#view-functions) implemented by the storage components. |
| 34 | + |
| 35 | +### Commitment Accumulator |
| 36 | + |
| 37 | +The implementation uses a modified version of the [OpenZeppelin `MerkleTree` v.5.2.0](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.2.0/contracts/utils/structs/MerkleTree.sol) that populates the binary tree from left to right and stores leaf indices in a hash table |
| 38 | + |
| 39 | +```solidity |
| 40 | + mapping(bytes32 commitment => uint256 index) internal _indices; |
| 41 | +``` |
| 42 | + |
| 43 | +allowing for commitment existence checks. |
| 44 | + |
| 45 | +In addition to the leaves, the [modified implementation](https://github.com/anoma/evm-protocol-adapter/blob/main/src/state/CommitmentAccumulator.sol) stores also the intermediary node hashes. |
| 46 | + |
| 47 | +Historical Merkle tree roots are stored in an [OpenZeppelin `EnumerableSet` v5.2.0](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.2.0/contracts/utils/structs/EnumerableSet.sol) allowing for existence checks. |
| 48 | + |
| 49 | +### Nullifier Set |
| 50 | + |
| 51 | +The implementation uses an [OpenZeppelin `EnumerableSet` v5.2.0](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.2.0/contracts/utils/structs/EnumerableSet.sol) to store nullifiers of consumed resources and allow for existence checks. |
| 52 | + |
| 53 | +### Blob Storage |
| 54 | + |
| 55 | +The [implementation](https://github.com/anoma/evm-protocol-adapter/blob/main/src/state/BlobStorage.sol) uses a simple hash table to store blobs content-addressed. |
| 56 | + |
| 57 | +```solidity |
| 58 | +mapping(bytes32 blobHash => bytes blob) internal _blobs; |
| 59 | +``` |
| 60 | + |
| 61 | +From the [[Stored data format#data-blob-storage|list of deletion criteria]], the current blob storage implementation supports the following two: |
| 62 | + |
| 63 | +```solidity |
| 64 | +enum DeletionCriterion { |
| 65 | + Immediately, |
| 66 | + Never |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +## Hash Function |
| 71 | + |
| 72 | +For hashing, we compute the SHA-256 hash of the [strictly ABI-encoded](https://docs.soliditylang.org/en/latest/abi-spec.html#strict-encoding-mode) data. SHA-256 is available as a pre-compile in both the [EVM](https://www.evm.codes/precompiled) and [RISC ZERO zkVM](https://dev.risczero.com/api/zkvm/precompiles). |
| 73 | + |
| 74 | +## Types & Computable Components |
| 75 | + |
| 76 | +The RM-related type and computable component definitions in Solidity can be found in the [`src/Types.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/Types.sol) and [`src/libs/ComputableComponents.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/libs/ComputableComponents.sol) file, respectively. |
| 77 | + |
| 78 | +## Proving Systems |
| 79 | + |
| 80 | +For [resource logic proof](#resource-logic-proofs) and [compliance proof](#compliance-proofs) generation, we use [RISC ZERO](https://risczero.com/)'s proving libraries. |
| 81 | + |
| 82 | +For proof verification, we use the [RISC ZERO verifier contracts](https://dev.risczero.com/api/blockchain-integration/contracts/verifier#contract-addresses). |
| 83 | + |
| 84 | +### Resource Logic Proofs |
| 85 | + |
| 86 | +For the current prototype and the only supported example application [basic shielded Kudos ](https://research.anoma.net/t/basic-e2e-shielded-kudos-app/1237), we use a specific circuit resulting in the loss of function privacy. This will be improved in future iterations. |
| 87 | + |
| 88 | +The associated types are defined in [`proving/Compliance.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/proving/Compliance.sol). |
| 89 | + |
| 90 | +### Compliance Proofs |
| 91 | + |
| 92 | +Compliance units have a fixed size and contain references to one consumed and one created resource. For transaction with $n_\text{consumed} \neq n_\text{created}$, we expect padding resources (ephemeral resources with quantity 0) to be used. |
| 93 | + |
| 94 | +The associated types are defined in [`proving/Compliance.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/proving/Compliance.sol). |
| 95 | + |
| 96 | +### Delta Proofs |
| 97 | + |
| 98 | +The delta values are computed as 2D points (`uint256[2]`) on the `secp256k1` (K-256) curve and can be verified using ECDSA. |
| 99 | + |
| 100 | +The curve implementation is taken from [Witnet's `eliptic-curve-solidity` library v0.2.1](https://github.com/witnet/elliptic-curve-solidity/tree/0.2.1). This includes |
| 101 | + |
| 102 | +- [curve parameters](https://github.com/witnet/elliptic-curve-solidity/blob/0.2.1/examples/Secp256k1.sol) |
| 103 | +- [curve addition](https://github.com/witnet/elliptic-curve-solidity/blob/3510760b0f20c1156aea795e68b30fe62ce7c20f/contracts/EllipticCurve.sol#L165) (`ecAdd`) |
| 104 | +- [curve multiplication](https://github.com/witnet/elliptic-curve-solidity/blob/3510760b0f20c1156aea795e68b30fe62ce7c20f/contracts/EllipticCurve.sol#L239) (`ecMul`) |
| 105 | + |
| 106 | +We use the zero delta public key derived from the private key `0`. |
| 107 | + |
| 108 | +As the message digest, we use the transaction hash that we've defined as follows (see [`src/ProtocolAdapter.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/ProtocolAdapter.sol)): |
| 109 | + |
| 110 | +```solidity |
| 111 | +function _transactionHash(bytes32[] memory tags) internal pure returns (bytes32 txHash) { |
| 112 | + txHash = sha256(abi.encode(tags)); |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +For key recovery from the message digest and signature, we use [OpenZeppelin's `ECDSA` library](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol). |
| 117 | + |
| 118 | +## EVM and RM State Correspondence |
| 119 | + |
| 120 | +Taking a protocol adapter contract-centric viewpoint, we distinguish between two types of EVM state: |
| 121 | + |
| 122 | +1. Internal [[Resource Machine|resource machine (RM)]] state being maintained inside the protocol adapter contract that is constituted by commitments, nullifiers, and blobs (see [Storage](#storage)). |
| 123 | +2. External state existing in smart contracts which are independent of the protocol adapter and its internal RM state. |
| 124 | + |
| 125 | +To **interoperate with state in external contracts**, the protocol adapter contract can, during transaction execution, **make read and write calls** to them and **create and consume corresponding resources** in its internal state reflecting the external state reads and writes. |
| 126 | + |
| 127 | +We achieve this by creating an indirection layer separating the protocol adapter from |
| 128 | +the external contract and resources that should be created and consumed in |
| 129 | +consequence. It consists of: |
| 130 | + |
| 131 | +- A [forwarder contract](#forwarder-contract) that |
| 132 | + - performs the actual state read or write calls into the target contract and returns eventual return data |
| 133 | + - is custom-built for the target contract to call and permissionlessly deployed by 3rd parties |
| 134 | + |
| 135 | +- A [calldata carrier resource](#calldata-carrier-resource) (singleton) that |
| 136 | + - must be part of the action data structure containing the forwarder call instruction |
| 137 | + - carries the inputs and outputs of the forwarded call |
| 138 | + - expresses constraints over other resources that must be present and correspond to the external call |
| 139 | + |
| 140 | +and allows the application to ensure the correspondence. |
| 141 | + |
| 142 | + |
| 143 | + |
| 144 | +This works as follows: |
| 145 | + |
| 146 | +The protocol adapter accepts an optional `ForwarderCalldata` struct with the RM transaction object as part of the action object (see [`src/Types.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/Types.sol)): |
| 147 | + |
| 148 | +```solidity |
| 149 | +struct ForwarderCalldata { |
| 150 | + address untrustedForwarderContract; |
| 151 | + bytes input; |
| 152 | + bytes output; |
| 153 | +} |
| 154 | +``` |
| 155 | +The struct contains the address of the [forwarder contract](#forwarder-contract) and `bytes input` data required for the intended state read or write calls on the target contract. It also contains the `bytes output` data that must match the data returned from the call. |
| 156 | + |
| 157 | +The protocol adapter ensures the `ForwarderCalldata` is part of the app data of the [singleton calldata carrier resources](#calldata-carrier-resource) that has a pre-determined kind being referenced in the forwarder contract. |
| 158 | + |
| 159 | +!!! note |
| 160 | + In the current, settlement-only protocol adapter design, the `output` data must already be known during proving time to be checked by resource logics and therefore is part of the `ForwarderCalldata` struct. |
| 161 | + |
| 162 | +The binding between the created calldata carrier resource and the called forwarder contract is ensured through the protocol adapter, which |
| 163 | + |
| 164 | +1. is the exclusive caller of the forwarder contract, |
| 165 | +2. ensures the presence of the created calldata carrier resource in correspondence to the call in the transaction, |
| 166 | +3. ensures that the forwarder contract call input data, call output data, and address is available in the app data entry of the created calldata carrier resource under its commitment, |
| 167 | +4. ensures that the kind of the created calldata carrier resource matches the kind being immutably referenced in the forwarder contract. This way, the calldata carrier resource logic and label are fully determined by the forwarder contract. |
| 168 | + |
| 169 | +Because the calldata carrier resource is a singleton, we know that the consumption of the old carrier is guaranteed through the transaction balance property. |
| 170 | + |
| 171 | +The created calldata carrier resource, in turn, can enforce creation or consumption of other [resources corresponding to external state](#resources-corresponding-to-external-state). |
| 172 | + |
| 173 | +In the following we describe the components in more detail. |
| 174 | + |
| 175 | +### Forwarder Contract |
| 176 | + |
| 177 | +The forwarder contract |
| 178 | + |
| 179 | +- is only callable by the protocol adapter |
| 180 | +- has the address to the external contract it corresponds to |
| 181 | +- forwards arbitrary calls to the external contract to read and write its state and changes the call context (i.e., [`msg.sender` and `msg.data`](https://docs.soliditylang.org/en/latest/units-and-global-variables.html#block-and-transaction-properties)) |
| 182 | +- returns the call return data to the protocol adapter |
| 183 | + |
| 184 | +The resulting indirection has the purpose to keep custom logic such as |
| 185 | + |
| 186 | +- callback logic (e.g., required by [ERC-721](https://eips.ethereum.org/EIPS/eip-721) or [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) tokens) |
| 187 | +- escrow logic (e.g., required to wrap owned state into resources) |
| 188 | +- event logic (e.g., required for EVM indexers) |
| 189 | + |
| 190 | +separate and independent of the protocol adapter contract. This allows the forwarder contract to be custom-built and permissionlessly deployed by untrusted 3rd parties. |
| 191 | + |
| 192 | +Besides referencing the external contract by its address, the forwarder contract must also reference |
| 193 | +the resource kind of the associated [calldata carrier resource](#calldata-carrier-resource) that the protocol adapter will require be created. This allows the forwarder contract to also to enforce its own contract address to be part of the carrier resource label, which ensures that the correspondence between the forwarder and carrier resource is unique. |
| 194 | + |
| 195 | +!!! note |
| 196 | + The mutual dependency between |
| 197 | + - the calldata carrier resource label containing the forwarder contract address |
| 198 | + - the forwarder contract referencing the calldata carrier resource label |
| 199 | + |
| 200 | + can be established by deterministic deployment or post-deployment initialization of the forwarder contract. |
| 201 | + |
| 202 | +## Implementation Details |
| 203 | + |
| 204 | +A minimal implementation is shown below: |
| 205 | + |
| 206 | +```solidity |
| 207 | +contract ExampleForwarder is Ownable { |
| 208 | + bytes32 internal immutable _CALLDATA_CARRIER_RESOURCE_KIND; |
| 209 | + address internal immutable _CONTRACT; |
| 210 | +
|
| 211 | + constructor(address protocolAdapter, bytes32 calldataCarrierLogicRef) Ownable(protocolAdapter) { |
| 212 | + _CALLDATA_CARRIER_RESOURCE_KIND = ComputableComponents.kind({ |
| 213 | + logicRef: calldataCarrierLogicRef, |
| 214 | + labelRef: sha256(abi.encode(address(this))) |
| 215 | + }); |
| 216 | + } |
| 217 | +
|
| 218 | + function forwardCall(bytes calldata input) external onlyOwner returns (bytes memory output) { |
| 219 | + output = _CONTRACT.functionCall(input); |
| 220 | + } |
| 221 | +
|
| 222 | + function calldataCarrierResourceKind() external view returns (bytes32 kind){ |
| 223 | + kind = _CALLDATA_CARRIER_RESOURCE_KIND; |
| 224 | + } |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +The required calldata is passed with the RM transaction object as part of the `Action` struct (see [`src/Types.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/Types.sol)). |
| 229 | + |
| 230 | +```solidity |
| 231 | +struct ForwarderCalldata { |
| 232 | + address untrustedForwarderContract; |
| 233 | + bytes input; |
| 234 | + bytes output; |
| 235 | +} |
| 236 | +``` |
| 237 | + |
| 238 | +On transaction execution by the protocol adapter, the `ForwarderCalldata` struct is processed as follows: |
| 239 | + |
| 240 | +```solidity |
| 241 | +function _executeForwarderCall(ForwarderCalldata calldata call) internal { |
| 242 | + bytes memory output = IForwarder(call.untrustedForwarder).forwardCall(call.input); |
| 243 | +
|
| 244 | + if (keccak256(output) != keccak256(call.output)) { |
| 245 | + revert ForwarderCallOutputMismatch({ expected: call.output, actual: output }); |
| 246 | + } |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +The forwarder contract base class can be found in [`src/ForwarderBase.sol`](https://github.com/anoma/evm-protocol-adapter/blob/main/src/ForwarderBase.sol). |
| 251 | + |
| 252 | +### Calldata Carrier Resource |
| 253 | + |
| 254 | +A calldata carrier resource is a singleton (i.e., it has a unique kind ensuring that only a single instance with quantity 1 exists) being bound to an associated forwarder contract. |
| 255 | +By default, calldata carrier resources can be consumed by everyone (because their nullifier key commitment is derived from the [[Identity Architecture#true-all|universal identity]]). |
| 256 | + |
| 257 | +!!! note |
| 258 | + When the singleton calldata carrier resource is consumed in a transaction, subsequent transactions in the same block cannot consume it anymore. This effectively limits the current design to a single forwarder contract call per block (if the commitment of the latest, unspent calldata carrier resource is not known to the subsequent transaction ahead of time). This will be improved in upcoming protocol adapter versions. |
| 259 | + |
| 260 | +The calldata carrier resource object is passed to the protocol adapter together with the `ForwarderCalldata` struct (see [`src/Types.sol`](https://github.com/anoma/evm-protocol-adapter/blob/6cdf69b92f58d56dc13df1c0b52539295ea59814/src/Types.sol#L31)): |
| 261 | + |
| 262 | + |
| 263 | +```solidity |
| 264 | +struct ResourceForwarderCalldataPair { |
| 265 | + Resource carrier; |
| 266 | + ForwarderCalldata call; |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +This allows the protocol adapter to ensure that |
| 271 | +1. the calldata carrier resource kind matches the one referenced in the forwarder contract and |
| 272 | +2. a corresponding `action.appData` entry exists for the calldata carrier resource commitment tag that includes the `ForwarderCalldata`. |
| 273 | + |
| 274 | +The latter allows calldata carrier to inspect the `bytes input` and `bytes output` in the `ForwarderCalldata` |
| 275 | +and ensure the creation and consumption of [resources corresponding to the external state reads or writes](#resources-corresponding-to-external-state) in the same action. |
| 276 | +Moreover, it can integrity check that its own label matches the `untrustedForwarderContract` address. |
| 277 | + |
| 278 | +This enables applications, such as wrapping [ERC20](https://eips.ethereum.org/EIPS/eip-20) tokens into resources, which works by |
| 279 | +1. transferring tokens from an owner into the forwarder contract via `transferFrom` and |
| 280 | +2. initializing an owned resource with a corresponding quantity and the kind being specified in the forwarder contract. |
| 281 | + |
| 282 | + |
| 283 | +### Resources Corresponding to External State |
| 284 | + |
| 285 | +Resources can correspond to EVM state and correspond to a specific [calldata carrier resource](#calldata-carrier-resource) kind being referenced in their label. Their initialization and finalization logic requires a created calldata carrier resource to be part of the same action. |
| 286 | + |
| 287 | +## Transaction Flow |
| 288 | + |
| 289 | +The protocol adapter transaction flow is shown below: |
| 290 | + |
| 291 | +```kroki-mermaid |
| 292 | +sequenceDiagram |
| 293 | + autonumber |
| 294 | + title: Transaction Flow of the Settlement-Only EVM Protocol Adapter |
| 295 | +
|
| 296 | +
|
| 297 | + actor Alice as User Alice |
| 298 | + actor Bob as User Bob |
| 299 | +
|
| 300 | + Note over TF, R0B: Resource Logic, Compliance, <br> and Delta Proof Computation |
| 301 | + box Anoma Client (Local) |
| 302 | + participant TF as Transaction<br>Function |
| 303 | + participant R0B as RISC ZERO<br>Backend |
| 304 | + end |
| 305 | +
|
| 306 | + Alice ->> TF: call |
| 307 | +
|
| 308 | + par Local Proving |
| 309 | + activate TF |
| 310 | + TF ->> R0B: prove |
| 311 | + R0B -->> TF: proofs |
| 312 | + end |
| 313 | + TF ->> IP: send tx1 (intent) |
| 314 | + deactivate TF |
| 315 | +
|
| 316 | + Bob ->> TF: call |
| 317 | +
|
| 318 | + activate TF |
| 319 | + TF ->> IP: send tx2 (intent) |
| 320 | + deactivate TF |
| 321 | +
|
| 322 | + Note over IP, Sally: Intent Matching |
| 323 | + box Anoma P2P Node |
| 324 | + participant IP as Intent<br>Pool |
| 325 | + actor Sally as Solver Sally |
| 326 | + end |
| 327 | +
|
| 328 | + Sally -->> IP: monitor pool |
| 329 | + activate Sally |
| 330 | + Sally ->> Sally: compose(tx1,tx2) |
| 331 | +
|
| 332 | + box EVM |
| 333 | + participant R0V as RISC ZERO<br>Verifier Contract |
| 334 | + participant PA as Protocol Adapter<br>Contract |
| 335 | + participant Forwarder as Forwarder<br>Contract |
| 336 | + participant Ext as External<br>Contract |
| 337 | + end |
| 338 | + Sally ->> PA: eth_sendTransaction [execute(tx)] |
| 339 | + deactivate Sally |
| 340 | +
|
| 341 | + %%par verify(tx) |
| 342 | + PA ->> R0V: verify(tx) |
| 343 | + %%end |
| 344 | +
|
| 345 | + par Settlement |
| 346 | + opt Optional Forwarder Calls |
| 347 | + Note over PA,Ext: Read/write external state |
| 348 | + PA ->> Forwarder: Forwarder Call <br>forwardCall(call.input) |
| 349 | + Forwarder ->> Ext: Forwarded Call |
| 350 | + opt |
| 351 | + Ext -->> Forwarder: Return Data |
| 352 | + end |
| 353 | + Forwarder -->> PA: Return Data<br>call.output |
| 354 | + end |
| 355 | + Note over PA: Update internal state |
| 356 | + PA ->> PA: add nullifiers,<br>commitments, blobs |
| 357 | + end |
| 358 | +``` |
| 359 | + |
| 360 | +1. A user Alice calls a transaction function of a Juvix application to produce an ARM transaction object (here expressing an intent) as well as the instances and witnesses for the various proof types (resource logic, compliance, and delta proofs). |
| 361 | +2. The transaction function requests proofs from the RISC ZERO backend. |
| 362 | +3. The backend returns the proofs for the transaction object. |
| 363 | +4. The Anoma client sends the intent transaction object |
| 364 | + to the intent pool. |
| 365 | +5. Another user Bob expresses his intent (see 1. to 3.). |
| 366 | +6. See 4. |
| 367 | +7. A solver Sally monitors the intent pool and sees the intent transactions by Alice and Bob and finds a match (using her algorithm). |
| 368 | +8. Sally composes the intent transactions and adds her own actions s.t. the transaction becomes balanced & valid. She converts the transaction object into the format required by the EVM protocol adapter. |
| 369 | +9. Sally being connected to an Ethereum node makes an [`eth_sendTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) call into the protocol adapter's `execute(Transaction tx)` function, which she signs with the private key of her account. |
| 370 | +10. The protocol adapter verifies the proofs from 3. by calling a RISC ZERO verifier contract deployed on the network. |
| 371 | +11. The protocol adapter makes an optional forwarder contract call by using the `ForwarderCalldata.input` data. |
| 372 | +12. The forwarder contract forwards the call to an external target contract to read from or write to its state. |
| 373 | +13. Optional return data is passed back to the forwarder contract. |
| 374 | +14. Return data (that can be empty) is passed to the protocol adapter contract allowing it to conduct integrity checks (by requiring the same data to be part of `action.appData`). |
| 375 | +15. The protocol adapter updates its internal state by storing |
| 376 | + |
| 377 | + - nullifiers of consumed resources |
| 378 | + |
| 379 | + - commitments of created resources |
| 380 | + |
| 381 | + - blobs with deletion criteria `!= DeletionCriterion.Immediately`. |
0 commit comments