use core::marker::PhantomData; use std::io; use scale::Encode; use borsh::{BorshSerialize, BorshDeserialize}; use serai_db::{Get, DbTxn, create_db}; use serai_in_instructions_primitives::InInstructionWithBalance; use primitives::{ReceivedOutput, BorshG}; use crate::{lifetime::LifetimeStage, ScannerFeed, KeyFor, AddressFor, OutputFor}; // The DB macro doesn't support `BorshSerialize + BorshDeserialize` as a bound, hence this. trait Borshy: BorshSerialize + BorshDeserialize {} impl Borshy for T {} #[derive(BorshSerialize, BorshDeserialize)] struct SeraiKeyDbEntry { activation_block_number: u64, key: K, } pub(crate) struct SeraiKey { pub(crate) key: K, pub(crate) stage: LifetimeStage, pub(crate) block_at_which_reporting_starts: u64, } pub(crate) struct OutputWithInInstruction { pub(crate) output: OutputFor, pub(crate) return_address: Option>, pub(crate) in_instruction: InInstructionWithBalance, } impl OutputWithInInstruction { fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { self.output.write(writer)?; // TODO self.return_address.write(writer)?; self.in_instruction.encode_to(writer); Ok(()) } } create_db!( Scanner { BlockId: (number: u64) -> [u8; 32], BlockNumber: (id: [u8; 32]) -> u64, ActiveKeys: () -> Vec>, // The latest finalized block to appear of a blockchain LatestFinalizedBlock: () -> u64, // The next block to scan for received outputs NextToScanForOutputsBlock: () -> u64, // The next block to check for resolving eventualities NextToCheckForEventualitiesBlock: () -> u64, // The next block to potentially report NextToPotentiallyReportBlock: () -> u64, // Highest acknowledged block HighestAcknowledgedBlock: () -> u64, // If a block was notable /* A block is notable if one of three conditions are met: 1) We activated a key within this block. 2) We retired a key within this block. 3) We received outputs within this block. The first two conditions, and the reasoning for them, is extensively documented in `spec/processor/Multisig Rotation.md`. The third is obvious (as any block we receive outputs in needs synchrony so that we can spend the received outputs). We save if a block is notable here by either the scan for received outputs task or the check for eventuality completion task. Once a block has been processed by both, the reporting task will report any notable blocks. Finally, the task which sets the block safe to scan to makes its decision based on the notable blocks and the acknowledged blocks. */ // This collapses from `bool` to `()`, using if the value was set for true and false otherwise NotableBlock: (number: u64) -> (), SerializedQueuedOutputs: (block_number: u64) -> Vec, SerializedForwardedOutputsIndex: (block_number: u64) -> Vec, SerializedForwardedOutput: (output_id: &[u8]) -> Vec, SerializedOutputs: (block_number: u64) -> Vec, } ); pub(crate) struct ScannerDb(PhantomData); impl ScannerDb { pub(crate) fn set_block(txn: &mut impl DbTxn, number: u64, id: [u8; 32]) { BlockId::set(txn, number, &id); BlockNumber::set(txn, id, &number); } pub(crate) fn block_id(getter: &impl Get, number: u64) -> Option<[u8; 32]> { BlockId::get(getter, number) } pub(crate) fn block_number(getter: &impl Get, id: [u8; 32]) -> Option { BlockNumber::get(getter, id) } // activation_block_number is inclusive, so the key will be scanned for starting at the specified // block pub(crate) fn queue_key(txn: &mut impl DbTxn, activation_block_number: u64, key: KeyFor) { // Set this block as notable NotableBlock::set(txn, activation_block_number, &()); // Push the key let mut keys: Vec>>> = ActiveKeys::get(txn).unwrap_or(vec![]); for key_i in &keys { if key == key_i.key.0 { panic!("queueing a key prior queued"); } } keys.push(SeraiKeyDbEntry { activation_block_number, key: BorshG(key) }); ActiveKeys::set(txn, &keys); } // TODO: This will be called from the Eventuality task yet this field is read by the scan task // We need to write the argument for its safety // TODO: retire_key needs to set the notable block pub(crate) fn retire_key(txn: &mut impl DbTxn, key: KeyFor) { let mut keys: Vec>>> = ActiveKeys::get(txn).expect("retiring key yet no active keys"); assert!(keys.len() > 1, "retiring our only key"); assert_eq!(keys[0].key.0, key, "not retiring the oldest key"); keys.remove(0); ActiveKeys::set(txn, &keys); } pub(crate) fn active_keys_as_of_next_to_scan_for_outputs_block( getter: &impl Get, ) -> Option>>> { // We don't take this as an argument as we don't keep all historical keys in memory // If we've scanned block 1,000,000, we can't answer the active keys as of block 0 let block_number = Self::next_to_scan_for_outputs_block(getter)?; let raw_keys: Vec>>> = ActiveKeys::get(getter)?; let mut keys = Vec::with_capacity(2); for i in 0 .. raw_keys.len() { if block_number < raw_keys[i].activation_block_number { continue; } let (stage, block_at_which_reporting_starts) = LifetimeStage::calculate_stage_and_reporting_start_block::( block_number, raw_keys[i].activation_block_number, raw_keys.get(i + 1).map(|key| key.activation_block_number), ); keys.push(SeraiKey { key: raw_keys[i].key.0, stage, block_at_which_reporting_starts }); } assert!(keys.len() <= 2); Some(keys) } pub(crate) fn set_start_block(txn: &mut impl DbTxn, start_block: u64, id: [u8; 32]) { assert!( LatestFinalizedBlock::get(txn).is_none(), "setting start block but prior set start block" ); Self::set_block(txn, start_block, id); LatestFinalizedBlock::set(txn, &start_block); NextToScanForOutputsBlock::set(txn, &start_block); // We can receive outputs in this block, but any descending transactions will be in the next // block. This, with the check on-set, creates a bound that this value in the DB is non-zero. NextToCheckForEventualitiesBlock::set(txn, &(start_block + 1)); NextToPotentiallyReportBlock::set(txn, &start_block); } pub(crate) fn set_latest_finalized_block(txn: &mut impl DbTxn, latest_finalized_block: u64) { LatestFinalizedBlock::set(txn, &latest_finalized_block); } pub(crate) fn latest_finalized_block(getter: &impl Get) -> Option { LatestFinalizedBlock::get(getter) } pub(crate) fn latest_scannable_block(getter: &impl Get) -> Option { // We can only scan up to whatever block we've checked the Eventualities of, plus the window // length. Since this returns an inclusive bound, we need to subtract 1 // See `eventuality.rs` for more info // TODO: Adjust based on register eventualities NextToCheckForEventualitiesBlock::get(getter).map(|b| b + S::WINDOW_LENGTH - 1) } pub(crate) fn set_next_to_scan_for_outputs_block( txn: &mut impl DbTxn, next_to_scan_for_outputs_block: u64, ) { NextToScanForOutputsBlock::set(txn, &next_to_scan_for_outputs_block); } pub(crate) fn next_to_scan_for_outputs_block(getter: &impl Get) -> Option { NextToScanForOutputsBlock::get(getter) } pub(crate) fn set_next_to_check_for_eventualities_block( txn: &mut impl DbTxn, next_to_check_for_eventualities_block: u64, ) { assert!( next_to_check_for_eventualities_block != 0, "next to check for eventualities block was 0 when it's bound non-zero" ); NextToCheckForEventualitiesBlock::set(txn, &next_to_check_for_eventualities_block); } pub(crate) fn next_to_check_for_eventualities_block(getter: &impl Get) -> Option { NextToCheckForEventualitiesBlock::get(getter) } pub(crate) fn set_next_to_potentially_report_block( txn: &mut impl DbTxn, next_to_potentially_report_block: u64, ) { NextToPotentiallyReportBlock::set(txn, &next_to_potentially_report_block); } pub(crate) fn next_to_potentially_report_block(getter: &impl Get) -> Option { NextToPotentiallyReportBlock::get(getter) } pub(crate) fn set_highest_acknowledged_block( txn: &mut impl DbTxn, highest_acknowledged_block: u64, ) { HighestAcknowledgedBlock::set(txn, &highest_acknowledged_block); } pub(crate) fn highest_acknowledged_block(getter: &impl Get) -> Option { HighestAcknowledgedBlock::get(getter) } pub(crate) fn take_queued_outputs( txn: &mut impl DbTxn, block_number: u64, ) -> Vec> { todo!("TODO") } pub(crate) fn queue_return( txn: &mut impl DbTxn, block_queued_from: u64, return_addr: AddressFor, output: OutputFor, ) { todo!("TODO") } pub(crate) fn queue_output_until_block( txn: &mut impl DbTxn, queue_for_block: u64, output: &OutputWithInInstruction, ) { let mut outputs = SerializedQueuedOutputs::get(txn, queue_for_block).unwrap_or(Vec::with_capacity(128)); output.write(&mut outputs).unwrap(); SerializedQueuedOutputs::set(txn, queue_for_block, &outputs); } pub(crate) fn save_output_being_forwarded( txn: &mut impl DbTxn, block_forwarded_from: u64, output: &OutputWithInInstruction, ) { let mut buf = Vec::with_capacity(128); output.write(&mut buf).unwrap(); let id = output.output.id(); // Save this to an index so we can later fetch all outputs to forward let mut forwarded_outputs = SerializedForwardedOutputsIndex::get(txn, block_forwarded_from) .unwrap_or(Vec::with_capacity(32)); forwarded_outputs.extend(id.as_ref()); SerializedForwardedOutputsIndex::set(txn, block_forwarded_from, &forwarded_outputs); // Save the output itself SerializedForwardedOutput::set(txn, id.as_ref(), &buf); } pub(crate) fn flag_notable(txn: &mut impl DbTxn, block_number: u64) { NotableBlock::set(txn, block_number, &()); } // TODO: Use a DbChannel here, and send the instructions to the report task and the outputs to // the eventuality task? That way this cleans up after itself pub(crate) fn set_in_instructions( txn: &mut impl DbTxn, block_number: u64, outputs: Vec>, ) { if !outputs.is_empty() { // Set this block as notable NotableBlock::set(txn, block_number, &()); } let mut buf = Vec::with_capacity(outputs.len() * 128); for output in outputs { output.write(&mut buf).unwrap(); } SerializedOutputs::set(txn, block_number, &buf); } pub(crate) fn in_instructions( getter: &impl Get, block_number: u64, ) -> Option>> { todo!("TODO") } pub(crate) fn is_block_notable(getter: &impl Get, number: u64) -> bool { NotableBlock::get(getter, number).is_some() } }