mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Have the processor verify the published Batches match expectations
This commit is contained in:
@@ -280,7 +280,13 @@ pub async fn main_loop<
|
||||
// Substrate sets this limit to prevent DoSs from malicious validator sets
|
||||
// That bound lets us consume this txn in the following loop body, as an optimization
|
||||
assert!(batches.len() <= 1);
|
||||
for messages::substrate::ExecutedBatch { id, in_instructions } in batches {
|
||||
for messages::substrate::ExecutedBatch {
|
||||
id,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
} in batches
|
||||
{
|
||||
let key_to_activate =
|
||||
KeyToActivate::<KeyFor<S>>::try_recv(txn.as_mut().unwrap()).map(|key| key.0);
|
||||
|
||||
@@ -288,7 +294,9 @@ pub async fn main_loop<
|
||||
let _: () = scanner.acknowledge_batch(
|
||||
txn.take().unwrap(),
|
||||
id,
|
||||
in_instructions,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
/*
|
||||
`acknowledge_batch` takes burns to optimize handling returns with standard
|
||||
payments. That's why handling these with a Batch (and not waiting until the
|
||||
|
||||
@@ -24,6 +24,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals
|
||||
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
||||
|
||||
# Cryptography
|
||||
blake2 = { version = "0.10", default-features = false, features = ["std"] }
|
||||
group = { version = "0.13", default-features = false }
|
||||
|
||||
# Application
|
||||
@@ -35,6 +36,7 @@ serai-db = { path = "../../common/db" }
|
||||
messages = { package = "serai-processor-messages", path = "../messages" }
|
||||
|
||||
serai-primitives = { path = "../../substrate/primitives", default-features = false, features = ["std"] }
|
||||
serai-validator-sets-primitives = { path = "../../substrate/validator-sets/primitives", default-features = false, features = ["std", "borsh"] }
|
||||
serai-in-instructions-primitives = { path = "../../substrate/in-instructions/primitives", default-features = false, features = ["std", "borsh"] }
|
||||
serai-coins-primitives = { path = "../../substrate/coins/primitives", default-features = false, features = ["std", "borsh"] }
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ use scale::{Encode, Decode, IoReader};
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
use serai_db::{Get, DbTxn, create_db, db_channel};
|
||||
|
||||
use serai_in_instructions_primitives::{InInstructionWithBalance, Batch};
|
||||
use serai_coins_primitives::OutInstructionWithBalance;
|
||||
use serai_validator_sets_primitives::Session;
|
||||
use serai_in_instructions_primitives::{InInstructionWithBalance, Batch};
|
||||
|
||||
use primitives::{EncodableG, ReceivedOutput};
|
||||
|
||||
@@ -25,11 +26,13 @@ impl<T: BorshSerialize + BorshDeserialize> Borshy for T {}
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
struct SeraiKeyDbEntry<K: Borshy> {
|
||||
activation_block_number: u64,
|
||||
session: Session,
|
||||
key: K,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SeraiKey<K> {
|
||||
pub(crate) session: Session,
|
||||
pub(crate) key: K,
|
||||
pub(crate) stage: LifetimeStage,
|
||||
pub(crate) activation_block_number: u64,
|
||||
@@ -165,7 +168,7 @@ impl<S: ScannerFeed> ScannerGlobalDb<S> {
|
||||
|
||||
// If this new key retires a key, mark the block at which forwarding explicitly occurs notable
|
||||
// This lets us obtain synchrony over the transactions we'll make to accomplish this
|
||||
if let Some(key_retired_by_this) = keys.last() {
|
||||
let this_keys_session = if let Some(key_retired_by_this) = keys.last() {
|
||||
NotableBlock::set(
|
||||
txn,
|
||||
Lifetime::calculate::<S>(
|
||||
@@ -182,10 +185,17 @@ impl<S: ScannerFeed> ScannerGlobalDb<S> {
|
||||
),
|
||||
&(),
|
||||
);
|
||||
}
|
||||
Session(key_retired_by_this.session.0 + 1)
|
||||
} else {
|
||||
Session(0)
|
||||
};
|
||||
|
||||
// Push and save the next key
|
||||
keys.push(SeraiKeyDbEntry { activation_block_number, key: EncodableG(key) });
|
||||
keys.push(SeraiKeyDbEntry {
|
||||
activation_block_number,
|
||||
session: this_keys_session,
|
||||
key: EncodableG(key),
|
||||
});
|
||||
ActiveKeys::set(txn, &keys);
|
||||
|
||||
// Now tidy the keys, ensuring this has a maximum length of 2
|
||||
@@ -236,6 +246,7 @@ impl<S: ScannerFeed> ScannerGlobalDb<S> {
|
||||
raw_keys.get(i + 1).map(|key| key.activation_block_number),
|
||||
);
|
||||
keys.push(SeraiKey {
|
||||
session: raw_keys[i].session,
|
||||
key: raw_keys[i].key.0,
|
||||
stage,
|
||||
activation_block_number: raw_keys[i].activation_block_number,
|
||||
@@ -477,6 +488,7 @@ db_channel! {
|
||||
}
|
||||
|
||||
pub(crate) struct InInstructionData<S: ScannerFeed> {
|
||||
pub(crate) session_to_sign_batch: Session,
|
||||
pub(crate) external_key_for_session_to_sign_batch: KeyFor<S>,
|
||||
pub(crate) returnable_in_instructions: Vec<Returnable<S>>,
|
||||
}
|
||||
@@ -488,7 +500,8 @@ impl<S: ScannerFeed> ScanToReportDb<S> {
|
||||
block_number: u64,
|
||||
data: &InInstructionData<S>,
|
||||
) {
|
||||
let mut buf = data.external_key_for_session_to_sign_batch.to_bytes().as_ref().to_vec();
|
||||
let mut buf = data.session_to_sign_batch.encode();
|
||||
buf.extend(data.external_key_for_session_to_sign_batch.to_bytes().as_ref());
|
||||
for returnable_in_instruction in &data.returnable_in_instructions {
|
||||
returnable_in_instruction.write(&mut buf).unwrap();
|
||||
}
|
||||
@@ -510,6 +523,7 @@ impl<S: ScannerFeed> ScanToReportDb<S> {
|
||||
);
|
||||
let mut buf = data.returnable_in_instructions.as_slice();
|
||||
|
||||
let session_to_sign_batch = Session::decode(&mut buf).unwrap();
|
||||
let external_key_for_session_to_sign_batch = {
|
||||
let mut external_key_for_session_to_sign_batch =
|
||||
<KeyFor<S> as GroupEncoding>::Repr::default();
|
||||
@@ -523,7 +537,11 @@ impl<S: ScannerFeed> ScanToReportDb<S> {
|
||||
while !buf.is_empty() {
|
||||
returnable_in_instructions.push(Returnable::read(&mut buf).unwrap());
|
||||
}
|
||||
InInstructionData { external_key_for_session_to_sign_batch, returnable_in_instructions }
|
||||
InInstructionData {
|
||||
session_to_sign_batch,
|
||||
external_key_for_session_to_sign_batch,
|
||||
returnable_in_instructions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
||||
use serai_db::{Get, DbTxn, Db};
|
||||
|
||||
use serai_primitives::{NetworkId, Coin, Amount};
|
||||
use serai_validator_sets_primitives::Session;
|
||||
use serai_coins_primitives::OutInstructionWithBalance;
|
||||
|
||||
use primitives::{task::*, Address, ReceivedOutput, Block, Payment};
|
||||
@@ -437,10 +438,13 @@ impl<S: ScannerFeed> Scanner<S> {
|
||||
/// `queue_burns`. Doing so will cause them to be executed multiple times.
|
||||
///
|
||||
/// The calls to this function must be ordered with regards to `queue_burns`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn acknowledge_batch(
|
||||
&mut self,
|
||||
mut txn: impl DbTxn,
|
||||
batch_id: u32,
|
||||
publisher: Session,
|
||||
in_instructions_hash: [u8; 32],
|
||||
in_instruction_results: Vec<messages::substrate::InInstructionResult>,
|
||||
burns: Vec<OutInstructionWithBalance>,
|
||||
key_to_activate: Option<KeyFor<S>>,
|
||||
@@ -451,6 +455,8 @@ impl<S: ScannerFeed> Scanner<S> {
|
||||
substrate::queue_acknowledge_batch::<S>(
|
||||
&mut txn,
|
||||
batch_id,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
burns,
|
||||
key_to_activate,
|
||||
|
||||
@@ -8,9 +8,17 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
||||
use serai_db::{Get, DbTxn, create_db};
|
||||
|
||||
use serai_primitives::Balance;
|
||||
use serai_validator_sets_primitives::Session;
|
||||
|
||||
use crate::{ScannerFeed, KeyFor, AddressFor};
|
||||
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
pub(crate) struct BatchInfo {
|
||||
pub(crate) block_number: u64,
|
||||
pub(crate) publisher: Session,
|
||||
pub(crate) in_instructions_hash: [u8; 32],
|
||||
}
|
||||
|
||||
create_db!(
|
||||
ScannerReport {
|
||||
// The next block to potentially report
|
||||
@@ -18,10 +26,11 @@ create_db!(
|
||||
// The next Batch ID to use
|
||||
NextBatchId: () -> u32,
|
||||
|
||||
// The block number which caused a batch
|
||||
BlockNumberForBatch: (batch: u32) -> u64,
|
||||
// The information needed to verify a batch
|
||||
InfoForBatch: (batch: u32) -> BatchInfo,
|
||||
|
||||
// The external key for the session which should sign a batch
|
||||
// TODO: Merge this with InfoForBatch
|
||||
ExternalKeyForSessionToSignBatch: (batch: u32) -> Vec<u8>,
|
||||
|
||||
// The return addresses for the InInstructions within a Batch
|
||||
@@ -46,15 +55,24 @@ impl<S: ScannerFeed> ReportDb<S> {
|
||||
NextToPotentiallyReportBlock::get(getter)
|
||||
}
|
||||
|
||||
pub(crate) fn acquire_batch_id(txn: &mut impl DbTxn, block_number: u64) -> u32 {
|
||||
pub(crate) fn acquire_batch_id(txn: &mut impl DbTxn) -> u32 {
|
||||
let id = NextBatchId::get(txn).unwrap_or(0);
|
||||
NextBatchId::set(txn, &(id + 1));
|
||||
BlockNumberForBatch::set(txn, id, &block_number);
|
||||
id
|
||||
}
|
||||
|
||||
pub(crate) fn take_block_number_for_batch(txn: &mut impl DbTxn, id: u32) -> Option<u64> {
|
||||
BlockNumberForBatch::take(txn, id)
|
||||
pub(crate) fn save_batch_info(
|
||||
txn: &mut impl DbTxn,
|
||||
id: u32,
|
||||
block_number: u64,
|
||||
publisher: Session,
|
||||
in_instructions_hash: [u8; 32],
|
||||
) {
|
||||
InfoForBatch::set(txn, id, &BatchInfo { block_number, publisher, in_instructions_hash });
|
||||
}
|
||||
|
||||
pub(crate) fn take_info_for_batch(txn: &mut impl DbTxn, id: u32) -> Option<BatchInfo> {
|
||||
InfoForBatch::take(txn, id)
|
||||
}
|
||||
|
||||
pub(crate) fn save_external_key_for_session_to_sign_batch(
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
use core::{marker::PhantomData, future::Future};
|
||||
|
||||
use blake2::{digest::typenum::U32, Digest, Blake2b};
|
||||
|
||||
use scale::Encode;
|
||||
use serai_db::{DbTxn, Db};
|
||||
|
||||
use serai_primitives::BlockHash;
|
||||
use serai_in_instructions_primitives::{MAX_BATCH_SIZE, Batch};
|
||||
|
||||
use primitives::task::ContinuallyRan;
|
||||
use crate::{
|
||||
db::{Returnable, ScannerGlobalDb, InInstructionData, ScanToReportDb, Batches, BatchesToSign},
|
||||
index,
|
||||
scan::next_to_scan_for_outputs_block,
|
||||
ScannerFeed, KeyFor,
|
||||
};
|
||||
|
||||
mod db;
|
||||
pub(crate) use db::ReturnInformation;
|
||||
pub(crate) use db::{BatchInfo, ReturnInformation};
|
||||
use db::ReportDb;
|
||||
|
||||
pub(crate) fn take_block_number_for_batch<S: ScannerFeed>(
|
||||
pub(crate) fn take_info_for_batch<S: ScannerFeed>(
|
||||
txn: &mut impl DbTxn,
|
||||
id: u32,
|
||||
) -> Option<u64> {
|
||||
ReportDb::<S>::take_block_number_for_batch(txn, id)
|
||||
) -> Option<BatchInfo> {
|
||||
ReportDb::<S>::take_info_for_batch(txn, id)
|
||||
}
|
||||
|
||||
pub(crate) fn take_external_key_for_session_to_sign_batch<S: ScannerFeed>(
|
||||
@@ -88,33 +88,28 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for ReportTask<D, S> {
|
||||
let next_to_potentially_report = ReportDb::<S>::next_to_potentially_report_block(&self.db)
|
||||
.expect("ReportTask run before writing the start block");
|
||||
|
||||
for b in next_to_potentially_report ..= highest_reportable {
|
||||
for block_number in next_to_potentially_report ..= highest_reportable {
|
||||
let mut txn = self.db.txn();
|
||||
|
||||
// Receive the InInstructions for this block
|
||||
// We always do this as we can't trivially tell if we should recv InInstructions before we
|
||||
// do
|
||||
let InInstructionData {
|
||||
session_to_sign_batch,
|
||||
external_key_for_session_to_sign_batch,
|
||||
returnable_in_instructions: in_instructions,
|
||||
} = ScanToReportDb::<S>::recv_in_instructions(&mut txn, b);
|
||||
let notable = ScannerGlobalDb::<S>::is_block_notable(&txn, b);
|
||||
} = ScanToReportDb::<S>::recv_in_instructions(&mut txn, block_number);
|
||||
let notable = ScannerGlobalDb::<S>::is_block_notable(&txn, block_number);
|
||||
if !notable {
|
||||
assert!(in_instructions.is_empty(), "block wasn't notable yet had InInstructions");
|
||||
}
|
||||
// If this block is notable, create the Batch(s) for it
|
||||
if notable {
|
||||
let network = S::NETWORK;
|
||||
let block_hash = index::block_id(&txn, b);
|
||||
let mut batch_id = ReportDb::<S>::acquire_batch_id(&mut txn, b);
|
||||
let mut batch_id = ReportDb::<S>::acquire_batch_id(&mut txn);
|
||||
|
||||
// start with empty batch
|
||||
let mut batches = vec![Batch {
|
||||
network,
|
||||
id: batch_id,
|
||||
block: BlockHash(block_hash),
|
||||
instructions: vec![],
|
||||
}];
|
||||
let mut batches = vec![Batch { network, id: batch_id, instructions: vec![] }];
|
||||
// We also track the return information for the InInstructions within a Batch in case
|
||||
// they error
|
||||
let mut return_information = vec![vec![]];
|
||||
@@ -131,15 +126,10 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for ReportTask<D, S> {
|
||||
let in_instruction = batch.instructions.pop().unwrap();
|
||||
|
||||
// bump the id for the new batch
|
||||
batch_id = ReportDb::<S>::acquire_batch_id(&mut txn, b);
|
||||
batch_id = ReportDb::<S>::acquire_batch_id(&mut txn);
|
||||
|
||||
// make a new batch with this instruction included
|
||||
batches.push(Batch {
|
||||
network,
|
||||
id: batch_id,
|
||||
block: BlockHash(block_hash),
|
||||
instructions: vec![in_instruction],
|
||||
});
|
||||
batches.push(Batch { network, id: batch_id, instructions: vec![in_instruction] });
|
||||
// Since we're allocating a new batch, allocate a new set of return addresses for it
|
||||
return_information.push(vec![]);
|
||||
}
|
||||
@@ -152,10 +142,17 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for ReportTask<D, S> {
|
||||
.push(return_address.map(|address| ReturnInformation { address, balance }));
|
||||
}
|
||||
|
||||
// Save the return addresses to the database
|
||||
// Now that we've finalized the Batches, save the information for each to the database
|
||||
assert_eq!(batches.len(), return_information.len());
|
||||
for (batch, return_information) in batches.iter().zip(&return_information) {
|
||||
assert_eq!(batch.instructions.len(), return_information.len());
|
||||
ReportDb::<S>::save_batch_info(
|
||||
&mut txn,
|
||||
batch.id,
|
||||
block_number,
|
||||
session_to_sign_batch,
|
||||
Blake2b::<U32>::digest(batch.instructions.encode()).into(),
|
||||
);
|
||||
ReportDb::<S>::save_external_key_for_session_to_sign_batch(
|
||||
&mut txn,
|
||||
batch.id,
|
||||
@@ -171,7 +168,7 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for ReportTask<D, S> {
|
||||
}
|
||||
|
||||
// Update the next to potentially report block
|
||||
ReportDb::<S>::set_next_to_potentially_report_block(&mut txn, b + 1);
|
||||
ReportDb::<S>::set_next_to_potentially_report_block(&mut txn, block_number + 1);
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
@@ -349,6 +349,7 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for ScanTask<D, S> {
|
||||
&mut txn,
|
||||
b,
|
||||
&InInstructionData {
|
||||
session_to_sign_batch: keys[0].session,
|
||||
external_key_for_session_to_sign_batch: keys[0].key,
|
||||
returnable_in_instructions: in_instructions,
|
||||
},
|
||||
|
||||
@@ -6,12 +6,15 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
||||
use serai_db::{Get, DbTxn, create_db, db_channel};
|
||||
|
||||
use serai_coins_primitives::OutInstructionWithBalance;
|
||||
use serai_validator_sets_primitives::Session;
|
||||
|
||||
use crate::{ScannerFeed, KeyFor};
|
||||
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
struct AcknowledgeBatchEncodable {
|
||||
batch_id: u32,
|
||||
publisher: Session,
|
||||
in_instructions_hash: [u8; 32],
|
||||
in_instruction_results: Vec<messages::substrate::InInstructionResult>,
|
||||
burns: Vec<OutInstructionWithBalance>,
|
||||
key_to_activate: Option<Vec<u8>>,
|
||||
@@ -25,6 +28,8 @@ enum ActionEncodable {
|
||||
|
||||
pub(crate) struct AcknowledgeBatch<S: ScannerFeed> {
|
||||
pub(crate) batch_id: u32,
|
||||
pub(crate) publisher: Session,
|
||||
pub(crate) in_instructions_hash: [u8; 32],
|
||||
pub(crate) in_instruction_results: Vec<messages::substrate::InInstructionResult>,
|
||||
pub(crate) burns: Vec<OutInstructionWithBalance>,
|
||||
pub(crate) key_to_activate: Option<KeyFor<S>>,
|
||||
@@ -46,6 +51,8 @@ impl<S: ScannerFeed> SubstrateDb<S> {
|
||||
pub(crate) fn queue_acknowledge_batch(
|
||||
txn: &mut impl DbTxn,
|
||||
batch_id: u32,
|
||||
publisher: Session,
|
||||
in_instructions_hash: [u8; 32],
|
||||
in_instruction_results: Vec<messages::substrate::InInstructionResult>,
|
||||
burns: Vec<OutInstructionWithBalance>,
|
||||
key_to_activate: Option<KeyFor<S>>,
|
||||
@@ -54,6 +61,8 @@ impl<S: ScannerFeed> SubstrateDb<S> {
|
||||
txn,
|
||||
&ActionEncodable::AcknowledgeBatch(AcknowledgeBatchEncodable {
|
||||
batch_id,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
burns,
|
||||
key_to_activate: key_to_activate.map(|key| key.to_bytes().as_ref().to_vec()),
|
||||
@@ -69,11 +78,15 @@ impl<S: ScannerFeed> SubstrateDb<S> {
|
||||
Some(match action_encodable {
|
||||
ActionEncodable::AcknowledgeBatch(AcknowledgeBatchEncodable {
|
||||
batch_id,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
burns,
|
||||
key_to_activate,
|
||||
}) => Action::AcknowledgeBatch(AcknowledgeBatch {
|
||||
batch_id,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
burns,
|
||||
key_to_activate: key_to_activate.map(|key| {
|
||||
|
||||
@@ -3,6 +3,7 @@ use core::{marker::PhantomData, future::Future};
|
||||
use serai_db::{DbTxn, Db};
|
||||
|
||||
use serai_coins_primitives::{OutInstruction, OutInstructionWithBalance};
|
||||
use serai_validator_sets_primitives::Session;
|
||||
|
||||
use primitives::task::ContinuallyRan;
|
||||
use crate::{
|
||||
@@ -16,6 +17,8 @@ use db::*;
|
||||
pub(crate) fn queue_acknowledge_batch<S: ScannerFeed>(
|
||||
txn: &mut impl DbTxn,
|
||||
batch_id: u32,
|
||||
publisher: Session,
|
||||
in_instructions_hash: [u8; 32],
|
||||
in_instruction_results: Vec<messages::substrate::InInstructionResult>,
|
||||
burns: Vec<OutInstructionWithBalance>,
|
||||
key_to_activate: Option<KeyFor<S>>,
|
||||
@@ -23,6 +26,8 @@ pub(crate) fn queue_acknowledge_batch<S: ScannerFeed>(
|
||||
SubstrateDb::<S>::queue_acknowledge_batch(
|
||||
txn,
|
||||
batch_id,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
burns,
|
||||
key_to_activate,
|
||||
@@ -67,17 +72,31 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for SubstrateTask<D, S> {
|
||||
match action {
|
||||
Action::AcknowledgeBatch(AcknowledgeBatch {
|
||||
batch_id,
|
||||
publisher,
|
||||
in_instructions_hash,
|
||||
in_instruction_results,
|
||||
mut burns,
|
||||
key_to_activate,
|
||||
}) => {
|
||||
// Check if we have the information for this batch
|
||||
let Some(block_number) = report::take_block_number_for_batch::<S>(&mut txn, batch_id)
|
||||
let Some(report::BatchInfo {
|
||||
block_number,
|
||||
publisher: expected_publisher,
|
||||
in_instructions_hash: expected_in_instructions_hash,
|
||||
}) = report::take_info_for_batch::<S>(&mut txn, batch_id)
|
||||
else {
|
||||
// If we don't, drop this txn (restoring the action to the database)
|
||||
drop(txn);
|
||||
return Ok(made_progress);
|
||||
};
|
||||
assert_eq!(
|
||||
publisher, expected_publisher,
|
||||
"batch acknowledged on-chain was acknowledged by an unexpected publisher"
|
||||
);
|
||||
assert_eq!(
|
||||
in_instructions_hash, expected_in_instructions_hash,
|
||||
"batch acknowledged on-chain was distinct"
|
||||
);
|
||||
|
||||
{
|
||||
let external_key_for_session_to_sign_batch =
|
||||
|
||||
Reference in New Issue
Block a user