Route burns through the scanner

This commit is contained in:
Luke Parker
2024-08-29 12:45:47 -04:00
parent 8ac501028d
commit f9d02d43c2
6 changed files with 223 additions and 39 deletions

View File

@@ -7,6 +7,7 @@ use serai_db::{Get, DbTxn, Db};
use serai_primitives::{NetworkId, Coin, Amount};
use serai_in_instructions_primitives::Batch;
use serai_coins_primitives::OutInstructionWithBalance;
use primitives::{task::*, Address, ReceivedOutput, Block};
@@ -15,7 +16,7 @@ mod lifetime;
// Database schema definition and associated functions.
mod db;
use db::ScannerGlobalDb;
use db::{ScannerGlobalDb, SubstrateToEventualityDb};
// Task to index the blockchain, ensuring we don't reorganize finalized blocks.
mod index;
// Scans blocks for received coins.
@@ -147,7 +148,7 @@ pub trait ScannerFeed: 'static + Send + Sync + Clone {
/// The dust threshold for the specified coin.
///
/// This MUST be constant. Serai MJUST NOT create internal outputs worth less than this. This
/// This MUST be constant. Serai MUST NOT create internal outputs worth less than this. This
/// SHOULD be a value worth handling at a human level.
fn dust(&self, coin: Coin) -> Amount;
}
@@ -195,6 +196,40 @@ pub trait Scheduler<S: ScannerFeed>: 'static + Send {
txn: &mut impl DbTxn,
update: SchedulerUpdate<S>,
) -> HashMap<Vec<u8>, Vec<EventualityFor<S>>>;
/// Fulfill a series of payments, yielding the Eventualities now to be scanned for.
///
/// Any Eventualities returned by this function must include an output-to-self (such as a Branch
/// or Change), unless they descend from a transaction returned by this function which satisfies
/// that requirement.
///
/// The `Vec<u8>` used as the key in the returned HashMap should be the encoded key the
/// Eventualities are for.
/*
We need an output-to-self so we can detect a block with an Eventuality completion with regards
to Burns, forcing us to ensure we have accumulated all the Burns we should by the time we
handle that block. We explicitly don't require children have this requirement as by detecting
the first resolution, we ensure we'll accumulate the Burns (therefore becoming aware of the
childrens' Eventualities, enabling recognizing their resolutions).
This carve out enables the following:
------------------ Fulfillment TX ----------------------
| Primary Output | ---------------> | New Primary Output |
------------------ | ----------------------
|
| ------------------------------
|------> | Branching Output for Burns |
------------------------------
Without wasting pointless Change outputs on every transaction (as there's a single parent which
has an output-to-self).
*/
fn fulfill(
&mut self,
txn: &mut impl DbTxn,
payments: Vec<OutInstructionWithBalance>,
) -> HashMap<Vec<u8>, Vec<EventualityFor<S>>>;
}
/// A representation of a scanner.
@@ -242,6 +277,8 @@ impl<S: ScannerFeed> Scanner<S> {
///
/// This means this block was ordered on Serai in relation to `Burn` events, and all validators
/// have achieved synchrony on it.
///
/// The calls to this function must be ordered with regards to `queue_burns`.
pub fn acknowledge_block(
&mut self,
mut txn: impl DbTxn,
@@ -249,10 +286,23 @@ impl<S: ScannerFeed> Scanner<S> {
key_to_activate: Option<KeyFor<S>>,
) {
log::info!("acknowledging block {block_number}");
assert!(
ScannerGlobalDb::<S>::is_block_notable(&txn, block_number),
"acknowledging a block which wasn't notable"
);
if let Some(prior_highest_acknowledged_block) =
ScannerGlobalDb::<S>::highest_acknowledged_block(&txn)
{
assert!(block_number > prior_highest_acknowledged_block, "acknowledging blocks out-of-order");
for b in (prior_highest_acknowledged_block + 1) .. (block_number - 1) {
assert!(
!ScannerGlobalDb::<S>::is_block_notable(&txn, b),
"skipped acknowledging a block which was notable"
);
}
}
ScannerGlobalDb::<S>::set_highest_acknowledged_block(&mut txn, block_number);
if let Some(key_to_activate) = key_to_activate {
ScannerGlobalDb::<S>::queue_key(&mut txn, block_number + S::WINDOW_LENGTH, key_to_activate);
@@ -268,13 +318,38 @@ impl<S: ScannerFeed> Scanner<S> {
/// Queue Burns.
///
/// The scanner only updates the scheduler with new outputs upon acknowledging a block. We can
/// safely queue Burns so long as they're only actually added once we've handled the outputs from
/// the block acknowledged prior to their queueing.
pub fn queue_burns(&mut self, txn: &mut impl DbTxn, burns: Vec<()>) {
/// The scanner only updates the scheduler with new outputs upon acknowledging a block. The
/// ability to fulfill Burns, and therefore their order, is dependent on the current output
/// state. This immediately sets a bound that this function is ordered with regards to
/// `acknowledge_block`.
/*
The fact Burns can be queued during any Substrate block is problematic. The scanner is allowed
to scan anything within the window set by the Eventuality task. The Eventuality task is allowed
to handle all blocks until it reaches a block needing acknowledgement.
This means we may queue Burns when the latest acknowledged block is 1, yet we've already
scanned 101. Such Burns may complete back in block 2, and we simply wouldn't have noticed due
to not having yet generated the Eventualities.
We solve this by mandating all transactions made as the result of an Eventuality include a
output-to-self worth at least `N::DUST`. If that occurs, the scanner will force a consensus
protocol on block 2. Accordingly, we won't scan all the way to block 101 (missing the
resolution of the Eventuality) as we'll obtain synchrony on block 2 and all Burns queued prior
to it.
Another option would be to re-check historical blocks, yet this would potentially redo an
unbounded amount of work. It would also not allow us to safely detect if received outputs were
in fact the result of Eventualities or not.
Another option would be to schedule Burns after the next-acknowledged block, yet this would add
latency and likely practically require we add regularly scheduled notable blocks (which may be
unnecessary).
*/
pub fn queue_burns(&mut self, txn: &mut impl DbTxn, burns: &Vec<OutInstructionWithBalance>) {
let queue_as_of = ScannerGlobalDb::<S>::highest_acknowledged_block(txn)
.expect("queueing Burns yet never acknowledged a block");
todo!("TODO")
SubstrateToEventualityDb::send_burns(txn, queue_as_of, burns)
}
}