Added VAA verification query to the Terra smart contract, refactoring

This commit is contained in:
Yuriy Savchenko 2021-02-10 15:34:30 +02:00 committed by Leopold Schabel
parent f5560eb3ff
commit 10c2fa4eea
4 changed files with 181 additions and 79 deletions

View File

@ -24,6 +24,7 @@ cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
thiserror = { version = "1.0.20" }
k256 = { version = "0.5.9", default-features = false, features = ["ecdsa"] }
sha3 = { version = "0.9.1", default-features = false }
generic-array = { version = "0.14.4" }
hex = "0.4.2"
lazy_static = "1.4.0"

View File

@ -11,7 +11,7 @@ use crate::msg::{GuardianSetInfoResponse, HandleMsg, InitMsg, QueryMsg};
use crate::state::{
config, config_read, guardian_set_get, guardian_set_set, vaa_archive_add, vaa_archive_check,
wrapped_asset, wrapped_asset_address, wrapped_asset_address_read, wrapped_asset_read,
ConfigInfo, GuardianAddress, GuardianSetInfo,
ConfigInfo, GuardianAddress, GuardianSetInfo, ParsedVAA,
};
use cw20_base::msg::HandleMsg as TokenMsg;
@ -31,6 +31,8 @@ use k256::ecdsa::VerifyKey;
use k256::EncodedPoint;
use sha3::{Digest, Keccak256};
use generic_array::GenericArray;
use std::convert::TryFrom;
// Chain ID of Terra
@ -101,75 +103,62 @@ fn handle_submit_vaa<S: Storage, A: Api, Q: Querier>(
return ContractError::ContractInactive.std_err();
}
/* VAA format:
let vaa = parse_and_verify_vaa(&deps.storage, data, env.block.time)?;
header (length 6):
0 uint8 version (0x01)
1 uint32 guardian set index
5 uint8 len signatures
let result = match vaa.action {
0x01 => {
if vaa.guardian_set_index != state.guardian_set_index {
return ContractError::NotCurrentGuardianSet.std_err();
}
vaa_update_guardian_set(deps, env, vaa.payload.as_slice())
}
0x10 => vaa_transfer(deps, env, vaa.payload.as_slice()),
_ => ContractError::InvalidVAAAction.std_err(),
};
per signature (length 66):
0 uint8 index of the signer (in guardian keys)
1 [65]uint8 signature
if result.is_ok() {
vaa_archive_add(&mut deps.storage, vaa.hash.as_slice())?;
}
body:
0 uint32 unix seconds
4 uint8 action
5 [payload_size]uint8 payload */
result
}
const HEADER_LEN: usize = 6;
const SIGNATURE_LEN: usize = 66;
/// Parses raw VAA data into a struct and verifies whether it contains sufficient signatures of an
/// active guardian set i.e. is valid according to Wormhole consensus rules
fn parse_and_verify_vaa<S: Storage>(
storage: &S,
data: &[u8],
block_time: u64,
) -> StdResult<ParsedVAA> {
let vaa = ParsedVAA::deserialize(data)?;
const GUARDIAN_SET_INDEX_POS: usize = 1;
const LEN_SIGNER_POS: usize = 5;
let version = data.get_u8(0);
if version != 1 {
if vaa.version != 1 {
return ContractError::InvalidVersion.std_err();
}
// Load 4 bytes starting from index 1
let vaa_guardian_set_index: u32 = data.get_u32(GUARDIAN_SET_INDEX_POS);
let len_signers = data.get_u8(LEN_SIGNER_POS) as usize;
let body_offset: usize = HEADER_LEN + SIGNATURE_LEN * len_signers as usize;
// Hash the body
if body_offset >= data.len() {
return ContractError::InvalidVAA.std_err();
}
let body = &data[body_offset..];
let mut hasher = Keccak256::new();
hasher.update(body);
let hash = hasher.finalize();
// Check if VAA with this hash was already accepted
if vaa_archive_check(&deps.storage, &hash) {
if vaa_archive_check(storage, vaa.hash.as_slice()) {
return ContractError::VaaAlreadyExecuted.std_err();
}
// Load and check guardian set
let guardian_set = guardian_set_get(&deps.storage, vaa_guardian_set_index);
let guardian_set = guardian_set_get(storage, vaa.guardian_set_index);
let guardian_set: GuardianSetInfo =
guardian_set.or_else(|_| ContractError::InvalidGuardianSetIndex.std_err())?;
if guardian_set.expiration_time != 0 && guardian_set.expiration_time < env.block.time {
if guardian_set.expiration_time != 0 && guardian_set.expiration_time < block_time {
return ContractError::GuardianSetExpired.std_err();
}
if len_signers < guardian_set.quorum() {
if vaa.len_signers < guardian_set.quorum() {
return ContractError::NoQuorum.std_err();
}
// Verify guardian signatures
let mut last_index: i32 = -1;
let mut pos = HEADER_LEN;
let mut pos = ParsedVAA::HEADER_LEN;
// Signature data offsets in the signature block
const SIG_DATA_POS: usize = 1;
const SIG_DATA_LEN: usize = 64; // Signature length minus recovery id at the end
const SIG_RECOVERY_POS: usize = SIG_DATA_POS + SIG_DATA_LEN; // Recovery byte is last affter the main signature
for _ in 0..len_signers {
if pos + SIGNATURE_LEN > data.len() {
for _ in 0..vaa.len_signers {
if pos + ParsedVAA::SIGNATURE_LEN > data.len() {
return ContractError::InvalidVAA.std_err();
}
let index = data.get_u8(pos) as i32;
@ -178,16 +167,18 @@ fn handle_submit_vaa<S: Storage, A: Api, Q: Querier>(
}
last_index = index;
let signature =
Signature::try_from(&data[pos + SIG_DATA_POS..pos + SIG_DATA_POS + SIG_DATA_LEN])
let signature = Signature::try_from(
&data[pos + ParsedVAA::SIG_DATA_POS
..pos + ParsedVAA::SIG_DATA_POS + ParsedVAA::SIG_DATA_LEN],
)
.or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
let id = RecoverableId::new(data.get_u8(pos + SIG_RECOVERY_POS))
let id = RecoverableId::new(data.get_u8(pos + ParsedVAA::SIG_RECOVERY_POS))
.or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
let recoverable_signature = RecoverableSignature::new(&signature, id)
.or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
let verify_key = recoverable_signature
.recover_verify_key_from_digest_bytes(&hash)
.recover_verify_key_from_digest_bytes(GenericArray::from_slice(vaa.hash.as_slice()))
.or_else(|_| ContractError::CannotRecoverKey.std_err())?;
let index = index as usize;
@ -197,34 +188,10 @@ fn handle_submit_vaa<S: Storage, A: Api, Q: Querier>(
if !keys_equal(&verify_key, &guardian_set.addresses[index]) {
return ContractError::GuardianSignatureError.std_err();
}
pos += SIGNATURE_LEN;
pos += ParsedVAA::SIGNATURE_LEN;
}
const VAA_ACTION_POS: usize = 4;
const VAA_PAYLOAD_POS: usize = 5;
// Signatures valid, apply VAA
if body_offset + VAA_PAYLOAD_POS > data.len() {
return ContractError::InvalidVAA.std_err();
}
let action = data.get_u8(body_offset + VAA_ACTION_POS);
let payload = &data[body_offset + VAA_PAYLOAD_POS..];
let result = match action {
0x01 => {
if vaa_guardian_set_index != state.guardian_set_index {
return ContractError::NotCurrentGuardianSet.std_err();
}
vaa_update_guardian_set(deps, env, payload)
}
0x10 => vaa_transfer(deps, env, payload),
_ => ContractError::InvalidVAAAction.std_err(),
};
if result.is_ok() {
vaa_archive_add(&mut deps.storage, &hash)?;
}
result
Ok(vaa)
}
/// Handle wrapped asset registration messages
@ -296,7 +263,6 @@ fn vaa_update_guardian_set<S: Storage, A: Api, Q: Querier>(
};
let mut pos = ADDRESS_POS;
for _ in 0..len {
if pos + ADDRESS_LEN > data.len() {
return ContractError::InvalidVAA.std_err();
}
@ -578,6 +544,7 @@ pub fn handle_set_active<S: Storage, A: Api, Q: Querier>(
pub fn query<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
env: Env,
msg: QueryMsg,
) -> StdResult<Binary> {
match msg {
@ -585,6 +552,9 @@ pub fn query<S: Storage, A: Api, Q: Querier>(
QueryMsg::WrappedRegistry { chain, address } => {
to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?)
}
QueryMsg::VerifyVAA { vaa } => {
to_binary(&query_parse_and_verify_vaa(deps, env, &vaa.as_slice())?)
}
}
}
@ -613,6 +583,14 @@ pub fn query_wrapped_registry<S: Storage, A: Api, Q: Querier>(
}
}
pub fn query_parse_and_verify_vaa<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
env: Env,
data: &[u8],
) -> StdResult<ParsedVAA> {
parse_and_verify_vaa(&deps.storage, data, env.block.time)
}
fn keys_equal(a: &VerifyKey, b: &GuardianAddress) -> bool {
let mut hasher = Keccak256::new();
@ -1255,8 +1233,9 @@ mod tests {
fn valid_query_guardian_set() {
let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]);
do_init_with_guardians(&mut deps, 3);
let env = mock_env(&HumanAddr::from(CREATOR_ADDR), &[]);
let result = query(&deps, QueryMsg::GuardianSetInfo {}).unwrap();
let result = query(&deps, env, QueryMsg::GuardianSetInfo {}).unwrap();
let result: GuardianSetInfoResponse = serde_json::from_slice(result.as_slice()).unwrap();
assert_eq!(
@ -1277,4 +1256,41 @@ mod tests {
}
)
}
#[test]
fn valid_query_verify_vaa() {
let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]);
do_init_with_guardians(&mut deps, 4);
let env = mock_env(&HumanAddr::from(SENDER_ADDR), &[]);
let decoded_vaa: Binary = hex::decode(VAA_VALID_TRANSFER_3_SIGS)
.expect("Decoding failed")
.into();
let result = query(&deps, env, QueryMsg::VerifyVAA { vaa: decoded_vaa });
assert!(result.is_ok());
}
#[test]
fn error_query_verify_vaa() {
let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]);
do_init(
&mut deps,
// Use 1-2-4 guardians
&vec![
GuardianAddress::from(ADDR_1),
GuardianAddress::from(ADDR_2),
GuardianAddress::from(ADDR_4),
],
unix_timestamp(),
);
let env = mock_env(&HumanAddr::from(SENDER_ADDR), &[]);
// Sign by 1-2-3 guardians
let decoded_vaa: Binary = hex::decode(VAA_VALID_TRANSFER_3_SIGS)
.expect("Decoding failed")
.into();
let result = query(&deps, env, QueryMsg::VerifyVAA { vaa: decoded_vaa });
assert_eq!(result, ContractError::GuardianSignatureError.std_err());
}
}

View File

@ -37,6 +37,7 @@ pub enum HandleMsg {
pub enum QueryMsg {
GuardianSetInfo {},
WrappedRegistry { chain: u8, address: Binary },
VerifyVAA { vaa: Binary },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]

View File

@ -7,6 +7,11 @@ use cosmwasm_storage::{
Singleton,
};
use crate::byte_utils::ByteUtils;
use crate::error::ContractError;
use sha3::{Digest, Keccak256};
pub static CONFIG_KEY: &[u8] = b"config";
pub static GUARDIAN_SET_KEY: &[u8] = b"guardian_set";
pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset";
@ -31,6 +36,85 @@ pub struct ConfigInfo {
pub is_active: bool,
}
// Validator Action Approval(VAA) data
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ParsedVAA {
pub version: u8,
pub guardian_set_index: u32,
pub len_signers: usize,
pub hash: Vec<u8>,
pub action: u8,
pub payload: Vec<u8>,
}
impl ParsedVAA {
/* VAA format:
header (length 6):
0 uint8 version (0x01)
1 uint32 guardian set index
5 uint8 len signatures
per signature (length 66):
0 uint8 index of the signer (in guardian keys)
1 [65]uint8 signature
body:
0 uint32 unix seconds
4 uint8 action
5 [payload_size]uint8 payload */
pub const HEADER_LEN: usize = 6;
pub const SIGNATURE_LEN: usize = 66;
pub const GUARDIAN_SET_INDEX_POS: usize = 1;
pub const LEN_SIGNER_POS: usize = 5;
pub const VAA_ACTION_POS: usize = 4;
pub const VAA_PAYLOAD_POS: usize = 5;
// Signature data offsets in the signature block
pub const SIG_DATA_POS: usize = 1;
// Signature length minus recovery id at the end
pub const SIG_DATA_LEN: usize = 64;
// Recovery byte is last after the main signature
pub const SIG_RECOVERY_POS: usize = Self::SIG_DATA_POS + Self::SIG_DATA_LEN;
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
let version = data.get_u8(0);
// Load 4 bytes starting from index 1
let guardian_set_index: u32 = data.get_u32(Self::GUARDIAN_SET_INDEX_POS);
let len_signers = data.get_u8(Self::LEN_SIGNER_POS) as usize;
let body_offset: usize = Self::HEADER_LEN + Self::SIGNATURE_LEN * len_signers as usize;
// Hash the body
if body_offset >= data.len() {
return ContractError::InvalidVAA.std_err();
}
let body = &data[body_offset..];
let mut hasher = Keccak256::new();
hasher.update(body);
let hash = hasher.finalize().to_vec();
// Signatures valid, apply VAA
if body_offset + Self::VAA_PAYLOAD_POS > data.len() {
return ContractError::InvalidVAA.std_err();
}
let action = data.get_u8(body_offset + Self::VAA_ACTION_POS);
let payload = &data[body_offset + Self::VAA_PAYLOAD_POS..];
Ok(ParsedVAA {
version,
guardian_set_index,
len_signers,
hash,
action,
payload: payload.to_vec(),
})
}
}
// Guardian address
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GuardianAddress {