hicommonwealth / commonwealth

A platform for decentralized communities
https://commonwealth.im
GNU General Public License v3.0
67 stars 42 forks source link

Custom signer for Ethereum-verifiable offchain actions and sessions #6064

Closed raykyri closed 7 months ago

raykyri commented 9 months ago

We want sessions and actions that can be verified on-chain by submitting them to a smart contract. The preferred way to verify signed messages on ETH is to receiving them as EIP712 signed typed data, and so we need a signer that uses those formats.

We can do this by creating a new Signer (e.g. @canvas-js/chain-ethereum-verifiable) that implements a custom SessionData and verification methods. When we update Commonwealth to Canvas 0.9/1.0, we can just initialize a Canvas instance inside Commonwealth with the Ethereum signer switched out for this verifiable signer. Then, non-Eth communities will transparently continue to use their own signing/verification methods, while Eth communities will automatically be upgraded to verifiable signatures (and other communities can be upgraded to be verifiable as that code is implemented).

Codec

Edit: chain-ethereum-verifiable should just be chain-ethereum with one change, which is to configure sign() with { codec: 'raw' }.

Inside codecs.ts, we want to make the raw codec default to eth compatible data.

Currently, the codec's encode() function is called with a Message<Session> or Message<Action> to encode, and it's expected to return a [Uint8Array]. We should use some fixed way of translating Message<Session> to an EIP712 message, something like:

const domain = [
    { name: "name", type: "string" }, // name should be exactly equal to the topic of the session
    { name: "version", type: "string" },
    { name: "chainId", type: "uint256" },
    { name: "verifyingContract", type: "address" },
    { name: "salt", type: "bytes32" },
];
const session = [
    { name: "address", type: "address" }, // the address that is delegated-signing the action
    { name: "publicKey", type: "address" }, // the burner address that is being authorized to sign actions
    { name: "blockhash", type: "string" }, // may be "" if no blockhash
    { name: "timestamp", type: "uint256" },
    { name: "duration", type: "uint256" },
];

Here, const domain configures the domain separator/Domain, and const session configures a Session typed data object.

Encoding

For actions, we need to figure out a way to convert JSON (or a subset of JSON) to 1) dynamically typed EIP712 data, and 2) some compatible serialization. Since we're using the raw codec, we're just going to use a built in ABI encoder that the EVM gives us to serialize/deserialize.

The EVM does not have types beyond bytearrays and int256/uint256, so

We also refuse to encode JS bytearrays but leave the door open to doing that later (since CBOR allows it).

TODO: We could restrict number to uint256 too. This might be more efficient for certain contracts. But, it's also not important as we aren't optimizing for gas right now.

Once we've constructed this type signature, we can just use abi.encode or another ABI encoder to serialize/deserialize the value, using the message type:

string memory ACTION_TYPE = "Action(int256 myValue)";

Then, we should pack the args as bytes, and insert it into the EIP712 payload.

const action = [
    { name: "name", type: "string" },
    { name: "args", type: "bytes" },
    { name: "address", type: "address" }, // the address that is delegated-signing the action
    { name: "session", type: "address" }, // the burner address that is directly signing the action
    { name: "blockhash", type: "string" }, // may be "" if no blockhash
    { name: "timestamp", type: "uint256" },
];

To serialize the entire message, we pack the ABI-encoded args inside the action object, ABI-encode that, and then pack the ABI-encoded action inside the message object, and ABI-encode it all one last time.

Verification

Same as above but in reverse.

raykyri commented 8 months ago

@rjwebb @joeltg Updated with specs on how to handle encoding.