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 {