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.
string matching the format /0x[a-f0-9]{40}/ - address
any string otherwise - string
true/false - boolean
number - int256, no uints
object, array, null, undefined - reject, refuse to encode (we can figure out ways to nest/pack arrays and objects in later versions)
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:
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.
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 aCanvas
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 bechain-ethereum
with one change, which is to configuresign()
with{ codec: 'raw' }
.Inside
codecs.ts
, we want to make theraw
codec default to eth compatible data.Currently, the codec's
encode()
function is called with aMessage<Session>
orMessage<Action>
to encode, and it's expected to return a [Uint8Array]. We should use some fixed way of translatingMessage<Session>
to an EIP712 message, something like:Here,
const domain
configures the domain separator/Domain, andconst session
configures aSession
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
value
's keys lexicographically.address
string
boolean
int256
, no uintsWe 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.
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.