Skip to content

Commit dcc58da

Browse files
authored
Add protocol adapter integration pages (#356)
1 parent 0001602 commit dcc58da

File tree

5 files changed

+920
-0
lines changed

5 files changed

+920
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- [**#356**](https://github.com/anoma/nspec/pull/356): Add protocol adapter integration pages
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
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+
![Schematic depiction of the state correspondence design.](pa-evm.drawio.svg)
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

Comments
 (0)