cartesi / rollups-contracts

Smart Contracts for Cartesi Rollups
https://cartesi.github.io/rollups-contracts/
Apache License 2.0
22 stars 39 forks source link

Output Unification #42

Closed guidanoli closed 9 months ago

guidanoli commented 1 year ago

📚 Context

Currently, the off-chain machine is able to generate two types of outputs: vouchers and notices. On-chain, these outputs can be validated by anyone with the help of validity proofs.

The proof consists on reconstructing the epoch hash and comparing it with the epoch hash that was claimed by the DApp's consensus. If they are equal, then we can be sure the output is valid.

Currently, this epoch hash is composed by the following components.

As you can see, vouchers and notices each have their own separate Merkle tree. This helps to avoid misinterpreting notices as vouchers and vice versa.

There are, however, two main issues with this design. First, it unnecessarily complicates the on-chain and off-chain code. Second, it hinders the implementation of new output types, since we would have to add a new Merkle tree, which would result in a breaking change in the validation procedure.

✔️ Solution

And so, we noticed that things would be much simpler if we had a single tree for all verifiable outputs. To differentiate between output types, we could add a prefix to each output. For example, 0x00 for vouchers and 0x01 for notices. As a result, the epoch hash would only be composed of two components:

tuler commented 1 year ago

Can you add more context on why do we need more output types? Can you give examples?

guidanoli commented 1 year ago

Can you add more context on why do we need more output types? Can you give examples?

Of course. Take @fvbizzo's effort in implementing Complex Vouchers as an example. One cannot currently encode a timed voucher, or a voucher dependency, or a permissioned voucher. This is why we'd need to create new output types for these kinds of use cases. In reality, most—if not all—of these complex vouchers could be implemented with only one new output type: a voucher that could do a DELEGATECALL instead of a regular CALL opcode. Still, we cannot execute an arbitrary DELEGATECALL with the vouchers we have today, and thus the need to create new output types.

miltonjonat commented 1 year ago

Could you give practical use case examples for these suggested complex vouchers? For instance, I'm not sure in which application we would need a timed voucher, or if we have had any real demand for these. I also don't even understand what a permissioned voucher is ;)

guidanoli commented 1 year ago

@miltonjonat Sorry, I meant to say targeted vouchers! Let me try to give some examples of use cases for each of the vouchers proposed in the Complex Vouchers RFP.

pedroargento commented 1 year ago

Time based vouchers also have a bunch off applications in finance. For example, we could have a forward operation that allows me to buy X amount of a token in a future date for a certain price x. The dapp can generate two vouchers at the moment of the deal that can only be executed at the forward contract expiration date: one transfer of x eth from the buyer to the seller and a transfer of X tokens to the buyer.

Complex vouchers are being developed in response to RFP#003.

guidanoli commented 1 year ago

An alternative encoding for outputs

Currently, notices and vouchers are encoded with abi.encode.

abi.encode(notice)
abi.encode(destination, payload)

In order to unify the outputs in the same tree, we need to make their encodings disjoint (to avoid ambiguity and make it easier to detect the type of outputs). In this issue, I've proposed to prepend the output with a header like 0x00 or 0x01.

bytes1 constant NOTICE = bytes1(0x00);
bytes1 constant VOUCHER = bytes1(0x01);
// [...]
abi.encodePacked(NOTICE, notice)
abi.encodePacked(VOUCHER, destination, payload)

The problem with this solution is that if we create output types that have more than one arbitrary-length field, we'll have to use abi.encode instead of abi.encodePacked, or a conjunction of both. If we only use abi.encode, then either the header will have an unnecessary padding of 32 bytes, which will make it different from the encoding of the other outputs. If we use both, then we'll have an extra cost of double encoding. Another issue is that the choice of headers is completely arbitrary. With every new output type, we'll have to give it a new arbitrary number.

To solve these issues, I've come up with a better solution: to use Solidity's function call encoding. This is how the encoding of notices and vouchers would look:

abi.encodeWithSignature("Notice(bytes)", notice)
abi.encodeWithSignature("Voucher(address,bytes)", destination, payload)

With this encoding, we can use the output name as the function name, and the output fields as the function arguments. This removes the burden of having to come up with arbitrary numbers for output types, and uses the strict encoding of arguments out-of-the-box (like we do now). Besides, Solidiy already supports a nicer syntax for this encoding.

ZzzzHui commented 1 year ago

Very nice idea. abi.encodeWithSignature results in 32-4=28 less bytes than only using abi.encode with a header

guidanoli commented 1 year ago

Now, from the perspective of the machine, outputs can be thought of as arbitrary blobs. You will be able to tell them apart by the first 4 bytes, which correspond to the function selector of the function call. How this will be reflected on the HTTP API is still under discussion, but some options are:

miltonjonat commented 1 year ago

Given that this changes the public API that devs use, maybe it would be nice to consider inputs from the Prototyping and DevAd units. @cf-cartesi @gbarros would you like to comment?

guidanoli commented 1 year ago

Encoding and decoding outputs in Solidity

So, recently I discovered a nice idiomatic, type-safe way to encode function calls (which we'll be doing a lot). Here's the step-by-step process. I'll link to files from the output unification branch for reference.

  1. Define the function in an interface. The nice thing about this approach is that we can use this definition elsewhere, either when encoding or decoding these function calls. You can also easily add documentation in NatSpec format, which can be later used by solidity-docgen to generate documentation in Markdown. Furthermore, off-chain code will be able to easily encode/decode data through the contract ABIs and their language bindings. https://github.com/cartesi/rollups-contracts/blob/8c0a23ec835d9b58191a9f60f979526f1410776e/onchain/rollups/contracts/common/Outputs.sol#L9-L20
  2. Encode the function call data using abi.encodeCall, passing the function pointer as the first argument. After that, the function arguments are passed as a single tuple. And good news: this method is completely type-safe! That means that the compiler will raise an error if you try to encode a function call with the wrong arguments. Language bindings in typed languages will most likely perform these type checks as well. https://github.com/cartesi/rollups-contracts/blob/8c0a23ec835d9b58191a9f60f979526f1410776e/onchain/rollups/test/foundry/dapp/CartesiDApp.t.sol#L728-L732
  3. Decode the function call. First, you need to check if the byte array starts with the right selector. For this, I wrote a library called LibCalldata, which has a function called trimSelector. https://github.com/cartesi/rollups-contracts/blob/8c0a23ec835d9b58191a9f60f979526f1410776e/onchain/rollups/contracts/library/LibCalldata.sol#L14-L24
  4. With the selector trimmed, you can decode the function arguments with abi.decode. Here, you'll have to specify the argument types, unfortunately. https://github.com/cartesi/rollups-contracts/blob/8c0a23ec835d9b58191a9f60f979526f1410776e/onchain/rollups/contracts/dapp/CartesiDApp.sol#L140-L143
ZzzzHui commented 1 year ago

abi.encodeCall was introduced in 0.8.11 and bug fixed in 0.8.13 (ref), so we probably need pragma solidity ^0.8.13;

gligneul commented 1 year ago

I created an issue in rollups-node repository to track the changes on our side: https://github.com/cartesi/rollups-node/issues/103

mpolitzer commented 12 months ago

The machine-sdk side changes are being tracked here: https://github.com/cartesi/machine-emulator/issues/137

guidanoli commented 11 months ago

OpenZeppelin v5 also migrated from abi.encodeWithSelector and abi.encodeWithSignature to abi.encodeCall. https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4293