Skip to content

Commit

Permalink
feat(multisig): add optional signature validation callback (#270)
Browse files Browse the repository at this point in the history
* add signature verifier api

* add call to signature verification

* fix compilation errors

* fix error handling

* add unit test

* update parameter type

* update documentation
  • Loading branch information
eguajardo authored Feb 15, 2024
1 parent 8f99242 commit 0ced191
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 14 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ serde = { version = "1.0.145", default-features = false, features = ["derive"] }
serde_json = "1.0.89"
schemars = "0.8.10"
sha3 = { version = "0.10.8", default-features = false, features = [] }
signature-verifier-api = { version = "^0.1.0", path = "packages/signature-verifier-api" }

[profile.release]
opt-level = 3
Expand Down
1 change: 1 addition & 0 deletions contracts/multisig/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ schemars = "0.8.10"
serde = { version = "1.0.145", default-features = false, features = ["derive"] }
serde_json = "1.0.89"
sha3 = { workspace = true }
signature-verifier-api = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
Expand Down
5 changes: 3 additions & 2 deletions contracts/multisig/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,17 @@ pub fn execute(
} => {
execute::require_authorized_caller(&deps, info.sender)?;

let _sig_verifier = sig_verifier
let sig_verifier = sig_verifier
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?; // TODO: handle callback
.transpose()?;
execute::start_signing_session(
deps,
env,
worker_set_id,
msg.try_into()
.map_err(axelar_wasm_std::ContractError::from)?,
chain_name,
sig_verifier,
)
}
ExecuteMsg::SubmitSignature {
Expand Down
12 changes: 12 additions & 0 deletions contracts/multisig/src/contract/execute.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use connection_router::state::ChainName;
use cosmwasm_std::WasmMsg;
use sha3::{Digest, Keccak256};
use signature_verifier_api::client::SignatureVerifier;

use crate::signing::validate_session_signature;
use crate::state::{load_session_signatures, save_pub_key, save_signature};
Expand All @@ -20,6 +21,7 @@ pub fn start_signing_session(
worker_set_id: String,
msg: MsgToSign,
chain_name: ChainName,
sig_verifier: Option<Addr>,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let worker_set = get_worker_set(deps.storage, &worker_set_id)?;
Expand All @@ -40,6 +42,7 @@ pub fn start_signing_session(
chain_name.clone(),
msg.clone(),
expires_at,
sig_verifier,
);

SIGNING_SESSIONS.save(deps.storage, session_id.into(), &signing_session)?;
Expand Down Expand Up @@ -81,12 +84,21 @@ pub fn submit_signature(

let signature: Signature = (pub_key.key_type(), signature).try_into()?;

let sig_verifier = session
.sig_verifier
.clone()
.map(|address| SignatureVerifier {
address,
querier: deps.querier,
});

validate_session_signature(
&session,
&info.sender,
&signature,
pub_key,
env.block.height,
sig_verifier,
)?;
let signature = save_signature(deps.storage, session_id, signature, &info.sender)?;

Expand Down
11 changes: 6 additions & 5 deletions contracts/multisig/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ pub enum ExecuteMsg {
worker_set_id: String,
msg: HexBinary,
chain_name: ChainName,
/* Address of a contract responsible for signature verification.
The multisig contract verifies each submitted signature by default.
But some chains need custom verification beyond this, so the verification can be optionally overridden.
If a callback address is provided, signature verification is handled by the contract at that address
instead of the multisig contract. TODO: define interface for callback */
/// Address of a contract responsible for signature verification.
/// The multisig contract verifies each submitted signature by default.
/// But some chains need custom verification beyond this, so the verification can be optionally overridden.
/// If a callback address is provided, signature verification is handled by the contract at that address
/// instead of the multisig contract. Signature verifier contracts must implement interface defined in
/// [signature_verifier_api::msg]
sig_verifier: Option<String>,
},
SubmitSignature {
Expand Down
92 changes: 85 additions & 7 deletions contracts/multisig/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::collections::HashMap;
use connection_router::state::ChainName;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Uint256, Uint64};
use signature_verifier_api::client::SignatureVerifier;

use crate::{
key::{PublicKey, Signature},
Expand All @@ -19,6 +20,7 @@ pub struct SigningSession {
pub msg: MsgToSign,
pub state: MultisigState,
pub expires_at: u64,
pub sig_verifier: Option<Addr>,
}

impl SigningSession {
Expand All @@ -28,6 +30,7 @@ impl SigningSession {
chain_name: ChainName,
msg: MsgToSign,
expires_at: u64,
sig_verifier: Option<Addr>,
) -> Self {
Self {
id: session_id,
Expand All @@ -36,6 +39,7 @@ impl SigningSession {
msg,
state: MultisigState::Pending,
expires_at,
sig_verifier,
}
}

Expand All @@ -61,14 +65,32 @@ pub fn validate_session_signature(
signature: &Signature,
pub_key: &PublicKey,
block_height: u64,
sig_verifier: Option<SignatureVerifier>,
) -> Result<(), ContractError> {
if session.expires_at < block_height {
return Err(ContractError::SigningSessionClosed {
session_id: session.id,
});
}

if !signature.verify(&session.msg, pub_key)? {
let valid = sig_verifier.map_or_else(
|| signature.verify(&session.msg, pub_key),
|verifier| {
verifier
.verify_signature(
signature.as_ref().into(),
session.msg.as_ref().into(),
pub_key.as_ref().into(),
signer.to_string(),
session.id,
)
.map_err(|err| ContractError::SignatureVerificationFailed {
reason: err.to_string(),
})
},
)?;

if !valid {
return Err(ContractError::InvalidSignature {
session_id: session.id,
signer: signer.into(),
Expand All @@ -93,7 +115,10 @@ fn signers_weight(signatures: &HashMap<String, Signature>, worker_set: &WorkerSe

#[cfg(test)]
mod tests {
use cosmwasm_std::{testing::MockStorage, Addr, HexBinary};
use cosmwasm_std::{
testing::{MockQuerier, MockStorage},
to_binary, Addr, HexBinary, QuerierWrapper,
};

use crate::{
key::KeyType,
Expand Down Expand Up @@ -128,6 +153,7 @@ mod tests {
"mock-chain".parse().unwrap(),
message.clone(),
expires_at,
None,
);

let signatures: HashMap<String, Signature> = signers
Expand Down Expand Up @@ -166,6 +192,7 @@ mod tests {
"mock-chain".parse().unwrap(),
message.clone(),
expires_at,
None,
);

let signatures: HashMap<String, Signature> = signers
Expand Down Expand Up @@ -217,7 +244,50 @@ mod tests {
let signature = config.signatures.values().next().unwrap();
let pub_key = &worker_set.signers.get(&signer.to_string()).unwrap().pub_key;

assert!(validate_session_signature(&session, &signer, signature, pub_key, 0).is_ok());
assert!(
validate_session_signature(&session, &signer, signature, pub_key, 0, None).is_ok()
);
}
}

#[test]
fn validation_through_signature_verifier_contract() {
for config in [ecdsa_setup(), ed25519_setup()] {
let session = config.session;
let worker_set = config.worker_set;
let signer = Addr::unchecked(config.signatures.keys().next().unwrap());
let signature = config.signatures.values().next().unwrap();
let pub_key = &worker_set.signers.get(&signer.to_string()).unwrap().pub_key;

for verification in [true, false] {
let mut querier = MockQuerier::default();
querier.update_wasm(move |_| Ok(to_binary(&verification).into()).into());
let sig_verifier = Some(SignatureVerifier {
address: Addr::unchecked("verifier".to_string()),
querier: QuerierWrapper::new(&querier),
});

let result = validate_session_signature(
&session,
&signer,
signature,
pub_key,
0,
sig_verifier,
);

if verification {
assert!(result.is_ok());
} else {
assert_eq!(
result.unwrap_err(),
ContractError::InvalidSignature {
session_id: session.id,
signer: signer.clone().into(),
}
);
}
}
}
}

Expand All @@ -236,7 +306,8 @@ mod tests {
&signer,
signature,
pub_key,
block_height
block_height,
None
)
.is_ok());
}
Expand All @@ -252,8 +323,14 @@ mod tests {
let block_height = 12346;
let pub_key = &worker_set.signers.get(&signer.to_string()).unwrap().pub_key;

let result =
validate_session_signature(&session, &signer, signature, pub_key, block_height);
let result = validate_session_signature(
&session,
&signer,
signature,
pub_key,
block_height,
None,
);

assert_eq!(
result.unwrap_err(),
Expand Down Expand Up @@ -281,7 +358,8 @@ mod tests {
.try_into()
.unwrap();

let result = validate_session_signature(&session, &signer, &invalid_sig, pub_key, 0);
let result =
validate_session_signature(&session, &signer, &invalid_sig, pub_key, 0, None);

assert_eq!(
result.unwrap_err(),
Expand Down
36 changes: 36 additions & 0 deletions doc/src/contracts/multisig.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,48 @@ Prover-->>-Relayer: returns data and proof
```

## Custom signature verification

If the multisig contract doesn't natively support the required signature verification, the `sig_verifier` parameter in `ExecuteMsg::StartSigningSession` can be set by the prover to specify a custom signature verification contract. The custom contract must implement the following interface defined in `packges/signature-verifier-api`:

```Rust
pub enum QueryMsg {
#[returns(bool)]
VerifySignature {
signature: HexBinary,
message: HexBinary,
public_key: HexBinary,
signer_address: String,
session_id: Uint64,
},
}
```

In case a custom verification contract is specified, when a signature is submitted, the multisig contract will call the `VerifySignature` query on the custom contract to verify the signature, which in turn should return `true` if the signature is valid or `false` otherwise.

```mermaid
sequenceDiagram
actor Signers
box LightYellow Axelar
participant Multisig
participant SignatureVerifier
end
Signers->>+Multisig: ExecuteMsg::SubmitSignature
Multisig->>SignatureVerifier: QueryMsg::VerifySignature
SignatureVerifier->>Multisig: returns true/false
deactivate Multisig
```

## Interface

```Rust
pub enum ExecuteMsg {
StartSigningSession {
worker_set_id: String,
msg: HexBinary,
chain_name: ChainName,
sig_verifier: Option<String>,
},
SubmitSignature {
session_id: Uint64,
Expand Down
12 changes: 12 additions & 0 deletions packages/signature-verifier-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "signature-verifier-api"
version = "0.1.0"
rust-version = { workspace = true }
edition = "2021"

[dependencies]
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-storage = { workspace = true }
error-stack = { workspace = true }
thiserror = { workspace = true }
Loading

0 comments on commit 0ced191

Please sign in to comment.