Signer changes (#1557)
* Signer and signable ethereum signer changes move signers into chain config (chains.rs) Undo removal of traits in hyperlane-core/traits/mod.rs fix chains.rs validator submit remove signers from hyperlane core Announcements and checkpoint Remove test_output trait_builder.rs hyperlane base settings mod base settings signers base settings mod trait builder multisig Undo checkpoint syncer change for now local storage s3 storage validator metadata builder validator submit relayer run locally configs config and deployment fmt * WIP * some refactoring of signing * fmt * Null signer in config * Fix after mergepull/1568/head
parent
1ff35f63e2
commit
65b10f1376
@ -0,0 +1,146 @@ |
||||
use async_trait::async_trait; |
||||
use ethers::prelude::{Address, Signature}; |
||||
use ethers::types::transaction::eip2718::TypedTransaction; |
||||
use ethers::types::transaction::eip712::Eip712; |
||||
use ethers_signers::{AwsSigner, AwsSignerError, LocalWallet, Signer, WalletError}; |
||||
|
||||
use hyperlane_core::{HyperlaneSigner, HyperlaneSignerError, H160, H256}; |
||||
|
||||
/// Ethereum-supported signer types
|
||||
#[derive(Debug, Clone)] |
||||
pub enum Signers { |
||||
/// A wallet instantiated with a locally stored private key
|
||||
Local(LocalWallet), |
||||
/// A signer using a key stored in aws kms
|
||||
Aws(AwsSigner<'static>), |
||||
} |
||||
|
||||
impl From<LocalWallet> for Signers { |
||||
fn from(s: LocalWallet) -> Self { |
||||
Signers::Local(s) |
||||
} |
||||
} |
||||
|
||||
impl From<AwsSigner<'static>> for Signers { |
||||
fn from(s: AwsSigner<'static>) -> Self { |
||||
Signers::Aws(s) |
||||
} |
||||
} |
||||
|
||||
#[async_trait] |
||||
impl Signer for Signers { |
||||
type Error = SignersError; |
||||
|
||||
async fn sign_message<S: Send + Sync + AsRef<[u8]>>( |
||||
&self, |
||||
message: S, |
||||
) -> Result<Signature, Self::Error> { |
||||
match self { |
||||
Signers::Local(signer) => Ok(signer.sign_message(message).await?), |
||||
Signers::Aws(signer) => Ok(signer.sign_message(message).await?), |
||||
} |
||||
} |
||||
|
||||
async fn sign_transaction(&self, message: &TypedTransaction) -> Result<Signature, Self::Error> { |
||||
match self { |
||||
Signers::Local(signer) => Ok(signer.sign_transaction(message).await?), |
||||
Signers::Aws(signer) => Ok(signer.sign_transaction(message).await?), |
||||
} |
||||
} |
||||
|
||||
async fn sign_typed_data<T: Eip712 + Send + Sync>( |
||||
&self, |
||||
payload: &T, |
||||
) -> Result<Signature, Self::Error> { |
||||
match self { |
||||
Signers::Local(signer) => Ok(signer.sign_typed_data(payload).await?), |
||||
Signers::Aws(signer) => Ok(signer.sign_typed_data(payload).await?), |
||||
} |
||||
} |
||||
|
||||
fn address(&self) -> Address { |
||||
match self { |
||||
Signers::Local(signer) => signer.address(), |
||||
Signers::Aws(signer) => signer.address(), |
||||
} |
||||
} |
||||
|
||||
fn chain_id(&self) -> u64 { |
||||
match self { |
||||
Signers::Local(signer) => signer.chain_id(), |
||||
Signers::Aws(signer) => signer.chain_id(), |
||||
} |
||||
} |
||||
|
||||
fn with_chain_id<T: Into<u64>>(self, chain_id: T) -> Self { |
||||
match self { |
||||
Signers::Local(signer) => signer.with_chain_id(chain_id).into(), |
||||
Signers::Aws(signer) => signer.with_chain_id(chain_id).into(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[async_trait] |
||||
impl HyperlaneSigner for Signers { |
||||
fn eth_address(&self) -> H160 { |
||||
Signer::address(self) |
||||
} |
||||
|
||||
async fn sign_hash(&self, hash: &H256) -> Result<Signature, HyperlaneSignerError> { |
||||
let mut signature = Signer::sign_message(self, hash) |
||||
.await |
||||
.map_err(|err| HyperlaneSignerError::from(Box::new(err) as Box<_>))?; |
||||
signature.v = 28 - (signature.v % 2); |
||||
Ok(signature) |
||||
} |
||||
} |
||||
|
||||
/// Error types for Signers
|
||||
#[derive(Debug, thiserror::Error)] |
||||
pub enum SignersError { |
||||
/// AWS Signer Error
|
||||
#[error("{0}")] |
||||
AwsSignerError(#[from] AwsSignerError), |
||||
/// Wallet Signer Error
|
||||
#[error("{0}")] |
||||
WalletError(#[from] WalletError), |
||||
} |
||||
|
||||
impl From<std::convert::Infallible> for SignersError { |
||||
fn from(_error: std::convert::Infallible) -> Self { |
||||
panic!("infallible") |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod test { |
||||
use hyperlane_core::{Checkpoint, HyperlaneSigner, HyperlaneSignerExt, H256}; |
||||
|
||||
use crate::signers::Signers; |
||||
|
||||
#[test] |
||||
fn it_sign() { |
||||
let t = async { |
||||
let signer: Signers = |
||||
"1111111111111111111111111111111111111111111111111111111111111111" |
||||
.parse::<ethers::signers::LocalWallet>() |
||||
.unwrap() |
||||
.into(); |
||||
let message = Checkpoint { |
||||
mailbox_address: H256::repeat_byte(2), |
||||
mailbox_domain: 5, |
||||
root: H256::repeat_byte(1), |
||||
index: 123, |
||||
}; |
||||
|
||||
let signed = signer.sign(message).await.expect("!sign"); |
||||
assert!(signed.signature.v == 27 || signed.signature.v == 28); |
||||
signed.verify(signer.eth_address()).expect("!verify"); |
||||
}; |
||||
tokio::runtime::Builder::new_current_thread() |
||||
.enable_all() |
||||
.build() |
||||
.unwrap() |
||||
.block_on(t) |
||||
} |
||||
} |
@ -0,0 +1,173 @@ |
||||
//! Test functions that output json files
|
||||
|
||||
#![cfg(test)] |
||||
|
||||
use std::{fs::OpenOptions, io::Write, str::FromStr}; |
||||
|
||||
use hex::FromHex; |
||||
use serde_json::{json, Value}; |
||||
|
||||
use ethers::signers::Signer; |
||||
use hyperlane_core::{ |
||||
accumulator::{ |
||||
merkle::{merkle_root_from_branch, MerkleTree}, |
||||
TREE_DEPTH, |
||||
}, |
||||
test_utils, |
||||
utils::domain_hash, |
||||
Checkpoint, HyperlaneMessage, HyperlaneSignerExt, H160, H256, |
||||
}; |
||||
use hyperlane_ethereum::Signers; |
||||
|
||||
/// Output proof to /vector/message.json
|
||||
pub fn output_message() { |
||||
let hyperlane_message = HyperlaneMessage { |
||||
nonce: 0, |
||||
version: 0, |
||||
origin: 1000, |
||||
sender: H256::from(H160::from_str("0x1111111111111111111111111111111111111111").unwrap()), |
||||
destination: 2000, |
||||
recipient: H256::from( |
||||
H160::from_str("0x2222222222222222222222222222222222222222").unwrap(), |
||||
), |
||||
body: Vec::from_hex("1234").unwrap(), |
||||
}; |
||||
|
||||
let message_json = json!({ |
||||
"nonce": hyperlane_message.nonce, |
||||
"version": hyperlane_message.version, |
||||
"origin": hyperlane_message.origin, |
||||
"sender": hyperlane_message.sender, |
||||
"destination": hyperlane_message.destination, |
||||
"recipient": hyperlane_message.recipient, |
||||
"body": hyperlane_message.body, |
||||
"id": hyperlane_message.id(), |
||||
}); |
||||
let json = json!([message_json]).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(test_utils::find_vector("message.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
} |
||||
|
||||
/// Output merkle proof test vectors
|
||||
pub fn output_merkle_proof() { |
||||
let mut tree = MerkleTree::create(&[], TREE_DEPTH); |
||||
|
||||
let index = 1; |
||||
|
||||
// kludge. these are random message ids
|
||||
tree.push_leaf( |
||||
"0xd89959d277019eee21f1c3c270a125964d63b71876880724d287fbb8b8de55f1" |
||||
.parse() |
||||
.unwrap(), |
||||
TREE_DEPTH, |
||||
) |
||||
.unwrap(); |
||||
tree.push_leaf( |
||||
"0x5068ac60cb6f9c5202bbe8e7a1babdd972133ea3ad37d7e0e753c7e4ddd7ffbd" |
||||
.parse() |
||||
.unwrap(), |
||||
TREE_DEPTH, |
||||
) |
||||
.unwrap(); |
||||
let proof = tree.generate_proof(index, TREE_DEPTH); |
||||
|
||||
let proof_json = json!({ "leaf": proof.0, "path": proof.1, "index": index}); |
||||
let json = json!({ "proof": proof_json, "root": merkle_root_from_branch(proof.0, &proof.1, 32, index)}).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(test_utils::find_vector("proof.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
} |
||||
|
||||
/// Outputs domain hash test cases in /vector/domainHash.json
|
||||
pub fn output_domain_hashes() { |
||||
let mailbox = H256::from(H160::from_str("0x2222222222222222222222222222222222222222").unwrap()); |
||||
let test_cases: Vec<Value> = (1..=3) |
||||
.map(|i| { |
||||
json!({ |
||||
"domain": i, |
||||
"mailbox": mailbox, |
||||
"expectedDomainHash": domain_hash(mailbox, i as u32) |
||||
}) |
||||
}) |
||||
.collect(); |
||||
|
||||
let json = json!(test_cases).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(test_utils::find_vector("domainHash.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
} |
||||
|
||||
/// Outputs signed checkpoint test cases in /vector/signedCheckpoint.json
|
||||
pub fn output_signed_checkpoints() { |
||||
let mailbox = H256::from(H160::from_str("0x2222222222222222222222222222222222222222").unwrap()); |
||||
let t = async { |
||||
let signer: Signers = "1111111111111111111111111111111111111111111111111111111111111111" |
||||
.parse::<ethers::signers::LocalWallet>() |
||||
.unwrap() |
||||
.into(); |
||||
|
||||
let mut test_cases: Vec<Value> = Vec::new(); |
||||
|
||||
// test suite
|
||||
for i in 1..=3 { |
||||
let signed_checkpoint = signer |
||||
.sign(Checkpoint { |
||||
mailbox_address: mailbox, |
||||
mailbox_domain: 1000, |
||||
root: H256::repeat_byte(i + 1), |
||||
index: i as u32, |
||||
}) |
||||
.await |
||||
.expect("!sign_with"); |
||||
|
||||
test_cases.push(json!({ |
||||
"mailbox": signed_checkpoint.value.mailbox_address, |
||||
"domain": signed_checkpoint.value.mailbox_domain, |
||||
"root": signed_checkpoint.value.root, |
||||
"index": signed_checkpoint.value.index, |
||||
"signature": signed_checkpoint.signature, |
||||
"signer": signer.address(), |
||||
})) |
||||
} |
||||
|
||||
let json = json!(test_cases).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(test_utils::find_vector("signedCheckpoint.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
}; |
||||
|
||||
tokio::runtime::Builder::new_current_thread() |
||||
.enable_all() |
||||
.build() |
||||
.unwrap() |
||||
.block_on(t) |
||||
} |
@ -1,177 +0,0 @@ |
||||
/// Test functions that output json files
|
||||
#[cfg(feature = "output")] |
||||
pub mod output_functions { |
||||
|
||||
use std::{fs::OpenOptions, io::Write, str::FromStr}; |
||||
|
||||
use ethers::signers::Signer; |
||||
use hex::FromHex; |
||||
use serde_json::{json, Value}; |
||||
|
||||
use crate::{ |
||||
accumulator::{ |
||||
merkle::{merkle_root_from_branch, MerkleTree}, |
||||
TREE_DEPTH, |
||||
}, |
||||
test_utils::find_vector, |
||||
utils::domain_hash, |
||||
Checkpoint, HyperlaneMessage, H160, H256, |
||||
}; |
||||
|
||||
/// Output proof to /vector/message.json
|
||||
pub fn output_message() { |
||||
let hyperlane_message = HyperlaneMessage { |
||||
nonce: 0, |
||||
version: 0, |
||||
origin: 1000, |
||||
sender: H256::from( |
||||
H160::from_str("0x1111111111111111111111111111111111111111").unwrap(), |
||||
), |
||||
destination: 2000, |
||||
recipient: H256::from( |
||||
H160::from_str("0x2222222222222222222222222222222222222222").unwrap(), |
||||
), |
||||
body: Vec::from_hex("1234").unwrap(), |
||||
}; |
||||
|
||||
let message_json = json!({ |
||||
"nonce": hyperlane_message.nonce, |
||||
"version": hyperlane_message.version, |
||||
"origin": hyperlane_message.origin, |
||||
"sender": hyperlane_message.sender, |
||||
"destination": hyperlane_message.destination, |
||||
"recipient": hyperlane_message.recipient, |
||||
"body": hyperlane_message.body, |
||||
"id": hyperlane_message.id(), |
||||
}); |
||||
let json = json!([message_json]).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(find_vector("message.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
} |
||||
|
||||
/// Output merkle proof test vectors
|
||||
pub fn output_merkle_proof() { |
||||
let mut tree = MerkleTree::create(&[], TREE_DEPTH); |
||||
|
||||
let index = 1; |
||||
|
||||
// kludge. these are random message ids
|
||||
tree.push_leaf( |
||||
"0xd89959d277019eee21f1c3c270a125964d63b71876880724d287fbb8b8de55f1" |
||||
.parse() |
||||
.unwrap(), |
||||
TREE_DEPTH, |
||||
) |
||||
.unwrap(); |
||||
tree.push_leaf( |
||||
"0x5068ac60cb6f9c5202bbe8e7a1babdd972133ea3ad37d7e0e753c7e4ddd7ffbd" |
||||
.parse() |
||||
.unwrap(), |
||||
TREE_DEPTH, |
||||
) |
||||
.unwrap(); |
||||
let proof = tree.generate_proof(index, TREE_DEPTH); |
||||
|
||||
let proof_json = json!({ "leaf": proof.0, "path": proof.1, "index": index}); |
||||
let json = json!({ "proof": proof_json, "root": merkle_root_from_branch(proof.0, &proof.1, 32, index)}).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(find_vector("proof.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
} |
||||
|
||||
/// Outputs domain hash test cases in /vector/domainHash.json
|
||||
pub fn output_domain_hashes() { |
||||
let mailbox = |
||||
H256::from(H160::from_str("0x2222222222222222222222222222222222222222").unwrap()); |
||||
let test_cases: Vec<Value> = (1..=3) |
||||
.map(|i| { |
||||
json!({ |
||||
"domain": i, |
||||
"mailbox": mailbox, |
||||
"expectedDomainHash": domain_hash(mailbox, i as u32) |
||||
}) |
||||
}) |
||||
.collect(); |
||||
|
||||
let json = json!(test_cases).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(find_vector("domainHash.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
} |
||||
|
||||
/// Outputs signed checkpoint test cases in /vector/signedCheckpoint.json
|
||||
pub fn output_signed_checkpoints() { |
||||
let mailbox = |
||||
H256::from(H160::from_str("0x2222222222222222222222222222222222222222").unwrap()); |
||||
let t = async { |
||||
let signer: ethers::signers::LocalWallet = |
||||
"1111111111111111111111111111111111111111111111111111111111111111" |
||||
.parse() |
||||
.unwrap(); |
||||
|
||||
let mut test_cases: Vec<Value> = Vec::new(); |
||||
|
||||
// test suite
|
||||
for i in 1..=3 { |
||||
let signed_checkpoint = Checkpoint { |
||||
mailbox_address: mailbox, |
||||
mailbox_domain: 1000, |
||||
root: H256::repeat_byte(i + 1), |
||||
index: i as u32, |
||||
} |
||||
.sign_with(&signer) |
||||
.await |
||||
.expect("!sign_with"); |
||||
|
||||
test_cases.push(json!({ |
||||
"mailbox": signed_checkpoint.checkpoint.mailbox_address, |
||||
"domain": signed_checkpoint.checkpoint.mailbox_domain, |
||||
"root": signed_checkpoint.checkpoint.root, |
||||
"index": signed_checkpoint.checkpoint.index, |
||||
"signature": signed_checkpoint.signature, |
||||
"signer": signer.address(), |
||||
})) |
||||
} |
||||
|
||||
let json = json!(test_cases).to_string(); |
||||
|
||||
let mut file = OpenOptions::new() |
||||
.write(true) |
||||
.create(true) |
||||
.truncate(true) |
||||
.open(find_vector("signedCheckpoint.json")) |
||||
.expect("Failed to open/create file"); |
||||
|
||||
file.write_all(json.as_bytes()) |
||||
.expect("Failed to write to file"); |
||||
}; |
||||
|
||||
tokio::runtime::Builder::new_current_thread() |
||||
.enable_all() |
||||
.build() |
||||
.unwrap() |
||||
.block_on(t) |
||||
} |
||||
} |
@ -0,0 +1,95 @@ |
||||
use std::fmt::Debug; |
||||
|
||||
use async_trait::async_trait; |
||||
use auto_impl::auto_impl; |
||||
use ethers::prelude::{Address, Signature}; |
||||
use ethers::utils::hash_message; |
||||
use serde::{Deserialize, Serialize}; |
||||
|
||||
use crate::{HyperlaneProtocolError, H160, H256}; |
||||
|
||||
/// An error incurred by a signer
|
||||
#[derive(thiserror::Error, Debug)] |
||||
#[error(transparent)] |
||||
pub struct HyperlaneSignerError(#[from] Box<dyn std::error::Error + Send + Sync>); |
||||
|
||||
/// A hyperlane signer for use by the validators. Currently signers will always
|
||||
/// use ethereum wallets.
|
||||
#[async_trait] |
||||
#[auto_impl(&, Box, Arc)] |
||||
pub trait HyperlaneSigner: Send + Sync + Debug { |
||||
/// The signer's address
|
||||
fn eth_address(&self) -> H160; |
||||
|
||||
/// Sign a hyperlane checkpoint hash. This must be a signature without eip
|
||||
/// 155.
|
||||
async fn sign_hash(&self, hash: &H256) -> Result<Signature, HyperlaneSignerError>; |
||||
} |
||||
|
||||
/// Auto-implemented extension trait for HyperlaneSigner.
|
||||
#[async_trait] |
||||
pub trait HyperlaneSignerExt { |
||||
/// Sign a `Signable` value
|
||||
async fn sign<T: Signable + Send>( |
||||
&self, |
||||
value: T, |
||||
) -> Result<SignedType<T>, HyperlaneSignerError>; |
||||
|
||||
/// Check whether a message was signed by a specific address.
|
||||
fn verify<T: Signable>(&self, signed: &SignedType<T>) -> Result<(), HyperlaneProtocolError>; |
||||
} |
||||
|
||||
#[async_trait] |
||||
impl<S: HyperlaneSigner> HyperlaneSignerExt for S { |
||||
async fn sign<T: Signable + Send>( |
||||
&self, |
||||
value: T, |
||||
) -> Result<SignedType<T>, HyperlaneSignerError> { |
||||
let signing_hash = value.signing_hash(); |
||||
let signature = self.sign_hash(&signing_hash).await?; |
||||
Ok(SignedType { value, signature }) |
||||
} |
||||
|
||||
fn verify<T: Signable>(&self, signed: &SignedType<T>) -> Result<(), HyperlaneProtocolError> { |
||||
signed.verify(self.eth_address()) |
||||
} |
||||
} |
||||
|
||||
/// A type that can be signed. The signature will be of a hash of select
|
||||
/// contents defined by `signing_hash`.
|
||||
#[async_trait] |
||||
pub trait Signable: Sized { |
||||
/// A hash of the contents.
|
||||
/// The EIP-191 compliant version of this hash is signed by validators.
|
||||
fn signing_hash(&self) -> H256; |
||||
|
||||
/// EIP-191 compliant hash of the signing hash.
|
||||
fn eth_signed_message_hash(&self) -> H256 { |
||||
hash_message(self.signing_hash()) |
||||
} |
||||
} |
||||
|
||||
/// A signed type. Contains the original value and the signature.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] |
||||
pub struct SignedType<T: Signable> { |
||||
/// The value which was signed
|
||||
pub value: T, |
||||
/// The signature for the value
|
||||
pub signature: Signature, |
||||
} |
||||
|
||||
impl<T: Signable> SignedType<T> { |
||||
/// Recover the Ethereum address of the signer
|
||||
pub fn recover(&self) -> Result<Address, HyperlaneProtocolError> { |
||||
Ok(self |
||||
.signature |
||||
.recover(self.value.eth_signed_message_hash())?) |
||||
} |
||||
|
||||
/// Check whether a message was signed by a specific address
|
||||
pub fn verify(&self, signer: Address) -> Result<(), HyperlaneProtocolError> { |
||||
Ok(self |
||||
.signature |
||||
.verify(self.value.eth_signed_message_hash(), signer)?) |
||||
} |
||||
} |
Loading…
Reference in new issue