Implement key retiry

This commit is contained in:
Luke Parker 2024-08-28 18:43:40 -04:00
parent a771fbe1c6
commit 7c1025dbcb
6 changed files with 165 additions and 87 deletions

View file

@ -45,15 +45,20 @@ pub trait Id:
}
impl<const N: usize> Id for [u8; N] where [u8; N]: Default {}
/// A wrapper for a group element which implements the borsh traits.
/// A wrapper for a group element which implements the scale/borsh traits.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct BorshG<G: GroupEncoding>(pub G);
impl<G: GroupEncoding> BorshSerialize for BorshG<G> {
pub struct EncodableG<G: GroupEncoding>(pub G);
impl<G: GroupEncoding> Encode for EncodableG<G> {
fn using_encoded<R, F: FnOnce(&[u8]) -> R>(&self, f: F) -> R {
f(self.0.to_bytes().as_ref())
}
}
impl<G: GroupEncoding> BorshSerialize for EncodableG<G> {
fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> {
writer.write_all(self.0.to_bytes().as_ref())
}
}
impl<G: GroupEncoding> BorshDeserialize for BorshG<G> {
impl<G: GroupEncoding> BorshDeserialize for EncodableG<G> {
fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
let mut repr = G::Repr::default();
reader.read_exact(repr.as_mut())?;

View file

@ -7,7 +7,7 @@ use serai_db::{Get, DbTxn, create_db, db_channel};
use serai_in_instructions_primitives::InInstructionWithBalance;
use primitives::{ReceivedOutput, BorshG};
use primitives::{ReceivedOutput, EncodableG};
use crate::{lifetime::LifetimeStage, ScannerFeed, KeyFor, AddressFor, OutputFor, Return};
@ -24,6 +24,7 @@ struct SeraiKeyDbEntry<K: Borshy> {
pub(crate) struct SeraiKey<K> {
pub(crate) key: K,
pub(crate) stage: LifetimeStage,
pub(crate) activation_block_number: u64,
pub(crate) block_at_which_reporting_starts: u64,
}
@ -45,11 +46,10 @@ impl<S: ScannerFeed> OutputWithInInstruction<S> {
create_db!(
Scanner {
ActiveKeys: <K: Borshy>() -> Vec<SeraiKeyDbEntry<K>>,
RetireAt: <K: Encode>(key: K) -> 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
@ -95,29 +95,52 @@ impl<S: ScannerFeed> ScannerDb<S> {
/// 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<S>) {
// Set this block as notable
// Set the block which has a key activate as notable
NotableBlock::set(txn, activation_block_number, &());
// TODO: Panic if we've ever seen this key before
// Push the key
let mut keys: Vec<SeraiKeyDbEntry<BorshG<KeyFor<S>>>> = ActiveKeys::get(txn).unwrap_or(vec![]);
keys.push(SeraiKeyDbEntry { activation_block_number, key: BorshG(key) });
let mut keys: Vec<SeraiKeyDbEntry<EncodableG<KeyFor<S>>>> =
ActiveKeys::get(txn).unwrap_or(vec![]);
keys.push(SeraiKeyDbEntry { activation_block_number, key: EncodableG(key) });
ActiveKeys::set(txn, &keys);
}
/// Retire a key.
///
/// The key retired must be the oldest key. There must be another key actively tracked.
// 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
pub(crate) fn retire_key(txn: &mut impl DbTxn, key: KeyFor<S>) {
let mut keys: Vec<SeraiKeyDbEntry<BorshG<KeyFor<S>>>> =
pub(crate) fn retire_key(txn: &mut impl DbTxn, at_block: u64, key: KeyFor<S>) {
// Set the block which has a key retire as notable
NotableBlock::set(txn, at_block, &());
let keys: Vec<SeraiKeyDbEntry<EncodableG<KeyFor<S>>>> =
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);
RetireAt::set(txn, EncodableG(key), &at_block);
}
pub(crate) fn tidy_keys(txn: &mut impl DbTxn) {
let mut keys: Vec<SeraiKeyDbEntry<EncodableG<KeyFor<S>>>> =
ActiveKeys::get(txn).expect("retiring key yet no active keys");
let Some(key) = keys.first() else { return };
// Get the block we're scanning for next
let block_number = Self::next_to_scan_for_outputs_block(txn).expect(
"tidying keys despite never setting the next to scan for block (done on initialization)",
);
// If this key is scheduled for retiry...
if let Some(retire_at) = RetireAt::get(txn, key.key) {
// And is retired by/at this block...
if retire_at <= block_number {
// Remove it from the list of keys
let key = keys.remove(0);
ActiveKeys::set(txn, &keys);
// Also clean up the retiry block
RetireAt::del(txn, key.key);
}
}
}
/// Fetch the active keys, as of the next-to-scan-for-outputs Block.
///
@ -129,9 +152,16 @@ impl<S: ScannerFeed> ScannerDb<S> {
// 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<SeraiKeyDbEntry<BorshG<KeyFor<S>>>> = ActiveKeys::get(getter)?;
let raw_keys: Vec<SeraiKeyDbEntry<EncodableG<KeyFor<S>>>> = ActiveKeys::get(getter)?;
let mut keys = Vec::with_capacity(2);
for i in 0 .. raw_keys.len() {
// Ensure this key isn't retired
if let Some(retire_at) = RetireAt::get(getter, raw_keys[i].key) {
if retire_at <= block_number {
continue;
}
}
// Ensure this key isn't yet to activate
if block_number < raw_keys[i].activation_block_number {
continue;
}
@ -141,7 +171,12 @@ impl<S: ScannerFeed> ScannerDb<S> {
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 });
keys.push(SeraiKey {
key: raw_keys[i].key.0,
stage,
activation_block_number: raw_keys[i].activation_block_number,
block_at_which_reporting_starts,
});
}
assert!(keys.len() <= 2, "more than two keys active");
Some(keys)
@ -154,19 +189,9 @@ impl<S: ScannerFeed> ScannerDb<S> {
);
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 latest_scannable_block(getter: &impl Get) -> Option<u64> {
// 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
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,
@ -177,20 +202,6 @@ impl<S: ScannerFeed> ScannerDb<S> {
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<u64> {
NextToCheckForEventualitiesBlock::get(getter)
}
pub(crate) fn set_next_to_potentially_report_block(
txn: &mut impl DbTxn,
next_to_potentially_report_block: u64,
@ -229,7 +240,15 @@ impl<S: ScannerFeed> ScannerDb<S> {
SerializedQueuedOutputs::set(txn, queue_for_block, &outputs);
}
pub(crate) fn flag_notable(txn: &mut impl DbTxn, block_number: u64) {
/*
This is so verbosely named as the DB itself already flags upon external outputs. Specifically,
if any block yields External outputs to accumulate, we flag it as notable.
There is the slight edge case where some External outputs are queued for accumulation later. We
consider those outputs received as of the block they're queued to (maintaining the policy any
blocks in which we receive outputs is notable).
*/
pub(crate) fn flag_notable_due_to_non_external_output(txn: &mut impl DbTxn, block_number: u64) {
assert!(
NextToPotentiallyReportBlock::get(txn).unwrap() <= block_number,
"already potentially reported a block we're only now flagging as notable"
@ -298,6 +317,17 @@ db_channel! {
pub(crate) struct ScanToEventualityDb<S: ScannerFeed>(PhantomData<S>);
impl<S: ScannerFeed> ScanToEventualityDb<S> {
pub(crate) fn send_scan_data(txn: &mut impl DbTxn, block_number: u64, data: &SenderScanData<S>) {
// If we received an External output to accumulate, or have an External output to forward
// (meaning we received an External output), or have an External output to return (again
// meaning we received an External output), set this block as notable due to receiving outputs
// The non-External output case is covered with `flag_notable_due_to_non_external_output`
if !(data.received_external_outputs.is_empty() &&
data.forwards.is_empty() &&
data.returns.is_empty())
{
NotableBlock::set(txn, block_number, &());
}
/*
SerializedForwardedOutputsIndex: (block_number: u64) -> Vec<u8>,
SerializedForwardedOutput: (output_id: &[u8]) -> Vec<u8>,
@ -357,11 +387,6 @@ impl<S: ScannerFeed> ScanToReportDb<S> {
block_number: u64,
in_instructions: Vec<InInstructionWithBalance>,
) {
if !in_instructions.is_empty() {
// Set this block as notable
NotableBlock::set(txn, block_number, &());
}
InInstructions::send(txn, (), &BlockBoundInInstructions { block_number, in_instructions });
}

View file

@ -13,12 +13,29 @@ impl<T: BorshSerialize + BorshDeserialize> Borshy for T {}
create_db!(
ScannerEventuality {
// The next block to check for resolving eventualities
NextToCheckForEventualitiesBlock: () -> u64,
SerializedEventualities: <K: Borshy>() -> Vec<u8>,
}
);
pub(crate) struct EventualityDb<S: ScannerFeed>(PhantomData<S>);
impl<S: ScannerFeed> EventualityDb<S> {
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<u64> {
NextToCheckForEventualitiesBlock::get(getter)
}
pub(crate) fn set_eventualities(
txn: &mut impl DbTxn,
key: KeyFor<S>,

View file

@ -1,6 +1,6 @@
use group::GroupEncoding;
use serai_db::{DbTxn, Db};
use serai_db::{Get, DbTxn, Db};
use primitives::{task::ContinuallyRan, OutputType, ReceivedOutput, Eventuality, Block};
@ -14,6 +14,16 @@ use crate::{
mod db;
use db::EventualityDb;
/// The latest scannable block, which is determined by this task.
///
/// This task decides when a key retires, which impacts the scan task. Accordingly, the scanner is
/// only allowed to scan `S::WINDOW_LENGTH - 1` blocks ahead so we can safely schedule keys to
/// retire `S::WINDOW_LENGTH` blocks out.
pub(crate) fn latest_scannable_block<S: ScannerFeed>(getter: &impl Get) -> Option<u64> {
EventualityDb::<S>::next_to_check_for_eventualities_block(getter)
.map(|b| b + S::WINDOW_LENGTH - 1)
}
/*
When we scan a block, we receive outputs. When this block is acknowledged, we accumulate those
outputs into some scheduler, potentially causing certain transactions to begin their signing
@ -64,6 +74,21 @@ struct EventualityTask<D: Db, S: ScannerFeed, Sch: Scheduler<S>> {
scheduler: Sch,
}
impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> EventualityTask<D, S, Sch> {
pub(crate) fn new(mut db: D, feed: S, scheduler: Sch, start_block: u64) -> Self {
if EventualityDb::<S>::next_to_check_for_eventualities_block(&db).is_none() {
// Initialize the DB
let mut txn = db.txn();
// We can receive outputs in `start_block`, but any descending transactions will be in the
// next block
EventualityDb::<S>::set_next_to_check_for_eventualities_block(&mut txn, start_block + 1);
txn.commit();
}
Self { db, feed, scheduler }
}
}
#[async_trait::async_trait]
impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> ContinuallyRan for EventualityTask<D, S, Sch> {
async fn run_iteration(&mut self) -> Result<bool, String> {
@ -93,7 +118,7 @@ impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> ContinuallyRan for EventualityTas
.expect("EventualityTask run before writing the start block");
// Fetch the next block to check
let next_to_check = ScannerDb::<S>::next_to_check_for_eventualities_block(&self.db)
let next_to_check = EventualityDb::<S>::next_to_check_for_eventualities_block(&self.db)
.expect("EventualityTask run before writing the start block");
// Check all blocks
@ -121,21 +146,19 @@ impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> ContinuallyRan for EventualityTas
/*
This is proper as the keys for the next to scan block (at most `WINDOW_LENGTH` ahead,
which is `<= CONFIRMATIONS`) will be the keys to use here.
which is `<= CONFIRMATIONS`) will be the keys to use here, with only minor edge cases.
If we had added a new key (which hasn't actually actived by the block we're currently
working on), it won't have any Eventualities for at least `CONFIRMATIONS` blocks (so it'd
have no impact here).
This may include a key which has yet to activate by our perception. We can simply drop
those.
As for retiring a key, that's done on this task's timeline. We ensure we don't bork the
scanner by officially retiring the key `WINDOW_LENGTH` blocks in the future (ensuring the
scanner never has a malleable view of the keys).
This may not include a key which has retired by the next-to-scan block. This task is the
one which decides when to retire a key, and when it marks a key to be retired, it is done
with it. Accordingly, it's not an issue if such a key was dropped.
*/
// TODO: Ensure the add key/remove key DB fns are called by the same task to prevent issues
// there
// TODO: On register eventuality, assert the above timeline assumptions
let mut keys = ScannerDb::<S>::active_keys_as_of_next_to_scan_for_outputs_block(&self.db)
.expect("scanning for a blockchain without any keys set");
// Since the next-to-scan block is ahead of us, drop keys which have yet to actually activate
keys.retain(|key| b <= key.activation_block_number);
let mut txn = self.db.txn();
@ -146,20 +169,16 @@ impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> ContinuallyRan for EventualityTas
scan_data;
let mut outputs = received_external_outputs;
for key in keys {
let (eventualities_is_empty, completed_eventualities) = {
for key in &keys {
let completed_eventualities = {
let mut eventualities = EventualityDb::<S>::eventualities(&txn, key.key);
let completed_eventualities = block.check_for_eventuality_resolutions(&mut eventualities);
EventualityDb::<S>::set_eventualities(&mut txn, key.key, &eventualities);
(eventualities.active_eventualities.is_empty(), completed_eventualities)
completed_eventualities
};
for (tx, completed_eventuality) in completed_eventualities {
log::info!(
"eventuality {} resolved by {}",
hex::encode(completed_eventuality.id()),
hex::encode(tx.as_ref())
);
for tx in completed_eventualities.keys() {
log::info!("eventuality resolved by {}", hex::encode(tx.as_ref()));
}
// Fetch all non-External outputs
@ -221,10 +240,12 @@ impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> ContinuallyRan for EventualityTas
outputs.extend(non_external_outputs);
}
// Update the scheduler
let mut scheduler_update = SchedulerUpdate { outputs, forwards, returns };
scheduler_update.outputs.sort_by(sort_outputs);
scheduler_update.forwards.sort_by(sort_outputs);
scheduler_update.returns.sort_by(|a, b| sort_outputs(&a.output, &b.output));
// Intake the new Eventualities
let new_eventualities = self.scheduler.update(&mut txn, scheduler_update);
for (key, new_eventualities) in new_eventualities {
let key = {
@ -234,6 +255,11 @@ impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> ContinuallyRan for EventualityTas
KeyFor::<S>::from_bytes(&key_repr).unwrap()
};
keys
.iter()
.find(|serai_key| serai_key.key == key)
.expect("queueing eventuality for key which isn't active");
let mut eventualities = EventualityDb::<S>::eventualities(&txn, key);
for new_eventuality in new_eventualities {
eventualities.active_eventualities.insert(new_eventuality.lookup(), new_eventuality);
@ -241,24 +267,26 @@ impl<D: Db, S: ScannerFeed, Sch: Scheduler<S>> ContinuallyRan for EventualityTas
EventualityDb::<S>::set_eventualities(&mut txn, key, &eventualities);
}
for key in keys {
// Now that we've intaked any Eventualities caused, check if we're retiring any keys
for key in &keys {
if key.stage == LifetimeStage::Finishing {
let eventualities = EventualityDb::<S>::eventualities(&txn, key.key);
// TODO: This assumes the Scheduler is empty
if eventualities.active_eventualities.is_empty() {
log::info!(
"key {} has finished and is being retired",
hex::encode(key.key.to_bytes().as_ref())
);
ScannerDb::<S>::flag_notable(&mut txn, b + S::WINDOW_LENGTH);
// TODO: Retire the key
todo!("TODO")
// Retire this key `WINDOW_LENGTH` blocks in the future to ensure the scan task never
// has a malleable view of the keys.
ScannerDb::<S>::retire_key(&mut txn, b + S::WINDOW_LENGTH, key.key);
}
}
}
// Update the next to check block
ScannerDb::<S>::set_next_to_check_for_eventualities_block(&mut txn, next_to_check);
// Update the next-to-check block
EventualityDb::<S>::set_next_to_check_for_eventualities_block(&mut txn, next_to_check);
txn.commit();
}

View file

@ -14,6 +14,7 @@ mod lifetime;
// Database schema definition and associated functions.
mod db;
use db::ScannerDb;
// Task to index the blockchain, ensuring we don't reorganize finalized blocks.
mod index;
// Scans blocks for received coins.
@ -208,7 +209,9 @@ impl<S: ScannerFeed> Scanner<S> {
"acknowledging a block which wasn't notable"
);
ScannerDb::<S>::set_highest_acknowledged_block(txn, block_number);
ScannerDb::<S>::queue_key(txn, block_number + S::WINDOW_LENGTH);
if let Some(key_to_activate) = key_to_activate {
ScannerDb::<S>::queue_key(txn, block_number + S::WINDOW_LENGTH, key_to_activate);
}
}
/// Queue Burns.
@ -249,11 +252,6 @@ impl<N: Network, D: Db> ScannerDb<N, D> {
// Return this block's outputs so they can be pruned from the RAM cache
outputs
}
fn latest_scanned_block<G: Get>(getter: &G) -> Option<usize> {
getter
.get(Self::scanned_block_key())
.map(|bytes| u64::from_le_bytes(bytes.try_into().unwrap()).try_into().unwrap())
}
}
// Panic if we've already seen these outputs

View file

@ -13,6 +13,7 @@ use crate::{
lifetime::LifetimeStage,
db::{OutputWithInInstruction, SenderScanData, ScannerDb, ScanToReportDb, ScanToEventualityDb},
BlockExt, ScannerFeed, AddressFor, OutputFor, Return, sort_outputs,
eventuality::latest_scannable_block,
};
// Construct an InInstruction from an external output.
@ -69,7 +70,7 @@ struct ScanForOutputsTask<D: Db, S: ScannerFeed> {
impl<D: Db, S: ScannerFeed> ContinuallyRan for ScanForOutputsTask<D, S> {
async fn run_iteration(&mut self) -> Result<bool, String> {
// Fetch the safe to scan block
let latest_scannable = ScannerDb::<S>::latest_scannable_block(&self.db)
let latest_scannable = latest_scannable_block::<S>(&self.db)
.expect("ScanForOutputsTask run before writing the start block");
// Fetch the next block to scan
let next_to_scan = ScannerDb::<S>::next_to_scan_for_outputs_block(&self.db)
@ -80,12 +81,16 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for ScanForOutputsTask<D, S> {
log::info!("scanning block: {} ({b})", hex::encode(block.id()));
assert_eq!(ScannerDb::<S>::next_to_scan_for_outputs_block(&self.db).unwrap(), b);
let keys = ScannerDb::<S>::active_keys_as_of_next_to_scan_for_outputs_block(&self.db)
.expect("scanning for a blockchain without any keys set");
let mut txn = self.db.txn();
assert_eq!(ScannerDb::<S>::next_to_scan_for_outputs_block(&txn).unwrap(), b);
// Tidy the keys, then fetch them
// We don't have to tidy them here, we just have to somewhere, so why not here?
ScannerDb::<S>::tidy_keys(&mut txn);
let keys = ScannerDb::<S>::active_keys_as_of_next_to_scan_for_outputs_block(&txn)
.expect("scanning for a blockchain without any keys set");
let mut scan_data = SenderScanData {
block_number: b,
received_external_outputs: vec![],
@ -156,7 +161,7 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for ScanForOutputsTask<D, S> {
// We ensure it's over the dust limit to prevent people sending 1 satoshi from causing
// an invocation of a consensus/signing protocol
if balance.amount.0 >= self.feed.dust(balance.coin).0 {
ScannerDb::<S>::flag_notable(&mut txn, b);
ScannerDb::<S>::flag_notable_due_to_non_external_output(&mut txn, b);
}
continue;
}