This repo implements a keystore account type based on M-of-N ECDSA signatures for use with the Axiom Keystore. Creating a new keystore account type requires:
- Implementing a Key Data Consumer.
- Implementing a ZK Authentication Rule.
- Setting up a Signature Prover.
We implement the IKeyDataConsumer interface
function consumeKeyData(bytes calldata keyData, bytes calldata authData, bytes32 userOpHash) external;where
keyDatais expected to be of the formabi.encodePacked(bytes1(0x00), abi.encode(ecdsaConsumerCodehash, requiredSigners, allowedSignersList)).0x00is a domain separator.bytes32 ecdsaConsumerCodehashis the creation codehash of theECDSAConsumercontract.uint256 requiredSignersis the number of signers required to sign the user operation for execution.address[] allowedSignersListis the list of allowed signers.
authDatais expected to be a concatenated list of signatures where a signature(uint256 r, uint256 s, uint8 v)is encoded as the 65-byte payloadabi.encodePacked(r, s, v).userOpHashis a commitment to the user operation that is being executed and the value that signers should be signing.
The implementation also makes the following assumptions:
- There are no duplicate addresses in the
allowedSignersList. - The maximum length of
allowedSignersListis 256.
The contract verifies that the list of signatures recover to at least requiredSigners unique addresses in the allowedSignersList array. Otherwise, it reverts.
In the m_of_n_ecdsa/guest crate we define the our M-of-N ECDSA ZK authentication rule.
First, we define the authentication input, MOfNEcdsaInput, as follows:
pub type MOfNEcdsaInput = SignatureProverInput<MOfNEcdsaKeyData, MOfNEcdsaAuthData>;
#[derive(Clone, Serialize, Deserialize)]
pub struct MOfNEcdsaKeyData {
pub codehash: B256,
pub m: u32,
pub eoa_addrs: Vec<Address>,
}
impl KeyData for MOfNEcdsaKeyData {
/// ABI encoding of the data hash data: abi.encodePacked(0x00, abi.encode(codehash, m, eoa_addrs))
fn encode(&self) -> Bytes {
.. snip ..
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct MOfNEcdsaAuthData {
pub signatures: Vec<FixedBytes<65>>,
}We have used the SignatureProverInput struct and KeyData traits defined in the Keystore Periphery.
Next we define the authentication rule for a given MOfNEcdsaInput.
/// Circuit statement:
/// * keccak256(abi.encodePacked(0x00, abi.encode(codehash, m, eoa_addrs))) == data_hash
/// * there are [ECDSA signatures] for msg_hash which verifies against [pub_keys]
/// * [eoa_addrs] corresponds to [pub_keys]
pub fn verify(inputs: MOfNEcdsaInput) {
.. snip ..
}m_of_n_ecdsa_server wraps the guest program to serve it as a signature prover server, built on the libraries defined in the Keystore Periphery.
Signature prover server implements:
- Input Decoder (
MOfNEcdsaInputDecoder): decodes raw JSON-RPC inputs (AuthInputstype) into theMOfNEcdsaInputtype. - Input Validator (
MOfNEcdsaValidator): validates a givenMOfNEcdsaInput. The validation logic should exactly match the logic in the OpenVM guest program, since it is the last step before the inputs are passed to the guest program to be proven. Because proof generation takes a long time and is computationally expensive, all input errors should ideally be caught by validation before proof generation. - Signature prover server binary: serves the guest program as a signature prover server.
Additionally, the signature prover server implements:
- Test suite: to run and test the guest program.
- Keygen: to generate the proving and verifying keys.