From 289d37771d19cb024b2171eb00022bd6d4810082 Mon Sep 17 00:00:00 2001 From: Chirantan Ekbote Date: Fri, 13 Jan 2023 15:52:36 +0900 Subject: [PATCH] cosmwasm: accounting: Return transfer status for observations When submitting a batch of observations, we don't want an observation for an already committed transfer to fail the entire batch. This leads to more complexity in the guardian and also delays all the legitimate observations by at least one more block (~5 seconds). Fix this by returning the transfer status of each observation as part of the response data. Observations for committed transfers will get a `TransferStatus::Committed` response without failing the tx as long as the digest of the observation matches the digest of the committed transfer. Digest mismatches are still an error and will fail the entire batch. --- .../schema/wormchain-accounting.json | 27 +-- .../wormchain-accounting/src/contract.rs | 62 ++++-- .../contracts/wormchain-accounting/src/msg.rs | 26 +++ .../tests/submit_observations.rs | 202 +++++++++++++----- .../wormchain-accounting/tests/submit_vaas.rs | 33 ++- .../packages/wormhole-bindings/src/fake.rs | 4 + 6 files changed, 242 insertions(+), 112 deletions(-) diff --git a/cosmwasm/contracts/wormchain-accounting/schema/wormchain-accounting.json b/cosmwasm/contracts/wormchain-accounting/schema/wormchain-accounting.json index 42fb12e65..4be6f5c40 100644 --- a/cosmwasm/contracts/wormchain-accounting/schema/wormchain-accounting.json +++ b/cosmwasm/contracts/wormchain-accounting/schema/wormchain-accounting.json @@ -756,14 +756,7 @@ "minimum": 0.0 }, "emitter_address": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 + "type": "string" }, "emitter_chain": { "type": "integer", @@ -1055,14 +1048,7 @@ "minimum": 0.0 }, "emitter_address": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 + "type": "string" }, "emitter_chain": { "type": "integer", @@ -1404,14 +1390,7 @@ "minimum": 0.0 }, "emitter_address": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 + "type": "string" }, "emitter_chain": { "type": "integer", diff --git a/cosmwasm/contracts/wormchain-accounting/src/contract.rs b/cosmwasm/contracts/wormchain-accounting/src/contract.rs index df732ab3a..ad5a42783 100644 --- a/cosmwasm/contracts/wormchain-accounting/src/contract.rs +++ b/cosmwasm/contracts/wormchain-accounting/src/contract.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use accounting::{ query_balance, query_modification, state::{account, transfer, Modification, TokenAddress, Transfer}, - validate_transfer, TransferError, + validate_transfer, }; use anyhow::{ensure, Context}; #[cfg(not(feature = "library"))] @@ -29,8 +29,9 @@ use crate::{ msg::{ AllAccountsResponse, AllModificationsResponse, AllPendingTransfersResponse, AllTransfersResponse, BatchTransferStatusResponse, ChainRegistrationResponse, ExecuteMsg, - MigrateMsg, MissingObservation, MissingObservationsResponse, Observation, QueryMsg, - TransferDetails, TransferStatus, Upgrade, + MigrateMsg, MissingObservation, MissingObservationsResponse, Observation, ObservationError, + ObservationStatus, QueryMsg, SubmitObservationResponse, TransferDetails, TransferStatus, + Upgrade, }, state::{Data, PendingTransfer, CHAIN_REGISTRATIONS, DIGESTS, PENDING_TRANSFERS}, }; @@ -118,20 +119,39 @@ fn submit_observations( let observations: Vec = from_binary(&observations).context("failed to parse `Observations`")?; - let events = observations - .into_iter() - .map(|o| { - let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); - handle_observation(deps.branch(), o, guardian_set_index, quorum, signature) - .with_context(|| format!("failed to handle observation for key {key}")) - }) - .filter_map(Result::transpose) - .collect::>>() - .context("failed to handle `Observation`")?; + let mut responses = Vec::with_capacity(observations.len()); + let mut events = Vec::with_capacity(observations.len()); + for o in observations { + let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); + match handle_observation(deps.branch(), o, guardian_set_index, quorum, signature) { + Ok((status, event)) => { + responses.push(SubmitObservationResponse { key, status }); + if let Some(evt) = event { + events.push(evt); + } + } + Err(e) => { + let err = ObservationError { + key, + error: format!("{e:#}"), + }; + let evt = cw_transcode::to_event(&err) + .context("failed to transcode observation error")?; + events.push(evt); + responses.push(SubmitObservationResponse { + key: err.key, + status: ObservationStatus::Error(err.error), + }); + } + } + } + + let data = to_binary(&responses).context("failed to serialize transfer details")?; Ok(Response::new() .add_attribute("action", "submit_observations") .add_attribute("owner", info.sender) + .set_data(data) .add_events(events)) } @@ -141,8 +161,10 @@ fn handle_observation( guardian_set_index: u32, quorum: usize, sig: Signature, -) -> anyhow::Result> { +) -> anyhow::Result<(ObservationStatus, Option)> { let digest_key = DIGESTS.key((o.emitter_chain, o.emitter_address.to_vec(), o.sequence)); + let tx_key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); + if let Some(saved_digest) = digest_key .may_load(deps.storage) .context("failed to load transfer digest")? @@ -152,11 +174,9 @@ fn handle_observation( bail!(ContractError::DigestMismatch); } - bail!(TransferError::DuplicateTransfer); + return Ok((ObservationStatus::Committed, None)); } - let tx_key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); - let key = PENDING_TRANSFERS.key(tx_key.clone()); let mut pending = key .may_load(deps.storage) @@ -181,7 +201,7 @@ fn handle_observation( key.save(deps.storage, &pending) .context("failed to save pending transfers")?; - return Ok(None); + return Ok((ObservationStatus::Pending, None)); } let msg = serde_wormhole::from_slice::>(&o.payload) @@ -238,9 +258,11 @@ fn handle_observation( // Now that the transfer has been committed, we don't need to keep it in the pending list. key.remove(deps.storage); - cw_transcode::to_event(&o) + let event = cw_transcode::to_event(&o) .map(Some) - .context("failed to transcode `Observation` to `Event`") + .context("failed to transcode `Observation` to `Event`")?; + + Ok((ObservationStatus::Committed, event)) } fn modify_balance( diff --git a/cosmwasm/contracts/wormchain-accounting/src/msg.rs b/cosmwasm/contracts/wormchain-accounting/src/msg.rs index 04e7b85ae..c4b849490 100644 --- a/cosmwasm/contracts/wormchain-accounting/src/msg.rs +++ b/cosmwasm/contracts/wormchain-accounting/src/msg.rs @@ -58,6 +58,32 @@ impl Observation { } } +// The default externally-tagged serde representation of enums is awkward in JSON when the +// enum contains unit variants mixed with newtype variants. We can't use the internally-tagged +// representation because it only supports newtype variants that contain structs or maps. So use +// the adjacently tagged variant representation here: the enum is always encoded as an object with +// a "type" field that indicates the variant and an optional "data" field that contains the data for +// the variant, if any. +#[cw_serde] +#[serde(tag = "type", content = "data")] +pub enum ObservationStatus { + Pending, + Committed, + Error(String), +} + +#[cw_serde] +pub struct SubmitObservationResponse { + pub key: transfer::Key, + pub status: ObservationStatus, +} + +#[cw_serde] +pub struct ObservationError { + pub key: transfer::Key, + pub error: String, +} + #[cw_serde] pub struct Upgrade { pub new_addr: [u8; 32], diff --git a/cosmwasm/contracts/wormchain-accounting/tests/submit_observations.rs b/cosmwasm/contracts/wormchain-accounting/tests/submit_observations.rs index e34dc6b71..417dbad9a 100644 --- a/cosmwasm/contracts/wormchain-accounting/tests/submit_observations.rs +++ b/cosmwasm/contracts/wormchain-accounting/tests/submit_observations.rs @@ -1,10 +1,12 @@ mod helpers; +use std::collections::BTreeMap; + use accounting::state::{account, transfer, Kind, Modification, TokenAddress}; -use cosmwasm_std::{to_binary, Binary, Event, Uint256}; +use cosmwasm_std::{from_binary, to_binary, Binary, Event, Uint256}; use cw_multi_test::AppResponse; use helpers::*; -use wormchain_accounting::msg::Observation; +use wormchain_accounting::msg::{Observation, ObservationStatus, SubmitObservationResponse}; use wormhole::{ token::Message, vaa::{Body, Header}, @@ -58,9 +60,15 @@ fn batch() { .unwrap() as usize; for (i, s) in signatures.into_iter().enumerate() { - if i < quorum { - contract.submit_observations(obs.clone(), index, s).unwrap(); + let resp = contract.submit_observations(obs.clone(), index, s).unwrap(); + let status = from_binary::>(&resp.data.unwrap()) + .unwrap() + .into_iter() + .map(|resp| (resp.key, resp.status)) + .collect::>(); + + if i < quorum { // Once there is a quorum the pending transfers are removed. if i < quorum - 1 { for o in &observations { @@ -70,6 +78,7 @@ fn batch() { assert_eq!(o, data[0].observation()); // Make sure the transfer hasn't yet been committed. + assert!(matches!(status[&key], ObservationStatus::Pending)); contract .query_transfer(key) .expect_err("transfer committed without quorum"); @@ -78,15 +87,19 @@ fn batch() { for o in &observations { let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); + assert!(matches!(status[&key], ObservationStatus::Committed)); contract .query_pending_transfer(key) .expect_err("found pending transfer for observation with quorum"); } } } else { - contract - .submit_observations(obs.clone(), index, s) - .expect_err("successfully submitted observation for committed transfer"); + // Submitting observations for committed transfers is not an error as long as the + // digests match. + for o in &observations { + let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); + assert!(matches!(status[&key], ObservationStatus::Committed)); + } } } @@ -151,15 +164,34 @@ fn duplicates() { .calculate_quorum(index, contract.app().block_info().height) .unwrap() as usize; - for (i, s) in signatures.iter().take(quorum).cloned().enumerate() { + for (i, s) in signatures.into_iter().enumerate() { contract.submit_observations(obs.clone(), index, s).unwrap(); - let err = contract - .submit_observations(obs.clone(), index, s) - .expect_err("successfully submitted duplicate observations"); + let resp = contract.submit_observations(obs.clone(), index, s).unwrap(); + let status = from_binary::>(&resp.data.unwrap()) + .unwrap() + .into_iter() + .map(|details| (details.key, details.status)) + .collect::>(); if i < quorum - 1 { - // Sadly we can't match on the exact error type in an integration test because the - // test frameworks converts it into a string before it reaches this point. - assert!(format!("{err:#}").contains("duplicate signatures")); + // Resubmitting the same signature without quorum will return an error. + for o in &observations { + let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); + if let ObservationStatus::Error(ref err) = status[&key] { + assert!(err.contains("duplicate signatures")); + } else { + panic!( + "unexpected status for duplicate signature: {:?}", + status[&key] + ); + } + } + } else { + // Submitting a signature for a committed transfer is not an error as long as the + // digests match. + for o in &observations { + let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); + assert!(matches!(status[&key], ObservationStatus::Committed)); + } } } @@ -207,12 +239,6 @@ fn duplicates() { assert_eq!(expected.amount, *dst); } - - for s in signatures { - contract - .submit_observations(obs.clone(), index, s) - .expect_err("successfully submitted observation for committed transfer"); - } } fn transfer_tokens( @@ -221,7 +247,7 @@ fn transfer_tokens( key: transfer::Key, msg: Message, index: u32, - quorum: usize, + num_signatures: usize, ) -> anyhow::Result<(Observation, Vec)> { let payload = serde_wormhole::to_vec(&msg).map(Binary::from).unwrap(); let o = Observation { @@ -240,7 +266,7 @@ fn transfer_tokens( let responses = signatures .into_iter() - .take(quorum) + .take(num_signatures) .map(|s| contract.submit_observations(obs.clone(), index, s)) .collect::>>()?; @@ -252,9 +278,7 @@ fn round_trip() { let (wh, mut contract) = proper_instantiate(); register_emitters(&wh, &mut contract, 15); let index = wh.guardian_set_index(); - let quorum = wh - .calculate_quorum(index, contract.app().block_info().height) - .unwrap() as usize; + let num_guardians = wh.num_guardians(); let emitter_chain = 2; let amount = Amount(Uint256::from(500u128).to_be_bytes()); @@ -272,7 +296,8 @@ fn round_trip() { fee: Amount([0u8; 32]), }; - let (o, _) = transfer_tokens(&wh, &mut contract, key.clone(), msg, index, quorum).unwrap(); + let (o, _) = + transfer_tokens(&wh, &mut contract, key.clone(), msg, index, num_guardians).unwrap(); let expected = transfer::Data { amount: Uint256::new(amount.0), @@ -298,7 +323,8 @@ fn round_trip() { recipient_chain: emitter_chain.into(), fee: Amount([0u8; 32]), }; - let (o, _) = transfer_tokens(&wh, &mut contract, key.clone(), msg, index, quorum).unwrap(); + let (o, _) = + transfer_tokens(&wh, &mut contract, key.clone(), msg, index, num_guardians).unwrap(); let expected = transfer::Data { amount: Uint256::new(amount.0), @@ -336,9 +362,7 @@ fn round_trip() { fn missing_guardian_set() { let (wh, mut contract) = proper_instantiate(); let index = wh.guardian_set_index(); - let quorum = wh - .calculate_quorum(index, contract.app().block_info().height) - .unwrap() as usize; + let num_guardians = wh.num_guardians(); let emitter_chain = 2; let amount = Amount(Uint256::from(500u128).to_be_bytes()); @@ -356,7 +380,7 @@ fn missing_guardian_set() { fee: Amount([0u8; 32]), }; - transfer_tokens(&wh, &mut contract, key, msg, index + 1, quorum) + transfer_tokens(&wh, &mut contract, key, msg, index + 1, num_guardians) .expect_err("successfully submitted observations with invalid guardian set"); } @@ -366,7 +390,7 @@ fn expired_guardian_set() { let index = wh.guardian_set_index(); let mut block = contract.app().block_info(); - let quorum = wh.calculate_quorum(index, block.height).unwrap() as usize; + let num_guardians = wh.num_guardians(); let emitter_chain = 2; let amount = Amount(Uint256::from(500u128).to_be_bytes()); @@ -389,7 +413,7 @@ fn expired_guardian_set() { block.height += 1; contract.app_mut().set_block(block); - transfer_tokens(&wh, &mut contract, key, msg, index, quorum) + transfer_tokens(&wh, &mut contract, key, msg, index, num_guardians) .expect_err("successfully submitted observations with expired guardian set"); } @@ -446,7 +470,9 @@ fn no_quorum() { #[test] fn missing_wrapped_account() { let (wh, mut contract) = proper_instantiate(); + register_emitters(&wh, &mut contract, 15); let index = wh.guardian_set_index(); + let num_guardians = wh.num_guardians(); let quorum = wh .calculate_quorum(index, contract.app().block_info().height) .unwrap() as usize; @@ -467,8 +493,27 @@ fn missing_wrapped_account() { fee: Amount([0u8; 32]), }; - transfer_tokens(&wh, &mut contract, key, msg, index, quorum) - .expect_err("successfully burned wrapped tokens without a wrapped amount"); + let (_, responses) = + transfer_tokens(&wh, &mut contract, key.clone(), msg, index, num_guardians).unwrap(); + for mut resp in responses.into_iter().skip(quorum - 1) { + let r = from_binary::>(&resp.data.take().unwrap()).unwrap(); + assert_eq!(key, r[0].key); + if let ObservationStatus::Error(ref err) = r[0].status { + assert!( + err.contains("cannot burn wrapped tokens without an existing wrapped account"), + "{err}" + ); + resp.assert_event( + &Event::new("wasm-ObservationError") + .add_attribute("key", serde_json_wasm::to_string(&key).unwrap()), + ); + } else { + panic!( + "unexpected response for transfer with missing wrapped account {:?}", + r[0] + ); + } + } } #[test] @@ -480,7 +525,9 @@ fn missing_native_account() { let token_chain = 2; let (wh, mut contract) = proper_instantiate(); + register_emitters(&wh, &mut contract, 15); let index = wh.guardian_set_index(); + let num_guardians = wh.num_guardians(); let quorum = wh .calculate_quorum(index, contract.app().block_info().height) .unwrap() as usize; @@ -509,8 +556,27 @@ fn missing_native_account() { fee: Amount([0u8; 32]), }; - transfer_tokens(&wh, &mut contract, key, msg, index, quorum) - .expect_err("successfully unlocked native tokens without a native account"); + let (_, responses) = + transfer_tokens(&wh, &mut contract, key.clone(), msg, index, num_guardians).unwrap(); + for mut resp in responses.into_iter().skip(quorum - 1) { + let r = from_binary::>(&resp.data.take().unwrap()).unwrap(); + assert_eq!(key, r[0].key); + if let ObservationStatus::Error(ref err) = r[0].status { + assert!( + err.contains("cannot unlock native tokens without an existing native account"), + "{err}" + ); + resp.assert_event( + &Event::new("wasm-ObservationError") + .add_attribute("key", serde_json_wasm::to_string(&key).unwrap()), + ); + } else { + panic!( + "unexpected response for transfer with missing native account {:?}", + r[0] + ); + } + } } #[test] @@ -520,9 +586,7 @@ fn repeated() { let (wh, mut contract) = proper_instantiate(); register_emitters(&wh, &mut contract, 3); let index = wh.guardian_set_index(); - let quorum = wh - .calculate_quorum(index, contract.app().block_info().height) - .unwrap() as usize; + let num_guardians = wh.num_guardians(); let emitter_chain = 2; let recipient_chain = 14; @@ -541,7 +605,15 @@ fn repeated() { for i in 0..ITERATIONS { let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), i as u64); - transfer_tokens(&wh, &mut contract, key.clone(), msg.clone(), index, quorum).unwrap(); + transfer_tokens( + &wh, + &mut contract, + key.clone(), + msg.clone(), + index, + num_guardians, + ) + .unwrap(); } let expected = Uint256::new(amount.0) * Uint256::from(ITERATIONS as u128); @@ -577,9 +649,7 @@ fn wrapped_to_wrapped() { let (wh, mut contract) = proper_instantiate(); register_emitters(&wh, &mut contract, 15); let index = wh.guardian_set_index(); - let quorum = wh - .calculate_quorum(index, contract.app().block_info().height) - .unwrap() as usize; + let num_guardians = wh.num_guardians(); // We need an initial fake wrapped account. let m = to_binary(&Modification { @@ -605,7 +675,8 @@ fn wrapped_to_wrapped() { fee: Amount([0u8; 32]), }; - let (o, _) = transfer_tokens(&wh, &mut contract, key.clone(), msg, index, quorum).unwrap(); + let (o, _) = + transfer_tokens(&wh, &mut contract, key.clone(), msg, index, num_guardians).unwrap(); let expected = transfer::Data { amount: Uint256::new(amount.0), @@ -642,6 +713,7 @@ fn wrapped_to_wrapped() { fn unknown_emitter() { let (wh, mut contract) = proper_instantiate(); let index = wh.guardian_set_index(); + let num_guardians = wh.num_guardians(); let quorum = wh .calculate_quorum(index, contract.app().block_info().height) .unwrap() as usize; @@ -662,8 +734,24 @@ fn unknown_emitter() { fee: Amount([0u8; 32]), }; - transfer_tokens(&wh, &mut contract, key, msg, index, quorum) - .expect_err("successfully transfered tokens with an invalid emitter address"); + let (_, responses) = + transfer_tokens(&wh, &mut contract, key.clone(), msg, index, num_guardians).unwrap(); + for mut resp in responses.into_iter().skip(quorum - 1) { + let r = from_binary::>(&resp.data.take().unwrap()).unwrap(); + assert_eq!(key, r[0].key); + if let ObservationStatus::Error(ref err) = r[0].status { + assert!(err.contains("no registered emitter")); + resp.assert_event( + &Event::new("wasm-ObservationError") + .add_attribute("key", serde_json_wasm::to_string(&key).unwrap()), + ); + } else { + panic!( + "unexpected response for transfer with unknown emitter address {:?}", + r[0] + ); + } + } } #[test] @@ -757,6 +845,7 @@ fn emit_event_with_quorum() { let quorum = wh .calculate_quorum(index, contract.app().block_info().height) .unwrap() as usize; + let num_guardians = wh.num_guardians(); let emitter_chain = 2; let amount = Amount(Uint256::from(500u128).to_be_bytes()); @@ -774,7 +863,8 @@ fn emit_event_with_quorum() { fee: Amount([0u8; 32]), }; - let (o, responses) = transfer_tokens(&wh, &mut contract, key, msg, index, quorum).unwrap(); + let (o, responses) = + transfer_tokens(&wh, &mut contract, key, msg, index, num_guardians).unwrap(); let expected = Event::new("wasm-Observation") .add_attribute("tx_hash", serde_json_wasm::to_string(&o.tx_hash).unwrap()) @@ -798,9 +888,9 @@ fn emit_event_with_quorum() { ) .add_attribute("payload", serde_json_wasm::to_string(&o.payload).unwrap()); - assert_eq!(responses.len(), quorum); + assert_eq!(responses.len(), num_guardians); for (i, r) in responses.into_iter().enumerate() { - if i < quorum - 1 { + if i < quorum - 1 || i >= quorum { assert!(!r.has_event(&expected)); } else { r.assert_event(&expected); @@ -814,9 +904,7 @@ fn duplicate_vaa() { register_emitters(&wh, &mut contract, 3); let index = wh.guardian_set_index(); - let quorum = wh - .calculate_quorum(index, contract.app().block_info().height) - .unwrap() as usize; + let num_guardians = wh.num_guardians(); let emitter_chain = 2; let amount = Amount(Uint256::from(500u128).to_be_bytes()); @@ -834,7 +922,7 @@ fn duplicate_vaa() { fee: Amount([0u8; 32]), }; - let (o, _) = transfer_tokens(&wh, &mut contract, key, msg, index, quorum).unwrap(); + let (o, _) = transfer_tokens(&wh, &mut contract, key, msg, index, num_guardians).unwrap(); // Now try to submit a VAA for this transfer. This should fail since the transfer is already // processed. @@ -871,9 +959,7 @@ fn digest_mismatch() { register_emitters(&wh, &mut contract, 3); let index = wh.guardian_set_index(); - let quorum = wh - .calculate_quorum(index, contract.app().block_info().height) - .unwrap() as usize; + let num_guardians = wh.num_guardians(); let emitter_chain = 2; let amount = Amount(Uint256::from(500u128).to_be_bytes()); @@ -891,7 +977,7 @@ fn digest_mismatch() { fee: Amount([0u8; 32]), }; - let (o, _) = transfer_tokens(&wh, &mut contract, key, msg, index, quorum).unwrap(); + let (o, _) = transfer_tokens(&wh, &mut contract, key, msg, index, num_guardians).unwrap(); // Now try submitting a VAA with the same (chain, address, sequence) tuple but with // different details. diff --git a/cosmwasm/contracts/wormchain-accounting/tests/submit_vaas.rs b/cosmwasm/contracts/wormchain-accounting/tests/submit_vaas.rs index a3b95b94d..5af18a9be 100644 --- a/cosmwasm/contracts/wormchain-accounting/tests/submit_vaas.rs +++ b/cosmwasm/contracts/wormchain-accounting/tests/submit_vaas.rs @@ -1,10 +1,10 @@ mod helpers; use accounting::state::{transfer, TokenAddress}; -use cosmwasm_std::{to_binary, Binary, Event, Uint256}; +use cosmwasm_std::{from_binary, to_binary, Binary, Event, Uint256}; use helpers::*; use serde_wormhole::RawMessage; -use wormchain_accounting::msg::Observation; +use wormchain_accounting::msg::{Observation, ObservationStatus, SubmitObservationResponse}; use wormhole::{ token::Message, vaa::{Body, Header, Vaa}, @@ -288,15 +288,20 @@ fn reobservation() { consistency_level: v.consistency_level, payload: serde_wormhole::to_vec(&v.payload).unwrap().into(), }; + let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); let obs = to_binary(&vec![o]).unwrap(); let index = wh.guardian_set_index(); let signatures = wh.sign(&obs); for s in signatures { - let err = contract - .submit_observations(obs.clone(), index, s) - .expect_err("successfully submitted observation for processed VAA"); - assert!(format!("{err:#}").contains("transfer already committed")); + let resp = contract.submit_observations(obs.clone(), index, s).unwrap(); + let mut responses: Vec = + from_binary(&resp.data.unwrap()).unwrap(); + + assert_eq!(1, responses.len()); + let d = responses.remove(0); + assert_eq!(key, d.key); + assert!(matches!(d.status, ObservationStatus::Committed)); } } @@ -322,13 +327,21 @@ fn digest_mismatch() { payload: serde_wormhole::to_vec(&v.payload).unwrap().into(), }; + let key = transfer::Key::new(o.emitter_chain, o.emitter_address.into(), o.sequence); let obs = to_binary(&vec![o]).unwrap(); let index = wh.guardian_set_index(); let signatures = wh.sign(&obs); for s in signatures { - let err = contract - .submit_observations(obs.clone(), index, s) - .expect_err("successfully submitted different observation for processed VAA"); - assert!(format!("{err:#}").contains("digest mismatch")); + let resp = contract.submit_observations(obs.clone(), index, s).unwrap(); + let responses = from_binary::>(&resp.data.unwrap()).unwrap(); + assert_eq!(key, responses[0].key); + if let ObservationStatus::Error(ref err) = responses[0].status { + assert!(err.contains("digest mismatch")); + } else { + panic!( + "unexpected status for observation with mismatched digest: {:?}", + responses[0].status + ); + } } } diff --git a/cosmwasm/packages/wormhole-bindings/src/fake.rs b/cosmwasm/packages/wormhole-bindings/src/fake.rs index 3fa929c3d..f925716a4 100644 --- a/cosmwasm/packages/wormhole-bindings/src/fake.rs +++ b/cosmwasm/packages/wormhole-bindings/src/fake.rs @@ -183,6 +183,10 @@ impl WormholeKeeper { pub fn set_index(&self, index: u32) { self.0.borrow_mut().index = index; } + + pub fn num_guardians(&self) -> usize { + self.0.borrow().guardians.len() + } } impl Default for WormholeKeeper {