2023-09-25 18:23:39 -04:00
|
|
|
use core::{ops::Deref, time::Duration};
|
2023-10-13 22:31:26 -04:00
|
|
|
use std::{
|
|
|
|
|
sync::Arc,
|
|
|
|
|
collections::{HashSet, HashMap},
|
|
|
|
|
};
|
2023-04-15 17:38:47 -04:00
|
|
|
|
2023-04-16 00:51:56 -04:00
|
|
|
use zeroize::Zeroizing;
|
|
|
|
|
|
2023-04-16 03:16:53 -04:00
|
|
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
2023-04-16 00:51:56 -04:00
|
|
|
|
Add a cosigning protocol to ensure finalizations are unique (#433)
* Add a function to deterministically decide which Serai blocks should be co-signed
Has a 5 minute latency between co-signs, also used as the maximal latency
before a co-sign is started.
* Get all active tributaries we're in at a specific block
* Add and route CosignSubstrateBlock, a new provided TX
* Split queued cosigns per network
* Rename BatchSignId to SubstrateSignId
* Add SubstrateSignableId, a meta-type for either Batch or Block, and modularize around it
* Handle the CosignSubstrateBlock provided TX
* Revert substrate_signer.rs to develop (and patch to still work)
Due to SubstrateSigner moving when the prior multisig closes, yet cosigning
occurring with the most recent key, a single SubstrateSigner can be reused.
We could manage multiple SubstrateSigners, yet considering the much lower
specifications for cosigning, I'd rather treat it distinctly.
* Route cosigning through the processor
* Add note to rename SubstrateSigner post-PR
I don't want to do so now in order to preserve the diff's clarity.
* Implement cosign evaluation into the coordinator
* Get tests to compile
* Bug fixes, mark blocks without cosigners available as cosigned
* Correct the ID Batch preprocesses are saved under, add log statements
* Create a dedicated function to handle cosigns
* Correct the flow around Batch verification/queueing
Verifying `Batch`s could stall when a `Batch` was signed before its
predecessors/before the block it's contained in was cosigned (the latter being
inevitable as we can't sign a block containing a signed batch before signing
the batch).
Now, Batch verification happens on a distinct async task in order to not block
the handling of processor messages. This task is the sole caller of verify in
order to ensure last_verified_batch isn't unexpectedly mutated.
When the processor message handler needs to access it, or needs to queue a
Batch, it associates the DB TXN with a lock preventing the other task from
doing so.
This lock, as currently implemented, is a poor and inefficient design. It
should be modified to the pattern used for cosign management. Additionally, a
new primitive of a DB-backed channel may be immensely valuable.
Fixes a standing potential deadlock and a deadlock introduced with the
cosigning protocol.
* Working full-stack tests
After the last commit, this only required extending a timeout.
* Replace "co-sign" with "cosign" to make finding text easier
* Update the coordinator tests to support cosigning
* Inline prior_batch calculation to prevent panic on rotation
Noticed when doing a final review of the branch.
2023-11-15 16:57:21 -05:00
|
|
|
use scale::{Encode, Decode};
|
2023-04-16 03:16:53 -04:00
|
|
|
use serai_client::{
|
2023-10-14 02:52:02 -04:00
|
|
|
SeraiError, Block, Serai, TemporalSerai,
|
2023-08-14 18:57:38 +03:00
|
|
|
primitives::{BlockHash, NetworkId},
|
2023-04-16 03:16:53 -04:00
|
|
|
validator_sets::{
|
Add a cosigning protocol to ensure finalizations are unique (#433)
* Add a function to deterministically decide which Serai blocks should be co-signed
Has a 5 minute latency between co-signs, also used as the maximal latency
before a co-sign is started.
* Get all active tributaries we're in at a specific block
* Add and route CosignSubstrateBlock, a new provided TX
* Split queued cosigns per network
* Rename BatchSignId to SubstrateSignId
* Add SubstrateSignableId, a meta-type for either Batch or Block, and modularize around it
* Handle the CosignSubstrateBlock provided TX
* Revert substrate_signer.rs to develop (and patch to still work)
Due to SubstrateSigner moving when the prior multisig closes, yet cosigning
occurring with the most recent key, a single SubstrateSigner can be reused.
We could manage multiple SubstrateSigners, yet considering the much lower
specifications for cosigning, I'd rather treat it distinctly.
* Route cosigning through the processor
* Add note to rename SubstrateSigner post-PR
I don't want to do so now in order to preserve the diff's clarity.
* Implement cosign evaluation into the coordinator
* Get tests to compile
* Bug fixes, mark blocks without cosigners available as cosigned
* Correct the ID Batch preprocesses are saved under, add log statements
* Create a dedicated function to handle cosigns
* Correct the flow around Batch verification/queueing
Verifying `Batch`s could stall when a `Batch` was signed before its
predecessors/before the block it's contained in was cosigned (the latter being
inevitable as we can't sign a block containing a signed batch before signing
the batch).
Now, Batch verification happens on a distinct async task in order to not block
the handling of processor messages. This task is the sole caller of verify in
order to ensure last_verified_batch isn't unexpectedly mutated.
When the processor message handler needs to access it, or needs to queue a
Batch, it associates the DB TXN with a lock preventing the other task from
doing so.
This lock, as currently implemented, is a poor and inefficient design. It
should be modified to the pattern used for cosign management. Additionally, a
new primitive of a DB-backed channel may be immensely valuable.
Fixes a standing potential deadlock and a deadlock introduced with the
cosigning protocol.
* Working full-stack tests
After the last commit, this only required extending a timeout.
* Replace "co-sign" with "cosign" to make finding text easier
* Update the coordinator tests to support cosigning
* Inline prior_batch calculation to prevent panic on rotation
Noticed when doing a final review of the branch.
2023-11-15 16:57:21 -05:00
|
|
|
primitives::{Session, ValidatorSet, KeyPair, amortize_excess_key_shares},
|
2023-04-16 03:16:53 -04:00
|
|
|
ValidatorSetsEvent,
|
|
|
|
|
},
|
|
|
|
|
in_instructions::InInstructionsEvent,
|
2023-10-19 13:22:21 +03:00
|
|
|
coins::CoinsEvent,
|
2023-04-16 03:16:53 -04:00
|
|
|
};
|
2023-04-16 00:51:56 -04:00
|
|
|
|
2023-04-20 05:04:08 -04:00
|
|
|
use serai_db::DbTxn;
|
|
|
|
|
|
2023-09-29 04:19:59 -04:00
|
|
|
use processor_messages::SubstrateContext;
|
2023-04-16 03:16:53 -04:00
|
|
|
|
2023-10-13 22:31:26 -04:00
|
|
|
use futures::stream::StreamExt;
|
|
|
|
|
use tokio::{sync::mpsc, time::sleep};
|
2023-08-06 12:38:44 -04:00
|
|
|
|
2023-08-25 18:37:22 -04:00
|
|
|
use crate::{
|
|
|
|
|
Db,
|
|
|
|
|
processors::Processors,
|
2023-11-26 12:14:23 -05:00
|
|
|
tributary::{TributarySpec, SeraiBlockNumber},
|
2023-08-25 18:37:22 -04:00
|
|
|
};
|
2023-04-16 00:51:56 -04:00
|
|
|
|
2023-04-20 05:04:08 -04:00
|
|
|
mod db;
|
|
|
|
|
pub use db::*;
|
2023-04-16 03:16:53 -04:00
|
|
|
|
2023-04-17 00:50:56 -04:00
|
|
|
async fn in_set(
|
|
|
|
|
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
2023-10-14 02:52:02 -04:00
|
|
|
serai: &TemporalSerai<'_>,
|
2023-04-17 00:50:56 -04:00
|
|
|
set: ValidatorSet,
|
2023-04-20 05:04:08 -04:00
|
|
|
) -> Result<Option<bool>, SeraiError> {
|
2023-10-14 02:52:02 -04:00
|
|
|
let Some(participants) = serai.validator_sets().participants(set.network).await? else {
|
2023-04-17 00:50:56 -04:00
|
|
|
return Ok(None);
|
|
|
|
|
};
|
|
|
|
|
let key = (Ristretto::generator() * key.deref()).to_bytes();
|
2023-11-22 14:22:46 +03:00
|
|
|
Ok(Some(participants.iter().any(|(participant, _)| participant.0 == key)))
|
2023-04-17 00:50:56 -04:00
|
|
|
}
|
|
|
|
|
|
2023-10-14 16:09:24 -04:00
|
|
|
async fn handle_new_set<D: Db>(
|
|
|
|
|
txn: &mut D::Transaction<'_>,
|
2023-04-17 00:50:56 -04:00
|
|
|
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
2023-10-14 16:09:24 -04:00
|
|
|
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
2023-04-17 00:50:56 -04:00
|
|
|
serai: &Serai,
|
|
|
|
|
block: &Block,
|
|
|
|
|
set: ValidatorSet,
|
|
|
|
|
) -> Result<(), SeraiError> {
|
2023-10-14 02:52:02 -04:00
|
|
|
if in_set(key, &serai.as_of(block.hash()), set)
|
|
|
|
|
.await?
|
|
|
|
|
.expect("NewSet for set which doesn't exist")
|
|
|
|
|
{
|
2023-08-06 12:38:44 -04:00
|
|
|
log::info!("present in set {:?}", set);
|
|
|
|
|
|
2023-10-14 02:52:02 -04:00
|
|
|
let set_data = {
|
|
|
|
|
let serai = serai.as_of(block.hash()).validator_sets();
|
|
|
|
|
let set_participants =
|
|
|
|
|
serai.participants(set.network).await?.expect("NewSet for set which doesn't exist");
|
|
|
|
|
|
2023-11-22 14:22:46 +03:00
|
|
|
let mut set_data = set_participants
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(k, w)| (k, u16::try_from(w).unwrap()))
|
|
|
|
|
.collect::<Vec<_>>();
|
2023-10-14 02:52:02 -04:00
|
|
|
amortize_excess_key_shares(&mut set_data);
|
|
|
|
|
set_data
|
|
|
|
|
};
|
2023-10-13 02:29:22 -04:00
|
|
|
|
2023-08-06 12:38:44 -04:00
|
|
|
let time = if let Ok(time) = block.time() {
|
|
|
|
|
time
|
|
|
|
|
} else {
|
|
|
|
|
assert_eq!(block.number(), 0);
|
|
|
|
|
// Use the next block's time
|
|
|
|
|
loop {
|
2023-10-14 02:52:02 -04:00
|
|
|
let Ok(Some(res)) = serai.block_by_number(1).await else {
|
2023-08-06 12:38:44 -04:00
|
|
|
sleep(Duration::from_secs(5)).await;
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
break res.time().unwrap();
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-08-08 15:12:47 -04:00
|
|
|
// The block time is in milliseconds yet the Tributary is in seconds
|
|
|
|
|
let time = time / 1000;
|
2023-08-13 02:21:56 -04:00
|
|
|
// Since this block is in the past, and Tendermint doesn't play nice with starting chains after
|
|
|
|
|
// their start time (though it does eventually work), delay the start time by 120 seconds
|
|
|
|
|
// This is meant to handle ~20 blocks of lack of finalization for this first block
|
2023-08-13 04:32:21 -04:00
|
|
|
const SUBSTRATE_TO_TRIBUTARY_TIME_DELAY: u64 = 120;
|
|
|
|
|
let time = time + SUBSTRATE_TO_TRIBUTARY_TIME_DELAY;
|
2023-08-13 02:21:56 -04:00
|
|
|
|
2023-10-13 02:29:22 -04:00
|
|
|
let spec = TributarySpec::new(block.hash(), time, set, set_data);
|
2023-10-14 16:09:24 -04:00
|
|
|
|
|
|
|
|
log::info!("creating new tributary for {:?}", spec.set());
|
|
|
|
|
|
|
|
|
|
// Save it to the database now, not on the channel receiver's side, so this is safe against
|
|
|
|
|
// reboots
|
|
|
|
|
// If this txn finishes, and we reboot, then this'll be reloaded from active Tributaries
|
|
|
|
|
// If this txn doesn't finish, this will be re-fired
|
|
|
|
|
// If we waited to save to the DB, this txn may be finished, preventing re-firing, yet the
|
|
|
|
|
// prior fired event may have not been received yet
|
2023-10-14 21:53:38 -04:00
|
|
|
crate::MainDb::<D>::add_participating_in_tributary(txn, &spec);
|
2023-10-14 16:09:24 -04:00
|
|
|
|
|
|
|
|
new_tributary_spec.send(spec).unwrap();
|
2023-08-06 12:38:44 -04:00
|
|
|
} else {
|
|
|
|
|
log::info!("not present in set {:?}", set);
|
2023-04-16 03:16:53 -04:00
|
|
|
}
|
2023-04-17 00:50:56 -04:00
|
|
|
|
|
|
|
|
Ok(())
|
2023-04-16 03:16:53 -04:00
|
|
|
}
|
|
|
|
|
|
2023-11-26 12:14:23 -05:00
|
|
|
async fn handle_key_gen<Pro: Processors>(
|
2023-05-09 23:44:41 -04:00
|
|
|
processors: &Pro,
|
2023-04-16 03:16:53 -04:00
|
|
|
serai: &Serai,
|
2023-04-17 00:50:56 -04:00
|
|
|
block: &Block,
|
2023-04-16 03:16:53 -04:00
|
|
|
set: ValidatorSet,
|
2023-04-17 00:50:56 -04:00
|
|
|
key_pair: KeyPair,
|
|
|
|
|
) -> Result<(), SeraiError> {
|
Add support for multiple multisigs to the processor (#377)
* Design and document a multisig rotation flow
* Make Scanner::eventualities a HashMap so it's per-key
* Don't drop eventualities, always follow through on them
Technical improvements made along the way.
* Start creating an isolate object to manage multisigs, which doesn't require being a signer
Removes key from SubstrateBlock.
* Move Scanner/Scheduler under multisigs
* Move Batch construction into MultisigManager
* Clarify "should" in Multisig Rotation docs
* Add block_number to MultisigManager, as it controls the scanner
* Move sign_plans into MultisigManager
Removes ThresholdKeys from prepare_send.
* Make SubstrateMutable an alias for MultisigManager
* Rewrite Multisig Rotation
The prior scheme had an exploit possible where funds were sent to the old
multisig, then burnt on Serai to send from the new multisig, locking liquidity
for 6 hours. While a fee could be applied to stragglers, to make this attack
unprofitable, the newly described scheme avoids all this.
* Add mini
mini is a miniature version of Serai, emphasizing Serai's nature as a
collection of independent clocks. The intended use is to identify race
conditions and prove protocols are comprehensive regarding when certain clocks
tick.
This uses loom, a prior candidate for evaluating the processor/coordinator as
free of race conditions (#361).
* Use mini to prove a race condition in the current multisig rotation docs, and prove safety of alternatives
Technically, the prior commit had mini prove the race condition.
The docs currently say the activation block of the new multisig is the block
after the next Batch's. If the two next Batches had already entered the
mempool, prior to set_keys being called, the second next Batch would be
expected to contain the new key's data yet fail to as the key wasn't public
when the Batch was actually created.
The naive solution is to create a Batch, publish it, wait until it's included,
and only then scan the next block. This sets a bound of
`Batch publication time < block time`. Optimistically, we can publish a Batch
in 24s while our shortest block time is 2m. Accordingly, we should be fine with
the naive solution which doesn't take advantage of throughput. #333 may
significantly change latency however and require an algorithm whose throughput
exceeds the rate of blocks created.
In order to re-introduce parallelization, enabling throughput, we need to
define a safe range of blocks to scan without Serai ordering the first one.
mini demonstrates safety of scanning n blocks Serai hasn't acknowledged, so
long as the first is scanned before block n+1 is (shifting the n-block window).
The docs will be updated next, to reflect this.
* Fix Multisig Rotation
I believe this is finally good enough to be final.
1) Fixes the race condition present in the prior document, as demonstrated by
mini.
`Batch`s for block `n` and `n+1`, may have been in the mempool when a
multisig's activation block was set to `n`. This would cause a potentially
distinct `Batch` for `n+1`, despite `n+1` already having a signed `Batch`.
2) Tightens when UIs should use the new multisig to prevent eclipse attacks,
and protection against `Batch` publication delays.
3) Removes liquidity fragmentation by tightening flow/handling of latency.
4) Several clarifications and documentation of reasoning.
5) Correction of "prior multisig" to "all prior multisigs" regarding historical
verification, with explanation why.
* Clarify terminology in mini
Synchronizes it from my original thoughts on potential schema to the design
actually created.
* Remove most of processor's README for a reference to docs/processor
This does drop some misc commentary, though none too beneficial. The section on
scanning, deemed sufficiently beneficial, has been moved to a document and
expanded on.
* Update scanner TODOs in line with new docs
* Correct documentation on Bitcoin::Block::time, and Block::time
* Make the scanner in MultisigManager no longer public
* Always send ConfirmKeyPair, regardless of if in-set
* Cargo.lock changes from a prior commit
* Add a policy document on defining a Canonical Chain
I accidentally committed a version of this with a few headers earlier, and this
is a proper version.
* Competent MultisigManager::new
* Update processor's comments
* Add mini to copied files
* Re-organize Scanner per multisig rotation document
* Add RUST_LOG trace targets to e2e tests
* Have the scanner wait once it gets too far ahead
Also bug fixes.
* Add activation blocks to the scanner
* Split received outputs into existing/new in MultisigManager
* Select the proper scheduler
* Schedule multisig activation as detailed in documentation
* Have the Coordinator assert if multiple `Batch`s occur within a block
While the processor used to have ack_up_to_block, enabling skips in the block
acked, support for this was removed while reworking it for multiple multisigs.
It should happen extremely infrequently.
While it would still be beneficial to have, if multiple `Batch`s could occur
within a block (with the complexity here not being worth adding that ban as a
policy), multiple `Batch`s were blocked for DoS reasons.
* Schedule payments to the proper multisig
* Correct >= to <
* Use the new multisig's key for change on schedule
* Don't report External TXs to prior multisig once deprecated
* Forward from the old multisig to the new one at all opportunities
* Move unfulfilled payments in queue from prior to new multisig
* Create MultisigsDb, splitting it out of MainDb
Drops the call to finish_signing from the Signer. While this will cause endless
re-attempts, the Signer will still consider them completed and drop them,
making this an O(n) cost at boot even if we did nothing from here.
The MultisigManager should call finish_signing once the Scanner completes the
Eventuality.
* Don't check Scanner-emitted completions, trust they are completions
Prevents needing to use async code to mark the completion and creates a
fault-free model. The current model, on fault, would cause a lack of marked
completion in the signer.
* Fix a possible panic in the processor
A shorter-chain reorg could cause this assert to trip. It's fixed by
de-duplicating the data, as the assertion checked consistency. Without the
potential for inconsistency, it's unnecessary.
* Document why an existing TODO isn't valid
* Change when we drop payments for being to the change address
The earlier timing prevents creating Plans solely to the branch address,
causing the payments to be dropped, and the TX to become an effective
aggregation TX.
* Extensively document solutions to Eventualities being potentially created after having already scanned their resolutions
* When closing, drop External/Branch outputs which don't cause progress
* Properly decide if Change outputs should be forward or not when closing
This completes all code needed to make the old multisig have a finite lifetime.
* Commentary on forwarding schemes
* Provide a 1 block window, with liquidity fragmentation risks, due to latency
On Bitcoin, this will be 10 minutes for the relevant Batch to be confirmed. On
Monero, 2 minutes. On Ethereum, ~6 minutes.
Also updates the Multisig Rotation document with the new forwarding plan.
* Implement transaction forwarding from old multisig to new multisig
Identifies a fault where Branch outputs which shouldn't be dropped may be, if
another output fulfills their next step. Locking Branch fulfillment down to
only Branch outputs is not done in this commit, but will be in the next.
* Only let Branch outputs fulfill branches
* Update TODOs
* Move the location of handling signer events to avoid a race condition
* Avoid a deadlock by using a RwLock on a single txn instead of two txns
* Move Batch ID out of the Scanner
* Increase from one block of latency on new keys activation to two
For Monero, this offered just two minutes when our latency to publish a Batch
is around a minute already. This does increase the time our liquidity can be
fragmented by up to 20 minutes (Bitcoin), yet it's a stupid attack only
possible once a week (when we rotate). Prioritizing normal users' transactions
not being subject to forwarding is more important here.
Ideally, we'd not do +2 blocks yet plus `time`, such as +10 minutes, making
this agnostic of the underlying network's block scheduling. This is a
complexity not worth it.
* Split MultisigManager::substrate_block into multiple functions
* Further tweaks to substrate_block
* Acquire a lock on all Scanner operations after calling ack_block
Gives time to call register_eventuality and initiate signing.
* Merge sign_plans into substrate_block
Also ensure the Scanner's lock isn't prematurely released.
* Use a HashMap to pass to-be-forwarded instructions, not the DB
* Successfully determine in ClosingExisting
* Move from 2 blocks of latency when rotating to 10 minutes
Superior as noted in 6d07af92ce10cfd74c17eb3400368b0150eb36d7, now trivial to
implement thanks to prior commit.
* Add note justifying measuring time in blocks when rotating
* Implement delaying of outputs received early to the new multisig per specification
* Documentation on why Branch outputs don't have the race condition concerns Change do
Also ensures 6 hours is at least N::CONFIRMATIONS, for sanity purposes.
* Remove TODO re: sanity checking Eventualities
We sanity check the Plan the Eventuality is derived from, and the Eventuality
is handled moments later (in the same file, with a clear call path). There's no
reason to add such APIs to Eventualities for a sanity check given that.
* Add TODO(now) for TODOs which must be done in this branch
Also deprecates a pair of TODOs to TODO2, and accepts the flow of the Signer
having the Eventuality.
* Correct errors in potential/future flow descriptions
* Accept having a single Plan Vec
Per the following code consuming it, there's no benefit to bifurcating it by
key.
* Only issue sign_transaction on boot for the proper signer
* Only set keys when participating in their construction
* Misc progress
Only send SubstrateBlockAck when we have a signer, as it's only used to tell
the Tributary of what Plans are being signed in response to this block.
Only immediately sets substrate_signer if session is 0.
On boot, doesn't panic if we don't have an active key (as we wouldn't if only
joining the next multisig). Continues.
* Correctly detect and set retirement block
Modifies the retirement block from first block meeting requirements to block
CONFIRMATIONS after.
Adds an ack flow to the Scanner's Confirmed event and Block event to accomplish
this, which may deadlock at this time (will be fixed shortly).
Removes an invalid await (after a point declared unsafe to use await) from
MultisigsManager::next_event.
* Remove deadlock in multisig_completed and document alternative
The alternative is simpler, albeit less efficient. There's no reason to adopt
it now, yet perhaps if it benefits modeling?
* Handle the final step of retirement, dropping the old key and setting new to existing
* Remove TODO about emitting a Block on every step
If we emit on NewAsChange, we lose the purpose of the NewAsChange period.
The only concern is if we reach ClosingExisting, and nothing has happened, then
all coins will still be in the old multisig until something finally does. This
isn't a problem worth solving, as it's latency under exceptional dead time.
* Add TODO about potentially not emitting a Block event for the reitrement block
* Restore accidentally deleted CI file
* Pair of slight tweaks
* Add missing if statement
* Disable an assertion when testing
One of the test flows currently abuses the Scanner in a way triggering it.
2023-09-25 09:48:15 -04:00
|
|
|
processors
|
|
|
|
|
.send(
|
|
|
|
|
set.network,
|
2023-09-29 04:19:59 -04:00
|
|
|
processor_messages::substrate::CoordinatorMessage::ConfirmKeyPair {
|
|
|
|
|
context: SubstrateContext {
|
|
|
|
|
serai_time: block.time().unwrap() / 1000,
|
|
|
|
|
network_latest_finalized_block: serai
|
2023-10-14 02:52:02 -04:00
|
|
|
.as_of(block.hash())
|
|
|
|
|
.in_instructions()
|
|
|
|
|
.latest_block_for_network(set.network)
|
2023-09-29 04:19:59 -04:00
|
|
|
.await?
|
|
|
|
|
// The processor treats this as a magic value which will cause it to find a network
|
|
|
|
|
// block which has a time greater than or equal to the Serai time
|
|
|
|
|
.unwrap_or(BlockHash([0; 32])),
|
Add support for multiple multisigs to the processor (#377)
* Design and document a multisig rotation flow
* Make Scanner::eventualities a HashMap so it's per-key
* Don't drop eventualities, always follow through on them
Technical improvements made along the way.
* Start creating an isolate object to manage multisigs, which doesn't require being a signer
Removes key from SubstrateBlock.
* Move Scanner/Scheduler under multisigs
* Move Batch construction into MultisigManager
* Clarify "should" in Multisig Rotation docs
* Add block_number to MultisigManager, as it controls the scanner
* Move sign_plans into MultisigManager
Removes ThresholdKeys from prepare_send.
* Make SubstrateMutable an alias for MultisigManager
* Rewrite Multisig Rotation
The prior scheme had an exploit possible where funds were sent to the old
multisig, then burnt on Serai to send from the new multisig, locking liquidity
for 6 hours. While a fee could be applied to stragglers, to make this attack
unprofitable, the newly described scheme avoids all this.
* Add mini
mini is a miniature version of Serai, emphasizing Serai's nature as a
collection of independent clocks. The intended use is to identify race
conditions and prove protocols are comprehensive regarding when certain clocks
tick.
This uses loom, a prior candidate for evaluating the processor/coordinator as
free of race conditions (#361).
* Use mini to prove a race condition in the current multisig rotation docs, and prove safety of alternatives
Technically, the prior commit had mini prove the race condition.
The docs currently say the activation block of the new multisig is the block
after the next Batch's. If the two next Batches had already entered the
mempool, prior to set_keys being called, the second next Batch would be
expected to contain the new key's data yet fail to as the key wasn't public
when the Batch was actually created.
The naive solution is to create a Batch, publish it, wait until it's included,
and only then scan the next block. This sets a bound of
`Batch publication time < block time`. Optimistically, we can publish a Batch
in 24s while our shortest block time is 2m. Accordingly, we should be fine with
the naive solution which doesn't take advantage of throughput. #333 may
significantly change latency however and require an algorithm whose throughput
exceeds the rate of blocks created.
In order to re-introduce parallelization, enabling throughput, we need to
define a safe range of blocks to scan without Serai ordering the first one.
mini demonstrates safety of scanning n blocks Serai hasn't acknowledged, so
long as the first is scanned before block n+1 is (shifting the n-block window).
The docs will be updated next, to reflect this.
* Fix Multisig Rotation
I believe this is finally good enough to be final.
1) Fixes the race condition present in the prior document, as demonstrated by
mini.
`Batch`s for block `n` and `n+1`, may have been in the mempool when a
multisig's activation block was set to `n`. This would cause a potentially
distinct `Batch` for `n+1`, despite `n+1` already having a signed `Batch`.
2) Tightens when UIs should use the new multisig to prevent eclipse attacks,
and protection against `Batch` publication delays.
3) Removes liquidity fragmentation by tightening flow/handling of latency.
4) Several clarifications and documentation of reasoning.
5) Correction of "prior multisig" to "all prior multisigs" regarding historical
verification, with explanation why.
* Clarify terminology in mini
Synchronizes it from my original thoughts on potential schema to the design
actually created.
* Remove most of processor's README for a reference to docs/processor
This does drop some misc commentary, though none too beneficial. The section on
scanning, deemed sufficiently beneficial, has been moved to a document and
expanded on.
* Update scanner TODOs in line with new docs
* Correct documentation on Bitcoin::Block::time, and Block::time
* Make the scanner in MultisigManager no longer public
* Always send ConfirmKeyPair, regardless of if in-set
* Cargo.lock changes from a prior commit
* Add a policy document on defining a Canonical Chain
I accidentally committed a version of this with a few headers earlier, and this
is a proper version.
* Competent MultisigManager::new
* Update processor's comments
* Add mini to copied files
* Re-organize Scanner per multisig rotation document
* Add RUST_LOG trace targets to e2e tests
* Have the scanner wait once it gets too far ahead
Also bug fixes.
* Add activation blocks to the scanner
* Split received outputs into existing/new in MultisigManager
* Select the proper scheduler
* Schedule multisig activation as detailed in documentation
* Have the Coordinator assert if multiple `Batch`s occur within a block
While the processor used to have ack_up_to_block, enabling skips in the block
acked, support for this was removed while reworking it for multiple multisigs.
It should happen extremely infrequently.
While it would still be beneficial to have, if multiple `Batch`s could occur
within a block (with the complexity here not being worth adding that ban as a
policy), multiple `Batch`s were blocked for DoS reasons.
* Schedule payments to the proper multisig
* Correct >= to <
* Use the new multisig's key for change on schedule
* Don't report External TXs to prior multisig once deprecated
* Forward from the old multisig to the new one at all opportunities
* Move unfulfilled payments in queue from prior to new multisig
* Create MultisigsDb, splitting it out of MainDb
Drops the call to finish_signing from the Signer. While this will cause endless
re-attempts, the Signer will still consider them completed and drop them,
making this an O(n) cost at boot even if we did nothing from here.
The MultisigManager should call finish_signing once the Scanner completes the
Eventuality.
* Don't check Scanner-emitted completions, trust they are completions
Prevents needing to use async code to mark the completion and creates a
fault-free model. The current model, on fault, would cause a lack of marked
completion in the signer.
* Fix a possible panic in the processor
A shorter-chain reorg could cause this assert to trip. It's fixed by
de-duplicating the data, as the assertion checked consistency. Without the
potential for inconsistency, it's unnecessary.
* Document why an existing TODO isn't valid
* Change when we drop payments for being to the change address
The earlier timing prevents creating Plans solely to the branch address,
causing the payments to be dropped, and the TX to become an effective
aggregation TX.
* Extensively document solutions to Eventualities being potentially created after having already scanned their resolutions
* When closing, drop External/Branch outputs which don't cause progress
* Properly decide if Change outputs should be forward or not when closing
This completes all code needed to make the old multisig have a finite lifetime.
* Commentary on forwarding schemes
* Provide a 1 block window, with liquidity fragmentation risks, due to latency
On Bitcoin, this will be 10 minutes for the relevant Batch to be confirmed. On
Monero, 2 minutes. On Ethereum, ~6 minutes.
Also updates the Multisig Rotation document with the new forwarding plan.
* Implement transaction forwarding from old multisig to new multisig
Identifies a fault where Branch outputs which shouldn't be dropped may be, if
another output fulfills their next step. Locking Branch fulfillment down to
only Branch outputs is not done in this commit, but will be in the next.
* Only let Branch outputs fulfill branches
* Update TODOs
* Move the location of handling signer events to avoid a race condition
* Avoid a deadlock by using a RwLock on a single txn instead of two txns
* Move Batch ID out of the Scanner
* Increase from one block of latency on new keys activation to two
For Monero, this offered just two minutes when our latency to publish a Batch
is around a minute already. This does increase the time our liquidity can be
fragmented by up to 20 minutes (Bitcoin), yet it's a stupid attack only
possible once a week (when we rotate). Prioritizing normal users' transactions
not being subject to forwarding is more important here.
Ideally, we'd not do +2 blocks yet plus `time`, such as +10 minutes, making
this agnostic of the underlying network's block scheduling. This is a
complexity not worth it.
* Split MultisigManager::substrate_block into multiple functions
* Further tweaks to substrate_block
* Acquire a lock on all Scanner operations after calling ack_block
Gives time to call register_eventuality and initiate signing.
* Merge sign_plans into substrate_block
Also ensure the Scanner's lock isn't prematurely released.
* Use a HashMap to pass to-be-forwarded instructions, not the DB
* Successfully determine in ClosingExisting
* Move from 2 blocks of latency when rotating to 10 minutes
Superior as noted in 6d07af92ce10cfd74c17eb3400368b0150eb36d7, now trivial to
implement thanks to prior commit.
* Add note justifying measuring time in blocks when rotating
* Implement delaying of outputs received early to the new multisig per specification
* Documentation on why Branch outputs don't have the race condition concerns Change do
Also ensures 6 hours is at least N::CONFIRMATIONS, for sanity purposes.
* Remove TODO re: sanity checking Eventualities
We sanity check the Plan the Eventuality is derived from, and the Eventuality
is handled moments later (in the same file, with a clear call path). There's no
reason to add such APIs to Eventualities for a sanity check given that.
* Add TODO(now) for TODOs which must be done in this branch
Also deprecates a pair of TODOs to TODO2, and accepts the flow of the Signer
having the Eventuality.
* Correct errors in potential/future flow descriptions
* Accept having a single Plan Vec
Per the following code consuming it, there's no benefit to bifurcating it by
key.
* Only issue sign_transaction on boot for the proper signer
* Only set keys when participating in their construction
* Misc progress
Only send SubstrateBlockAck when we have a signer, as it's only used to tell
the Tributary of what Plans are being signed in response to this block.
Only immediately sets substrate_signer if session is 0.
On boot, doesn't panic if we don't have an active key (as we wouldn't if only
joining the next multisig). Continues.
* Correctly detect and set retirement block
Modifies the retirement block from first block meeting requirements to block
CONFIRMATIONS after.
Adds an ack flow to the Scanner's Confirmed event and Block event to accomplish
this, which may deadlock at this time (will be fixed shortly).
Removes an invalid await (after a point declared unsafe to use await) from
MultisigsManager::next_event.
* Remove deadlock in multisig_completed and document alternative
The alternative is simpler, albeit less efficient. There's no reason to adopt
it now, yet perhaps if it benefits modeling?
* Handle the final step of retirement, dropping the old key and setting new to existing
* Remove TODO about emitting a Block on every step
If we emit on NewAsChange, we lose the purpose of the NewAsChange period.
The only concern is if we reach ClosingExisting, and nothing has happened, then
all coins will still be in the old multisig until something finally does. This
isn't a problem worth solving, as it's latency under exceptional dead time.
* Add TODO about potentially not emitting a Block event for the reitrement block
* Restore accidentally deleted CI file
* Pair of slight tweaks
* Add missing if statement
* Disable an assertion when testing
One of the test flows currently abuses the Scanner in a way triggering it.
2023-09-25 09:48:15 -04:00
|
|
|
},
|
2023-11-26 12:14:23 -05:00
|
|
|
session: set.session,
|
2023-09-29 04:19:59 -04:00
|
|
|
key_pair,
|
|
|
|
|
},
|
Add support for multiple multisigs to the processor (#377)
* Design and document a multisig rotation flow
* Make Scanner::eventualities a HashMap so it's per-key
* Don't drop eventualities, always follow through on them
Technical improvements made along the way.
* Start creating an isolate object to manage multisigs, which doesn't require being a signer
Removes key from SubstrateBlock.
* Move Scanner/Scheduler under multisigs
* Move Batch construction into MultisigManager
* Clarify "should" in Multisig Rotation docs
* Add block_number to MultisigManager, as it controls the scanner
* Move sign_plans into MultisigManager
Removes ThresholdKeys from prepare_send.
* Make SubstrateMutable an alias for MultisigManager
* Rewrite Multisig Rotation
The prior scheme had an exploit possible where funds were sent to the old
multisig, then burnt on Serai to send from the new multisig, locking liquidity
for 6 hours. While a fee could be applied to stragglers, to make this attack
unprofitable, the newly described scheme avoids all this.
* Add mini
mini is a miniature version of Serai, emphasizing Serai's nature as a
collection of independent clocks. The intended use is to identify race
conditions and prove protocols are comprehensive regarding when certain clocks
tick.
This uses loom, a prior candidate for evaluating the processor/coordinator as
free of race conditions (#361).
* Use mini to prove a race condition in the current multisig rotation docs, and prove safety of alternatives
Technically, the prior commit had mini prove the race condition.
The docs currently say the activation block of the new multisig is the block
after the next Batch's. If the two next Batches had already entered the
mempool, prior to set_keys being called, the second next Batch would be
expected to contain the new key's data yet fail to as the key wasn't public
when the Batch was actually created.
The naive solution is to create a Batch, publish it, wait until it's included,
and only then scan the next block. This sets a bound of
`Batch publication time < block time`. Optimistically, we can publish a Batch
in 24s while our shortest block time is 2m. Accordingly, we should be fine with
the naive solution which doesn't take advantage of throughput. #333 may
significantly change latency however and require an algorithm whose throughput
exceeds the rate of blocks created.
In order to re-introduce parallelization, enabling throughput, we need to
define a safe range of blocks to scan without Serai ordering the first one.
mini demonstrates safety of scanning n blocks Serai hasn't acknowledged, so
long as the first is scanned before block n+1 is (shifting the n-block window).
The docs will be updated next, to reflect this.
* Fix Multisig Rotation
I believe this is finally good enough to be final.
1) Fixes the race condition present in the prior document, as demonstrated by
mini.
`Batch`s for block `n` and `n+1`, may have been in the mempool when a
multisig's activation block was set to `n`. This would cause a potentially
distinct `Batch` for `n+1`, despite `n+1` already having a signed `Batch`.
2) Tightens when UIs should use the new multisig to prevent eclipse attacks,
and protection against `Batch` publication delays.
3) Removes liquidity fragmentation by tightening flow/handling of latency.
4) Several clarifications and documentation of reasoning.
5) Correction of "prior multisig" to "all prior multisigs" regarding historical
verification, with explanation why.
* Clarify terminology in mini
Synchronizes it from my original thoughts on potential schema to the design
actually created.
* Remove most of processor's README for a reference to docs/processor
This does drop some misc commentary, though none too beneficial. The section on
scanning, deemed sufficiently beneficial, has been moved to a document and
expanded on.
* Update scanner TODOs in line with new docs
* Correct documentation on Bitcoin::Block::time, and Block::time
* Make the scanner in MultisigManager no longer public
* Always send ConfirmKeyPair, regardless of if in-set
* Cargo.lock changes from a prior commit
* Add a policy document on defining a Canonical Chain
I accidentally committed a version of this with a few headers earlier, and this
is a proper version.
* Competent MultisigManager::new
* Update processor's comments
* Add mini to copied files
* Re-organize Scanner per multisig rotation document
* Add RUST_LOG trace targets to e2e tests
* Have the scanner wait once it gets too far ahead
Also bug fixes.
* Add activation blocks to the scanner
* Split received outputs into existing/new in MultisigManager
* Select the proper scheduler
* Schedule multisig activation as detailed in documentation
* Have the Coordinator assert if multiple `Batch`s occur within a block
While the processor used to have ack_up_to_block, enabling skips in the block
acked, support for this was removed while reworking it for multiple multisigs.
It should happen extremely infrequently.
While it would still be beneficial to have, if multiple `Batch`s could occur
within a block (with the complexity here not being worth adding that ban as a
policy), multiple `Batch`s were blocked for DoS reasons.
* Schedule payments to the proper multisig
* Correct >= to <
* Use the new multisig's key for change on schedule
* Don't report External TXs to prior multisig once deprecated
* Forward from the old multisig to the new one at all opportunities
* Move unfulfilled payments in queue from prior to new multisig
* Create MultisigsDb, splitting it out of MainDb
Drops the call to finish_signing from the Signer. While this will cause endless
re-attempts, the Signer will still consider them completed and drop them,
making this an O(n) cost at boot even if we did nothing from here.
The MultisigManager should call finish_signing once the Scanner completes the
Eventuality.
* Don't check Scanner-emitted completions, trust they are completions
Prevents needing to use async code to mark the completion and creates a
fault-free model. The current model, on fault, would cause a lack of marked
completion in the signer.
* Fix a possible panic in the processor
A shorter-chain reorg could cause this assert to trip. It's fixed by
de-duplicating the data, as the assertion checked consistency. Without the
potential for inconsistency, it's unnecessary.
* Document why an existing TODO isn't valid
* Change when we drop payments for being to the change address
The earlier timing prevents creating Plans solely to the branch address,
causing the payments to be dropped, and the TX to become an effective
aggregation TX.
* Extensively document solutions to Eventualities being potentially created after having already scanned their resolutions
* When closing, drop External/Branch outputs which don't cause progress
* Properly decide if Change outputs should be forward or not when closing
This completes all code needed to make the old multisig have a finite lifetime.
* Commentary on forwarding schemes
* Provide a 1 block window, with liquidity fragmentation risks, due to latency
On Bitcoin, this will be 10 minutes for the relevant Batch to be confirmed. On
Monero, 2 minutes. On Ethereum, ~6 minutes.
Also updates the Multisig Rotation document with the new forwarding plan.
* Implement transaction forwarding from old multisig to new multisig
Identifies a fault where Branch outputs which shouldn't be dropped may be, if
another output fulfills their next step. Locking Branch fulfillment down to
only Branch outputs is not done in this commit, but will be in the next.
* Only let Branch outputs fulfill branches
* Update TODOs
* Move the location of handling signer events to avoid a race condition
* Avoid a deadlock by using a RwLock on a single txn instead of two txns
* Move Batch ID out of the Scanner
* Increase from one block of latency on new keys activation to two
For Monero, this offered just two minutes when our latency to publish a Batch
is around a minute already. This does increase the time our liquidity can be
fragmented by up to 20 minutes (Bitcoin), yet it's a stupid attack only
possible once a week (when we rotate). Prioritizing normal users' transactions
not being subject to forwarding is more important here.
Ideally, we'd not do +2 blocks yet plus `time`, such as +10 minutes, making
this agnostic of the underlying network's block scheduling. This is a
complexity not worth it.
* Split MultisigManager::substrate_block into multiple functions
* Further tweaks to substrate_block
* Acquire a lock on all Scanner operations after calling ack_block
Gives time to call register_eventuality and initiate signing.
* Merge sign_plans into substrate_block
Also ensure the Scanner's lock isn't prematurely released.
* Use a HashMap to pass to-be-forwarded instructions, not the DB
* Successfully determine in ClosingExisting
* Move from 2 blocks of latency when rotating to 10 minutes
Superior as noted in 6d07af92ce10cfd74c17eb3400368b0150eb36d7, now trivial to
implement thanks to prior commit.
* Add note justifying measuring time in blocks when rotating
* Implement delaying of outputs received early to the new multisig per specification
* Documentation on why Branch outputs don't have the race condition concerns Change do
Also ensures 6 hours is at least N::CONFIRMATIONS, for sanity purposes.
* Remove TODO re: sanity checking Eventualities
We sanity check the Plan the Eventuality is derived from, and the Eventuality
is handled moments later (in the same file, with a clear call path). There's no
reason to add such APIs to Eventualities for a sanity check given that.
* Add TODO(now) for TODOs which must be done in this branch
Also deprecates a pair of TODOs to TODO2, and accepts the flow of the Signer
having the Eventuality.
* Correct errors in potential/future flow descriptions
* Accept having a single Plan Vec
Per the following code consuming it, there's no benefit to bifurcating it by
key.
* Only issue sign_transaction on boot for the proper signer
* Only set keys when participating in their construction
* Misc progress
Only send SubstrateBlockAck when we have a signer, as it's only used to tell
the Tributary of what Plans are being signed in response to this block.
Only immediately sets substrate_signer if session is 0.
On boot, doesn't panic if we don't have an active key (as we wouldn't if only
joining the next multisig). Continues.
* Correctly detect and set retirement block
Modifies the retirement block from first block meeting requirements to block
CONFIRMATIONS after.
Adds an ack flow to the Scanner's Confirmed event and Block event to accomplish
this, which may deadlock at this time (will be fixed shortly).
Removes an invalid await (after a point declared unsafe to use await) from
MultisigsManager::next_event.
* Remove deadlock in multisig_completed and document alternative
The alternative is simpler, albeit less efficient. There's no reason to adopt
it now, yet perhaps if it benefits modeling?
* Handle the final step of retirement, dropping the old key and setting new to existing
* Remove TODO about emitting a Block on every step
If we emit on NewAsChange, we lose the purpose of the NewAsChange period.
The only concern is if we reach ClosingExisting, and nothing has happened, then
all coins will still be in the old multisig until something finally does. This
isn't a problem worth solving, as it's latency under exceptional dead time.
* Add TODO about potentially not emitting a Block event for the reitrement block
* Restore accidentally deleted CI file
* Pair of slight tweaks
* Add missing if statement
* Disable an assertion when testing
One of the test flows currently abuses the Scanner in a way triggering it.
2023-09-25 09:48:15 -04:00
|
|
|
)
|
|
|
|
|
.await;
|
2023-04-17 00:50:56 -04:00
|
|
|
|
|
|
|
|
Ok(())
|
2023-04-16 03:16:53 -04:00
|
|
|
}
|
|
|
|
|
|
2023-09-29 03:51:01 -04:00
|
|
|
async fn handle_batch_and_burns<D: Db, Pro: Processors>(
|
|
|
|
|
db: &mut D,
|
2023-05-09 23:44:41 -04:00
|
|
|
processors: &Pro,
|
2023-04-17 00:50:56 -04:00
|
|
|
serai: &Serai,
|
|
|
|
|
block: &Block,
|
|
|
|
|
) -> Result<(), SeraiError> {
|
|
|
|
|
// Track which networks had events with a Vec in ordr to preserve the insertion order
|
|
|
|
|
// While that shouldn't be needed, ensuring order never hurts, and may enable design choices
|
|
|
|
|
// with regards to Processor <-> Coordinator message passing
|
|
|
|
|
let mut networks_with_event = vec![];
|
2023-08-14 18:57:38 +03:00
|
|
|
let mut network_had_event = |burns: &mut HashMap<_, _>, batches: &mut HashMap<_, _>, network| {
|
2023-04-17 00:50:56 -04:00
|
|
|
// Don't insert this network multiple times
|
|
|
|
|
// A Vec is still used in order to maintain the insertion order
|
|
|
|
|
if !networks_with_event.contains(&network) {
|
|
|
|
|
networks_with_event.push(network);
|
|
|
|
|
burns.insert(network, vec![]);
|
2023-08-14 18:57:38 +03:00
|
|
|
batches.insert(network, vec![]);
|
2023-04-17 00:50:56 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut batch_block = HashMap::new();
|
2023-08-14 18:57:38 +03:00
|
|
|
let mut batches = HashMap::<NetworkId, Vec<u32>>::new();
|
2023-04-17 00:50:56 -04:00
|
|
|
let mut burns = HashMap::new();
|
|
|
|
|
|
2023-10-14 02:52:02 -04:00
|
|
|
let serai = serai.as_of(block.hash());
|
|
|
|
|
for batch in serai.in_instructions().batch_events().await? {
|
2023-09-29 03:51:01 -04:00
|
|
|
if let InInstructionsEvent::Batch { network, id, block: network_block, instructions_hash } =
|
|
|
|
|
batch
|
|
|
|
|
{
|
2023-08-14 18:57:38 +03:00
|
|
|
network_had_event(&mut burns, &mut batches, network);
|
2023-04-17 00:50:56 -04:00
|
|
|
|
2023-09-29 03:51:01 -04:00
|
|
|
let mut txn = db.txn();
|
|
|
|
|
SubstrateDb::<D>::save_batch_instructions_hash(&mut txn, network, id, instructions_hash);
|
|
|
|
|
txn.commit();
|
|
|
|
|
|
Add support for multiple multisigs to the processor (#377)
* Design and document a multisig rotation flow
* Make Scanner::eventualities a HashMap so it's per-key
* Don't drop eventualities, always follow through on them
Technical improvements made along the way.
* Start creating an isolate object to manage multisigs, which doesn't require being a signer
Removes key from SubstrateBlock.
* Move Scanner/Scheduler under multisigs
* Move Batch construction into MultisigManager
* Clarify "should" in Multisig Rotation docs
* Add block_number to MultisigManager, as it controls the scanner
* Move sign_plans into MultisigManager
Removes ThresholdKeys from prepare_send.
* Make SubstrateMutable an alias for MultisigManager
* Rewrite Multisig Rotation
The prior scheme had an exploit possible where funds were sent to the old
multisig, then burnt on Serai to send from the new multisig, locking liquidity
for 6 hours. While a fee could be applied to stragglers, to make this attack
unprofitable, the newly described scheme avoids all this.
* Add mini
mini is a miniature version of Serai, emphasizing Serai's nature as a
collection of independent clocks. The intended use is to identify race
conditions and prove protocols are comprehensive regarding when certain clocks
tick.
This uses loom, a prior candidate for evaluating the processor/coordinator as
free of race conditions (#361).
* Use mini to prove a race condition in the current multisig rotation docs, and prove safety of alternatives
Technically, the prior commit had mini prove the race condition.
The docs currently say the activation block of the new multisig is the block
after the next Batch's. If the two next Batches had already entered the
mempool, prior to set_keys being called, the second next Batch would be
expected to contain the new key's data yet fail to as the key wasn't public
when the Batch was actually created.
The naive solution is to create a Batch, publish it, wait until it's included,
and only then scan the next block. This sets a bound of
`Batch publication time < block time`. Optimistically, we can publish a Batch
in 24s while our shortest block time is 2m. Accordingly, we should be fine with
the naive solution which doesn't take advantage of throughput. #333 may
significantly change latency however and require an algorithm whose throughput
exceeds the rate of blocks created.
In order to re-introduce parallelization, enabling throughput, we need to
define a safe range of blocks to scan without Serai ordering the first one.
mini demonstrates safety of scanning n blocks Serai hasn't acknowledged, so
long as the first is scanned before block n+1 is (shifting the n-block window).
The docs will be updated next, to reflect this.
* Fix Multisig Rotation
I believe this is finally good enough to be final.
1) Fixes the race condition present in the prior document, as demonstrated by
mini.
`Batch`s for block `n` and `n+1`, may have been in the mempool when a
multisig's activation block was set to `n`. This would cause a potentially
distinct `Batch` for `n+1`, despite `n+1` already having a signed `Batch`.
2) Tightens when UIs should use the new multisig to prevent eclipse attacks,
and protection against `Batch` publication delays.
3) Removes liquidity fragmentation by tightening flow/handling of latency.
4) Several clarifications and documentation of reasoning.
5) Correction of "prior multisig" to "all prior multisigs" regarding historical
verification, with explanation why.
* Clarify terminology in mini
Synchronizes it from my original thoughts on potential schema to the design
actually created.
* Remove most of processor's README for a reference to docs/processor
This does drop some misc commentary, though none too beneficial. The section on
scanning, deemed sufficiently beneficial, has been moved to a document and
expanded on.
* Update scanner TODOs in line with new docs
* Correct documentation on Bitcoin::Block::time, and Block::time
* Make the scanner in MultisigManager no longer public
* Always send ConfirmKeyPair, regardless of if in-set
* Cargo.lock changes from a prior commit
* Add a policy document on defining a Canonical Chain
I accidentally committed a version of this with a few headers earlier, and this
is a proper version.
* Competent MultisigManager::new
* Update processor's comments
* Add mini to copied files
* Re-organize Scanner per multisig rotation document
* Add RUST_LOG trace targets to e2e tests
* Have the scanner wait once it gets too far ahead
Also bug fixes.
* Add activation blocks to the scanner
* Split received outputs into existing/new in MultisigManager
* Select the proper scheduler
* Schedule multisig activation as detailed in documentation
* Have the Coordinator assert if multiple `Batch`s occur within a block
While the processor used to have ack_up_to_block, enabling skips in the block
acked, support for this was removed while reworking it for multiple multisigs.
It should happen extremely infrequently.
While it would still be beneficial to have, if multiple `Batch`s could occur
within a block (with the complexity here not being worth adding that ban as a
policy), multiple `Batch`s were blocked for DoS reasons.
* Schedule payments to the proper multisig
* Correct >= to <
* Use the new multisig's key for change on schedule
* Don't report External TXs to prior multisig once deprecated
* Forward from the old multisig to the new one at all opportunities
* Move unfulfilled payments in queue from prior to new multisig
* Create MultisigsDb, splitting it out of MainDb
Drops the call to finish_signing from the Signer. While this will cause endless
re-attempts, the Signer will still consider them completed and drop them,
making this an O(n) cost at boot even if we did nothing from here.
The MultisigManager should call finish_signing once the Scanner completes the
Eventuality.
* Don't check Scanner-emitted completions, trust they are completions
Prevents needing to use async code to mark the completion and creates a
fault-free model. The current model, on fault, would cause a lack of marked
completion in the signer.
* Fix a possible panic in the processor
A shorter-chain reorg could cause this assert to trip. It's fixed by
de-duplicating the data, as the assertion checked consistency. Without the
potential for inconsistency, it's unnecessary.
* Document why an existing TODO isn't valid
* Change when we drop payments for being to the change address
The earlier timing prevents creating Plans solely to the branch address,
causing the payments to be dropped, and the TX to become an effective
aggregation TX.
* Extensively document solutions to Eventualities being potentially created after having already scanned their resolutions
* When closing, drop External/Branch outputs which don't cause progress
* Properly decide if Change outputs should be forward or not when closing
This completes all code needed to make the old multisig have a finite lifetime.
* Commentary on forwarding schemes
* Provide a 1 block window, with liquidity fragmentation risks, due to latency
On Bitcoin, this will be 10 minutes for the relevant Batch to be confirmed. On
Monero, 2 minutes. On Ethereum, ~6 minutes.
Also updates the Multisig Rotation document with the new forwarding plan.
* Implement transaction forwarding from old multisig to new multisig
Identifies a fault where Branch outputs which shouldn't be dropped may be, if
another output fulfills their next step. Locking Branch fulfillment down to
only Branch outputs is not done in this commit, but will be in the next.
* Only let Branch outputs fulfill branches
* Update TODOs
* Move the location of handling signer events to avoid a race condition
* Avoid a deadlock by using a RwLock on a single txn instead of two txns
* Move Batch ID out of the Scanner
* Increase from one block of latency on new keys activation to two
For Monero, this offered just two minutes when our latency to publish a Batch
is around a minute already. This does increase the time our liquidity can be
fragmented by up to 20 minutes (Bitcoin), yet it's a stupid attack only
possible once a week (when we rotate). Prioritizing normal users' transactions
not being subject to forwarding is more important here.
Ideally, we'd not do +2 blocks yet plus `time`, such as +10 minutes, making
this agnostic of the underlying network's block scheduling. This is a
complexity not worth it.
* Split MultisigManager::substrate_block into multiple functions
* Further tweaks to substrate_block
* Acquire a lock on all Scanner operations after calling ack_block
Gives time to call register_eventuality and initiate signing.
* Merge sign_plans into substrate_block
Also ensure the Scanner's lock isn't prematurely released.
* Use a HashMap to pass to-be-forwarded instructions, not the DB
* Successfully determine in ClosingExisting
* Move from 2 blocks of latency when rotating to 10 minutes
Superior as noted in 6d07af92ce10cfd74c17eb3400368b0150eb36d7, now trivial to
implement thanks to prior commit.
* Add note justifying measuring time in blocks when rotating
* Implement delaying of outputs received early to the new multisig per specification
* Documentation on why Branch outputs don't have the race condition concerns Change do
Also ensures 6 hours is at least N::CONFIRMATIONS, for sanity purposes.
* Remove TODO re: sanity checking Eventualities
We sanity check the Plan the Eventuality is derived from, and the Eventuality
is handled moments later (in the same file, with a clear call path). There's no
reason to add such APIs to Eventualities for a sanity check given that.
* Add TODO(now) for TODOs which must be done in this branch
Also deprecates a pair of TODOs to TODO2, and accepts the flow of the Signer
having the Eventuality.
* Correct errors in potential/future flow descriptions
* Accept having a single Plan Vec
Per the following code consuming it, there's no benefit to bifurcating it by
key.
* Only issue sign_transaction on boot for the proper signer
* Only set keys when participating in their construction
* Misc progress
Only send SubstrateBlockAck when we have a signer, as it's only used to tell
the Tributary of what Plans are being signed in response to this block.
Only immediately sets substrate_signer if session is 0.
On boot, doesn't panic if we don't have an active key (as we wouldn't if only
joining the next multisig). Continues.
* Correctly detect and set retirement block
Modifies the retirement block from first block meeting requirements to block
CONFIRMATIONS after.
Adds an ack flow to the Scanner's Confirmed event and Block event to accomplish
this, which may deadlock at this time (will be fixed shortly).
Removes an invalid await (after a point declared unsafe to use await) from
MultisigsManager::next_event.
* Remove deadlock in multisig_completed and document alternative
The alternative is simpler, albeit less efficient. There's no reason to adopt
it now, yet perhaps if it benefits modeling?
* Handle the final step of retirement, dropping the old key and setting new to existing
* Remove TODO about emitting a Block on every step
If we emit on NewAsChange, we lose the purpose of the NewAsChange period.
The only concern is if we reach ClosingExisting, and nothing has happened, then
all coins will still be in the old multisig until something finally does. This
isn't a problem worth solving, as it's latency under exceptional dead time.
* Add TODO about potentially not emitting a Block event for the reitrement block
* Restore accidentally deleted CI file
* Pair of slight tweaks
* Add missing if statement
* Disable an assertion when testing
One of the test flows currently abuses the Scanner in a way triggering it.
2023-09-25 09:48:15 -04:00
|
|
|
// Make sure this is the only Batch event for this network in this Block
|
|
|
|
|
assert!(batch_block.insert(network, network_block).is_none());
|
2023-08-14 18:57:38 +03:00
|
|
|
|
|
|
|
|
// Add the batch included by this block
|
|
|
|
|
batches.get_mut(&network).unwrap().push(id);
|
2023-04-17 00:50:56 -04:00
|
|
|
} else {
|
|
|
|
|
panic!("Batch event wasn't Batch: {batch:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 14:37:31 +03:00
|
|
|
for burn in serai.coins().burn_with_instruction_events().await? {
|
|
|
|
|
if let CoinsEvent::BurnWithInstruction { from: _, instruction } = burn {
|
2023-10-19 13:22:21 +03:00
|
|
|
let network = instruction.balance.coin.network();
|
2023-08-14 18:57:38 +03:00
|
|
|
network_had_event(&mut burns, &mut batches, network);
|
2023-04-17 00:50:56 -04:00
|
|
|
|
|
|
|
|
// network_had_event should register an entry in burns
|
2023-10-19 13:22:21 +03:00
|
|
|
burns.get_mut(&network).unwrap().push(instruction);
|
2023-04-17 00:50:56 -04:00
|
|
|
} else {
|
|
|
|
|
panic!("Burn event wasn't Burn: {burn:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_eq!(HashSet::<&_>::from_iter(networks_with_event.iter()).len(), networks_with_event.len());
|
|
|
|
|
|
|
|
|
|
for network in networks_with_event {
|
2023-07-30 16:11:30 -04:00
|
|
|
let network_latest_finalized_block = if let Some(block) = batch_block.remove(&network) {
|
2023-04-17 00:50:56 -04:00
|
|
|
block
|
|
|
|
|
} else {
|
|
|
|
|
// If it's had a batch or a burn, it must have had a block acknowledged
|
|
|
|
|
serai
|
2023-10-14 02:52:02 -04:00
|
|
|
.in_instructions()
|
|
|
|
|
.latest_block_for_network(network)
|
2023-04-17 00:50:56 -04:00
|
|
|
.await?
|
|
|
|
|
.expect("network had a batch/burn yet never set a latest block")
|
|
|
|
|
};
|
|
|
|
|
|
2023-05-09 23:44:41 -04:00
|
|
|
processors
|
|
|
|
|
.send(
|
|
|
|
|
network,
|
2023-09-29 04:19:59 -04:00
|
|
|
processor_messages::substrate::CoordinatorMessage::SubstrateBlock {
|
|
|
|
|
context: SubstrateContext {
|
|
|
|
|
serai_time: block.time().unwrap() / 1000,
|
|
|
|
|
network_latest_finalized_block,
|
2023-04-18 03:04:52 -04:00
|
|
|
},
|
2023-09-29 04:19:59 -04:00
|
|
|
block: block.number(),
|
|
|
|
|
burns: burns.remove(&network).unwrap(),
|
|
|
|
|
batches: batches.remove(&network).unwrap(),
|
|
|
|
|
},
|
2023-05-09 23:44:41 -04:00
|
|
|
)
|
2023-04-17 02:10:33 -04:00
|
|
|
.await;
|
2023-04-17 00:50:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle a specific Substrate block, returning an error when it fails to get data
|
|
|
|
|
// (not blocking / holding)
|
2023-10-14 16:09:24 -04:00
|
|
|
async fn handle_block<D: Db, Pro: Processors>(
|
2023-04-20 05:04:08 -04:00
|
|
|
db: &mut SubstrateDb<D>,
|
2023-04-16 03:16:53 -04:00
|
|
|
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
2023-10-14 16:09:24 -04:00
|
|
|
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
2023-10-14 16:47:25 -04:00
|
|
|
tributary_retired: &mpsc::UnboundedSender<ValidatorSet>,
|
2023-05-09 23:44:41 -04:00
|
|
|
processors: &Pro,
|
2023-04-16 00:51:56 -04:00
|
|
|
serai: &Serai,
|
|
|
|
|
block: Block,
|
|
|
|
|
) -> Result<(), SeraiError> {
|
2023-04-15 17:38:47 -04:00
|
|
|
let hash = block.hash();
|
2023-04-16 00:51:56 -04:00
|
|
|
|
2023-04-17 00:50:56 -04:00
|
|
|
// Define an indexed event ID.
|
2023-04-16 00:51:56 -04:00
|
|
|
let mut event_id = 0;
|
2023-04-15 17:38:47 -04:00
|
|
|
|
|
|
|
|
// If a new validator set was activated, create tributary/inform processor to do a DKG
|
2023-10-14 02:52:02 -04:00
|
|
|
for new_set in serai.as_of(hash).validator_sets().new_set_events().await? {
|
2023-04-17 00:50:56 -04:00
|
|
|
// Individually mark each event as handled so on reboot, we minimize duplicates
|
|
|
|
|
// Additionally, if the Serai connection also fails 1/100 times, this means a block with 1000
|
|
|
|
|
// events will successfully be incrementally handled (though the Serai connection should be
|
|
|
|
|
// stable)
|
2023-09-08 09:55:19 -04:00
|
|
|
let ValidatorSetsEvent::NewSet { set } = new_set else {
|
|
|
|
|
panic!("NewSet event wasn't NewSet: {new_set:?}");
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-10 22:53:15 -04:00
|
|
|
// If this is Serai, do nothing
|
|
|
|
|
// We only coordinate/process external networks
|
2023-09-08 09:55:19 -04:00
|
|
|
if set.network == NetworkId::Serai {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 05:04:08 -04:00
|
|
|
if !SubstrateDb::<D>::handled_event(&db.0, hash, event_id) {
|
2023-09-08 09:55:19 -04:00
|
|
|
log::info!("found fresh new set event {:?}", new_set);
|
2023-04-20 05:04:08 -04:00
|
|
|
let mut txn = db.0.txn();
|
2023-10-14 16:09:24 -04:00
|
|
|
handle_new_set::<D>(&mut txn, key, new_tributary_spec, serai, &block, set).await?;
|
2023-04-20 05:04:08 -04:00
|
|
|
SubstrateDb::<D>::handle_event(&mut txn, hash, event_id);
|
|
|
|
|
txn.commit();
|
2023-04-16 00:51:56 -04:00
|
|
|
}
|
|
|
|
|
event_id += 1;
|
2023-04-15 17:38:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If a key pair was confirmed, inform the processor
|
2023-10-14 02:52:02 -04:00
|
|
|
for key_gen in serai.as_of(hash).validator_sets().key_gen_events().await? {
|
2023-04-20 05:04:08 -04:00
|
|
|
if !SubstrateDb::<D>::handled_event(&db.0, hash, event_id) {
|
2023-08-06 12:38:44 -04:00
|
|
|
log::info!("found fresh key gen event {:?}", key_gen);
|
2023-04-16 03:16:53 -04:00
|
|
|
if let ValidatorSetsEvent::KeyGen { set, key_pair } = key_gen {
|
2023-11-26 12:14:23 -05:00
|
|
|
handle_key_gen(processors, serai, &block, set, key_pair).await?;
|
2023-04-16 03:16:53 -04:00
|
|
|
} else {
|
|
|
|
|
panic!("KeyGen event wasn't KeyGen: {key_gen:?}");
|
|
|
|
|
}
|
2023-04-20 05:04:08 -04:00
|
|
|
let mut txn = db.0.txn();
|
|
|
|
|
SubstrateDb::<D>::handle_event(&mut txn, hash, event_id);
|
|
|
|
|
txn.commit();
|
2023-04-16 00:51:56 -04:00
|
|
|
}
|
|
|
|
|
event_id += 1;
|
2023-04-15 17:38:47 -04:00
|
|
|
}
|
|
|
|
|
|
2023-10-14 16:47:25 -04:00
|
|
|
for retired_set in serai.as_of(hash).validator_sets().set_retired_events().await? {
|
|
|
|
|
let ValidatorSetsEvent::SetRetired { set } = retired_set else {
|
|
|
|
|
panic!("SetRetired event wasn't SetRetired: {retired_set:?}");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if set.network == NetworkId::Serai {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !SubstrateDb::<D>::handled_event(&db.0, hash, event_id) {
|
|
|
|
|
log::info!("found fresh set retired event {:?}", retired_set);
|
|
|
|
|
let mut txn = db.0.txn();
|
|
|
|
|
crate::MainDb::<D>::retire_tributary(&mut txn, set);
|
|
|
|
|
tributary_retired.send(set).unwrap();
|
|
|
|
|
SubstrateDb::<D>::handle_event(&mut txn, hash, event_id);
|
|
|
|
|
txn.commit();
|
|
|
|
|
}
|
|
|
|
|
event_id += 1;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-17 00:50:56 -04:00
|
|
|
// Finally, tell the processor of acknowledged blocks/burns
|
|
|
|
|
// This uses a single event as. unlike prior events which individually executed code, all
|
|
|
|
|
// following events share data collection
|
|
|
|
|
// This does break the uniqueness of (hash, event_id) -> one event, yet
|
|
|
|
|
// (network, (hash, event_id)) remains valid as a unique ID for an event
|
2023-04-20 05:04:08 -04:00
|
|
|
if !SubstrateDb::<D>::handled_event(&db.0, hash, event_id) {
|
2023-09-29 03:51:01 -04:00
|
|
|
handle_batch_and_burns(&mut db.0, processors, serai, &block).await?;
|
2023-04-15 17:38:47 -04:00
|
|
|
}
|
2023-04-20 05:04:08 -04:00
|
|
|
let mut txn = db.0.txn();
|
|
|
|
|
SubstrateDb::<D>::handle_event(&mut txn, hash, event_id);
|
|
|
|
|
txn.commit();
|
2023-04-15 17:38:47 -04:00
|
|
|
|
2023-04-16 00:51:56 -04:00
|
|
|
Ok(())
|
2023-04-15 17:38:47 -04:00
|
|
|
}
|
|
|
|
|
|
2023-10-14 16:09:24 -04:00
|
|
|
async fn handle_new_blocks<D: Db, Pro: Processors>(
|
2023-04-20 05:04:08 -04:00
|
|
|
db: &mut SubstrateDb<D>,
|
2023-04-16 03:16:53 -04:00
|
|
|
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
2023-10-14 16:09:24 -04:00
|
|
|
new_tributary_spec: &mpsc::UnboundedSender<TributarySpec>,
|
2023-10-14 16:47:25 -04:00
|
|
|
tributary_retired: &mpsc::UnboundedSender<ValidatorSet>,
|
2023-05-09 23:44:41 -04:00
|
|
|
processors: &Pro,
|
2023-04-15 17:38:47 -04:00
|
|
|
serai: &Serai,
|
2023-08-02 12:18:50 -04:00
|
|
|
next_block: &mut u64,
|
2023-04-15 17:38:47 -04:00
|
|
|
) -> Result<(), SeraiError> {
|
|
|
|
|
// Check if there's been a new Substrate block
|
Add a cosigning protocol to ensure finalizations are unique (#433)
* Add a function to deterministically decide which Serai blocks should be co-signed
Has a 5 minute latency between co-signs, also used as the maximal latency
before a co-sign is started.
* Get all active tributaries we're in at a specific block
* Add and route CosignSubstrateBlock, a new provided TX
* Split queued cosigns per network
* Rename BatchSignId to SubstrateSignId
* Add SubstrateSignableId, a meta-type for either Batch or Block, and modularize around it
* Handle the CosignSubstrateBlock provided TX
* Revert substrate_signer.rs to develop (and patch to still work)
Due to SubstrateSigner moving when the prior multisig closes, yet cosigning
occurring with the most recent key, a single SubstrateSigner can be reused.
We could manage multiple SubstrateSigners, yet considering the much lower
specifications for cosigning, I'd rather treat it distinctly.
* Route cosigning through the processor
* Add note to rename SubstrateSigner post-PR
I don't want to do so now in order to preserve the diff's clarity.
* Implement cosign evaluation into the coordinator
* Get tests to compile
* Bug fixes, mark blocks without cosigners available as cosigned
* Correct the ID Batch preprocesses are saved under, add log statements
* Create a dedicated function to handle cosigns
* Correct the flow around Batch verification/queueing
Verifying `Batch`s could stall when a `Batch` was signed before its
predecessors/before the block it's contained in was cosigned (the latter being
inevitable as we can't sign a block containing a signed batch before signing
the batch).
Now, Batch verification happens on a distinct async task in order to not block
the handling of processor messages. This task is the sole caller of verify in
order to ensure last_verified_batch isn't unexpectedly mutated.
When the processor message handler needs to access it, or needs to queue a
Batch, it associates the DB TXN with a lock preventing the other task from
doing so.
This lock, as currently implemented, is a poor and inefficient design. It
should be modified to the pattern used for cosign management. Additionally, a
new primitive of a DB-backed channel may be immensely valuable.
Fixes a standing potential deadlock and a deadlock introduced with the
cosigning protocol.
* Working full-stack tests
After the last commit, this only required extending a timeout.
* Replace "co-sign" with "cosign" to make finding text easier
* Update the coordinator tests to support cosigning
* Inline prior_batch calculation to prevent panic on rotation
Noticed when doing a final review of the branch.
2023-11-15 16:57:21 -05:00
|
|
|
let latest_number = serai.latest_block().await?.number();
|
|
|
|
|
|
|
|
|
|
// TODO: If this block directly builds off a cosigned block *and* doesn't contain events, mark
|
|
|
|
|
// cosigned,
|
|
|
|
|
{
|
|
|
|
|
// If:
|
|
|
|
|
// A) This block has events and it's been at least X blocks since the last cosign or
|
|
|
|
|
// B) This block doesn't have events but it's been X blocks since a skipped block which did
|
|
|
|
|
// have events or
|
|
|
|
|
// C) This block key gens (which changes who the cosigners are)
|
|
|
|
|
// cosign this block.
|
|
|
|
|
const COSIGN_DISTANCE: u64 = 5 * 60 / 6; // 5 minutes, expressed in blocks
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)]
|
|
|
|
|
enum HasEvents {
|
|
|
|
|
KeyGen,
|
|
|
|
|
Yes,
|
|
|
|
|
No,
|
|
|
|
|
}
|
|
|
|
|
async fn block_has_events(
|
|
|
|
|
txn: &mut impl DbTxn,
|
|
|
|
|
serai: &Serai,
|
|
|
|
|
block: u64,
|
|
|
|
|
) -> Result<HasEvents, SeraiError> {
|
|
|
|
|
let cached = BlockHasEvents::get(txn, block);
|
|
|
|
|
match cached {
|
|
|
|
|
None => {
|
|
|
|
|
let serai = serai.as_of(
|
|
|
|
|
serai
|
|
|
|
|
.block_by_number(block)
|
|
|
|
|
.await?
|
|
|
|
|
.expect("couldn't get block which should've been finalized")
|
|
|
|
|
.hash(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if !serai.validator_sets().key_gen_events().await?.is_empty() {
|
|
|
|
|
return Ok(HasEvents::KeyGen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let has_no_events = serai.coins().burn_with_instruction_events().await?.is_empty() &&
|
|
|
|
|
serai.in_instructions().batch_events().await?.is_empty() &&
|
|
|
|
|
serai.validator_sets().new_set_events().await?.is_empty() &&
|
|
|
|
|
serai.validator_sets().set_retired_events().await?.is_empty();
|
|
|
|
|
|
|
|
|
|
let has_events = if has_no_events { HasEvents::No } else { HasEvents::Yes };
|
|
|
|
|
|
|
|
|
|
let has_events = has_events.encode();
|
|
|
|
|
assert_eq!(has_events.len(), 1);
|
|
|
|
|
BlockHasEvents::set(txn, block, &has_events[0]);
|
|
|
|
|
Ok(HasEvents::Yes)
|
|
|
|
|
}
|
|
|
|
|
Some(code) => Ok(HasEvents::decode(&mut [code].as_slice()).unwrap()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut txn = db.0.txn();
|
|
|
|
|
let Some((last_intended_to_cosign_block, mut skipped_block)) = IntendedCosign::get(&txn) else {
|
|
|
|
|
IntendedCosign::set_intended_cosign(&mut txn, 1);
|
|
|
|
|
txn.commit();
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// If we haven't flagged skipped, and a block within the distance had events, flag the first
|
|
|
|
|
// such block as skipped
|
|
|
|
|
let mut distance_end_exclusive = last_intended_to_cosign_block + COSIGN_DISTANCE;
|
|
|
|
|
// If we've never triggered a cosign, don't skip any cosigns
|
|
|
|
|
if CosignTriggered::get(&txn).is_none() {
|
|
|
|
|
distance_end_exclusive = 0;
|
|
|
|
|
}
|
|
|
|
|
if skipped_block.is_none() {
|
|
|
|
|
for b in (last_intended_to_cosign_block + 1) .. distance_end_exclusive {
|
|
|
|
|
if b > latest_number {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if block_has_events(&mut txn, serai, b).await? == HasEvents::Yes {
|
|
|
|
|
skipped_block = Some(b);
|
|
|
|
|
log::debug!("skipping cosigning {b} due to proximity to prior cosign");
|
|
|
|
|
IntendedCosign::set_skipped_cosign(&mut txn, b);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut has_no_cosigners = None;
|
|
|
|
|
let mut cosign = vec![];
|
|
|
|
|
|
|
|
|
|
// Block we should cosign no matter what if no prior blocks qualified for cosigning
|
|
|
|
|
let maximally_latent_cosign_block =
|
|
|
|
|
skipped_block.map(|skipped_block| skipped_block + COSIGN_DISTANCE);
|
|
|
|
|
for block in (last_intended_to_cosign_block + 1) ..= latest_number {
|
2023-11-15 22:49:58 -05:00
|
|
|
let actual_block = serai
|
|
|
|
|
.block_by_number(block)
|
|
|
|
|
.await?
|
|
|
|
|
.expect("couldn't get block which should've been finalized");
|
|
|
|
|
SeraiBlockNumber::set(&mut txn, actual_block.hash(), &block);
|
2023-11-15 20:23:19 -05:00
|
|
|
|
Add a cosigning protocol to ensure finalizations are unique (#433)
* Add a function to deterministically decide which Serai blocks should be co-signed
Has a 5 minute latency between co-signs, also used as the maximal latency
before a co-sign is started.
* Get all active tributaries we're in at a specific block
* Add and route CosignSubstrateBlock, a new provided TX
* Split queued cosigns per network
* Rename BatchSignId to SubstrateSignId
* Add SubstrateSignableId, a meta-type for either Batch or Block, and modularize around it
* Handle the CosignSubstrateBlock provided TX
* Revert substrate_signer.rs to develop (and patch to still work)
Due to SubstrateSigner moving when the prior multisig closes, yet cosigning
occurring with the most recent key, a single SubstrateSigner can be reused.
We could manage multiple SubstrateSigners, yet considering the much lower
specifications for cosigning, I'd rather treat it distinctly.
* Route cosigning through the processor
* Add note to rename SubstrateSigner post-PR
I don't want to do so now in order to preserve the diff's clarity.
* Implement cosign evaluation into the coordinator
* Get tests to compile
* Bug fixes, mark blocks without cosigners available as cosigned
* Correct the ID Batch preprocesses are saved under, add log statements
* Create a dedicated function to handle cosigns
* Correct the flow around Batch verification/queueing
Verifying `Batch`s could stall when a `Batch` was signed before its
predecessors/before the block it's contained in was cosigned (the latter being
inevitable as we can't sign a block containing a signed batch before signing
the batch).
Now, Batch verification happens on a distinct async task in order to not block
the handling of processor messages. This task is the sole caller of verify in
order to ensure last_verified_batch isn't unexpectedly mutated.
When the processor message handler needs to access it, or needs to queue a
Batch, it associates the DB TXN with a lock preventing the other task from
doing so.
This lock, as currently implemented, is a poor and inefficient design. It
should be modified to the pattern used for cosign management. Additionally, a
new primitive of a DB-backed channel may be immensely valuable.
Fixes a standing potential deadlock and a deadlock introduced with the
cosigning protocol.
* Working full-stack tests
After the last commit, this only required extending a timeout.
* Replace "co-sign" with "cosign" to make finding text easier
* Update the coordinator tests to support cosigning
* Inline prior_batch calculation to prevent panic on rotation
Noticed when doing a final review of the branch.
2023-11-15 16:57:21 -05:00
|
|
|
let mut set = false;
|
|
|
|
|
|
|
|
|
|
let block_has_events = block_has_events(&mut txn, serai, block).await?;
|
|
|
|
|
// If this block is within the distance,
|
|
|
|
|
if block < distance_end_exclusive {
|
|
|
|
|
// and set a key, cosign it
|
|
|
|
|
if block_has_events == HasEvents::KeyGen {
|
|
|
|
|
IntendedCosign::set_intended_cosign(&mut txn, block);
|
|
|
|
|
set = true;
|
|
|
|
|
// Carry skipped if it isn't included by cosigning this block
|
|
|
|
|
if let Some(skipped) = skipped_block {
|
|
|
|
|
if skipped > block {
|
|
|
|
|
IntendedCosign::set_skipped_cosign(&mut txn, block);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (Some(block) == maximally_latent_cosign_block) ||
|
|
|
|
|
(block_has_events != HasEvents::No)
|
|
|
|
|
{
|
|
|
|
|
// Since this block was outside the distance and had events/was maximally latent, cosign it
|
|
|
|
|
IntendedCosign::set_intended_cosign(&mut txn, block);
|
|
|
|
|
set = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if set {
|
|
|
|
|
// Get the keys as of the prior block
|
|
|
|
|
// That means if this block is setting new keys (which won't lock in until we process this
|
|
|
|
|
// block), we won't freeze up waiting for the yet-to-be-processed keys to sign this block
|
|
|
|
|
let serai = serai.as_of(actual_block.header().parent_hash.into());
|
|
|
|
|
|
|
|
|
|
has_no_cosigners = Some(actual_block.clone());
|
|
|
|
|
|
|
|
|
|
for network in serai_client::primitives::NETWORKS {
|
|
|
|
|
// Get the latest session to have set keys
|
|
|
|
|
let Some(latest_session) = serai.validator_sets().session(network).await? else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let prior_session = Session(latest_session.0.saturating_sub(1));
|
|
|
|
|
let set_with_keys = if serai
|
|
|
|
|
.validator_sets()
|
|
|
|
|
.keys(ValidatorSet { network, session: prior_session })
|
|
|
|
|
.await?
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
|
|
|
|
ValidatorSet { network, session: prior_session }
|
|
|
|
|
} else {
|
|
|
|
|
let set = ValidatorSet { network, session: latest_session };
|
|
|
|
|
if serai.validator_sets().keys(set).await?.is_none() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Since this is a valid cosigner, don't flag this block as having no cosigners
|
|
|
|
|
has_no_cosigners = None;
|
|
|
|
|
log::debug!("{:?} will be cosigning {block}", set_with_keys.network);
|
|
|
|
|
|
|
|
|
|
if in_set(key, &serai, set_with_keys).await?.unwrap() {
|
|
|
|
|
cosign.push((set_with_keys, block, actual_block.hash()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If this block doesn't have cosigners, yet does have events, automatically mark it as
|
|
|
|
|
// cosigned
|
|
|
|
|
if let Some(has_no_cosigners) = has_no_cosigners {
|
|
|
|
|
log::debug!("{} had no cosigners available, marking as cosigned", has_no_cosigners.number());
|
|
|
|
|
SubstrateDb::<D>::set_latest_cosigned_block(&mut txn, has_no_cosigners.number());
|
|
|
|
|
} else {
|
|
|
|
|
CosignTriggered::set(&mut txn, &());
|
|
|
|
|
for (set, block, hash) in cosign {
|
|
|
|
|
log::debug!("cosigning {block} with {:?} {:?}", set.network, set.session);
|
|
|
|
|
CosignTransactions::append_cosign(&mut txn, set, block, hash);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-19 02:05:01 -05:00
|
|
|
txn.commit();
|
Add a cosigning protocol to ensure finalizations are unique (#433)
* Add a function to deterministically decide which Serai blocks should be co-signed
Has a 5 minute latency between co-signs, also used as the maximal latency
before a co-sign is started.
* Get all active tributaries we're in at a specific block
* Add and route CosignSubstrateBlock, a new provided TX
* Split queued cosigns per network
* Rename BatchSignId to SubstrateSignId
* Add SubstrateSignableId, a meta-type for either Batch or Block, and modularize around it
* Handle the CosignSubstrateBlock provided TX
* Revert substrate_signer.rs to develop (and patch to still work)
Due to SubstrateSigner moving when the prior multisig closes, yet cosigning
occurring with the most recent key, a single SubstrateSigner can be reused.
We could manage multiple SubstrateSigners, yet considering the much lower
specifications for cosigning, I'd rather treat it distinctly.
* Route cosigning through the processor
* Add note to rename SubstrateSigner post-PR
I don't want to do so now in order to preserve the diff's clarity.
* Implement cosign evaluation into the coordinator
* Get tests to compile
* Bug fixes, mark blocks without cosigners available as cosigned
* Correct the ID Batch preprocesses are saved under, add log statements
* Create a dedicated function to handle cosigns
* Correct the flow around Batch verification/queueing
Verifying `Batch`s could stall when a `Batch` was signed before its
predecessors/before the block it's contained in was cosigned (the latter being
inevitable as we can't sign a block containing a signed batch before signing
the batch).
Now, Batch verification happens on a distinct async task in order to not block
the handling of processor messages. This task is the sole caller of verify in
order to ensure last_verified_batch isn't unexpectedly mutated.
When the processor message handler needs to access it, or needs to queue a
Batch, it associates the DB TXN with a lock preventing the other task from
doing so.
This lock, as currently implemented, is a poor and inefficient design. It
should be modified to the pattern used for cosign management. Additionally, a
new primitive of a DB-backed channel may be immensely valuable.
Fixes a standing potential deadlock and a deadlock introduced with the
cosigning protocol.
* Working full-stack tests
After the last commit, this only required extending a timeout.
* Replace "co-sign" with "cosign" to make finding text easier
* Update the coordinator tests to support cosigning
* Inline prior_batch calculation to prevent panic on rotation
Noticed when doing a final review of the branch.
2023-11-15 16:57:21 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reduce to the latest cosigned block
|
|
|
|
|
let latest_number = latest_number.min(SubstrateDb::<D>::latest_cosigned_block(&db.0));
|
|
|
|
|
|
2023-08-02 12:18:50 -04:00
|
|
|
if latest_number < *next_block {
|
2023-04-15 17:38:47 -04:00
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 12:18:50 -04:00
|
|
|
for b in *next_block ..= latest_number {
|
2023-08-01 19:00:48 -04:00
|
|
|
log::info!("found substrate block {b}");
|
2023-04-16 00:51:56 -04:00
|
|
|
handle_block(
|
|
|
|
|
db,
|
2023-04-16 03:16:53 -04:00
|
|
|
key,
|
2023-10-14 16:09:24 -04:00
|
|
|
new_tributary_spec,
|
2023-10-14 16:47:25 -04:00
|
|
|
tributary_retired,
|
2023-05-09 23:44:41 -04:00
|
|
|
processors,
|
2023-04-15 17:38:47 -04:00
|
|
|
serai,
|
Add a cosigning protocol to ensure finalizations are unique (#433)
* Add a function to deterministically decide which Serai blocks should be co-signed
Has a 5 minute latency between co-signs, also used as the maximal latency
before a co-sign is started.
* Get all active tributaries we're in at a specific block
* Add and route CosignSubstrateBlock, a new provided TX
* Split queued cosigns per network
* Rename BatchSignId to SubstrateSignId
* Add SubstrateSignableId, a meta-type for either Batch or Block, and modularize around it
* Handle the CosignSubstrateBlock provided TX
* Revert substrate_signer.rs to develop (and patch to still work)
Due to SubstrateSigner moving when the prior multisig closes, yet cosigning
occurring with the most recent key, a single SubstrateSigner can be reused.
We could manage multiple SubstrateSigners, yet considering the much lower
specifications for cosigning, I'd rather treat it distinctly.
* Route cosigning through the processor
* Add note to rename SubstrateSigner post-PR
I don't want to do so now in order to preserve the diff's clarity.
* Implement cosign evaluation into the coordinator
* Get tests to compile
* Bug fixes, mark blocks without cosigners available as cosigned
* Correct the ID Batch preprocesses are saved under, add log statements
* Create a dedicated function to handle cosigns
* Correct the flow around Batch verification/queueing
Verifying `Batch`s could stall when a `Batch` was signed before its
predecessors/before the block it's contained in was cosigned (the latter being
inevitable as we can't sign a block containing a signed batch before signing
the batch).
Now, Batch verification happens on a distinct async task in order to not block
the handling of processor messages. This task is the sole caller of verify in
order to ensure last_verified_batch isn't unexpectedly mutated.
When the processor message handler needs to access it, or needs to queue a
Batch, it associates the DB TXN with a lock preventing the other task from
doing so.
This lock, as currently implemented, is a poor and inefficient design. It
should be modified to the pattern used for cosign management. Additionally, a
new primitive of a DB-backed channel may be immensely valuable.
Fixes a standing potential deadlock and a deadlock introduced with the
cosigning protocol.
* Working full-stack tests
After the last commit, this only required extending a timeout.
* Replace "co-sign" with "cosign" to make finding text easier
* Update the coordinator tests to support cosigning
* Inline prior_batch calculation to prevent panic on rotation
Noticed when doing a final review of the branch.
2023-11-15 16:57:21 -05:00
|
|
|
serai
|
|
|
|
|
.block_by_number(b)
|
|
|
|
|
.await?
|
|
|
|
|
.expect("couldn't get block before the latest finalized block"),
|
2023-04-15 17:38:47 -04:00
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-02 12:18:50 -04:00
|
|
|
*next_block += 1;
|
|
|
|
|
db.set_next_block(*next_block);
|
2023-08-01 19:00:48 -04:00
|
|
|
log::info!("handled substrate block {b}");
|
2023-04-15 17:38:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2023-10-13 21:46:17 -04:00
|
|
|
|
2023-10-13 22:31:26 -04:00
|
|
|
pub async fn scan_task<D: Db, Pro: Processors>(
|
|
|
|
|
db: D,
|
|
|
|
|
key: Zeroizing<<Ristretto as Ciphersuite>::F>,
|
|
|
|
|
processors: Pro,
|
|
|
|
|
serai: Arc<Serai>,
|
|
|
|
|
new_tributary_spec: mpsc::UnboundedSender<TributarySpec>,
|
2023-10-14 16:47:25 -04:00
|
|
|
tributary_retired: mpsc::UnboundedSender<ValidatorSet>,
|
2023-10-13 22:31:26 -04:00
|
|
|
) {
|
|
|
|
|
log::info!("scanning substrate");
|
|
|
|
|
|
|
|
|
|
let mut db = SubstrateDb::new(db);
|
|
|
|
|
let mut next_substrate_block = db.next_block();
|
|
|
|
|
|
|
|
|
|
let new_substrate_block_notifier = {
|
|
|
|
|
let serai = &serai;
|
|
|
|
|
move || async move {
|
|
|
|
|
loop {
|
|
|
|
|
match serai.newly_finalized_block().await {
|
|
|
|
|
Ok(sub) => return sub,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("couldn't communicate with serai node: {e}");
|
|
|
|
|
sleep(Duration::from_secs(5)).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let mut substrate_block_notifier = new_substrate_block_notifier().await;
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
// await the next block, yet if our notifier had an error, re-create it
|
|
|
|
|
{
|
|
|
|
|
let Ok(next_block) =
|
|
|
|
|
tokio::time::timeout(Duration::from_secs(60), substrate_block_notifier.next()).await
|
|
|
|
|
else {
|
|
|
|
|
// Timed out, which may be because Serai isn't finalizing or may be some issue with the
|
|
|
|
|
// notifier
|
2023-10-14 02:52:02 -04:00
|
|
|
if serai.latest_block().await.map(|block| block.number()).ok() ==
|
2023-10-13 22:31:26 -04:00
|
|
|
Some(next_substrate_block.saturating_sub(1))
|
|
|
|
|
{
|
|
|
|
|
log::info!("serai hasn't finalized a block in the last 60s...");
|
|
|
|
|
} else {
|
|
|
|
|
substrate_block_notifier = new_substrate_block_notifier().await;
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// next_block is a Option<Result>
|
|
|
|
|
if next_block.and_then(Result::ok).is_none() {
|
|
|
|
|
substrate_block_notifier = new_substrate_block_notifier().await;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match handle_new_blocks(
|
|
|
|
|
&mut db,
|
|
|
|
|
&key,
|
2023-10-14 16:09:24 -04:00
|
|
|
&new_tributary_spec,
|
2023-10-14 16:47:25 -04:00
|
|
|
&tributary_retired,
|
2023-10-13 22:31:26 -04:00
|
|
|
&processors,
|
|
|
|
|
&serai,
|
|
|
|
|
&mut next_substrate_block,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::error!("couldn't communicate with serai node: {e}");
|
|
|
|
|
sleep(Duration::from_secs(5)).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-13 23:36:07 -04:00
|
|
|
|
|
|
|
|
/// Gets the expected ID for the next Batch.
|
|
|
|
|
pub(crate) async fn get_expected_next_batch(serai: &Serai, network: NetworkId) -> u32 {
|
|
|
|
|
let mut first = true;
|
|
|
|
|
loop {
|
|
|
|
|
if !first {
|
|
|
|
|
log::error!("{} {network:?}", "couldn't connect to Serai node to get the next batch ID for",);
|
|
|
|
|
sleep(Duration::from_secs(5)).await;
|
|
|
|
|
}
|
|
|
|
|
first = false;
|
|
|
|
|
|
2023-10-14 02:52:02 -04:00
|
|
|
let Ok(latest_block) = serai.latest_block().await else {
|
2023-10-13 23:36:07 -04:00
|
|
|
continue;
|
|
|
|
|
};
|
2023-10-14 02:52:02 -04:00
|
|
|
let Ok(last) =
|
|
|
|
|
serai.as_of(latest_block.hash()).in_instructions().last_batch_for_network(network).await
|
|
|
|
|
else {
|
2023-10-13 23:36:07 -04:00
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
break if let Some(last) = last { last + 1 } else { 0 };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verifies `Batch`s which have already been indexed from Substrate.
|
2023-10-27 23:08:31 -04:00
|
|
|
///
|
Add a cosigning protocol to ensure finalizations are unique (#433)
* Add a function to deterministically decide which Serai blocks should be co-signed
Has a 5 minute latency between co-signs, also used as the maximal latency
before a co-sign is started.
* Get all active tributaries we're in at a specific block
* Add and route CosignSubstrateBlock, a new provided TX
* Split queued cosigns per network
* Rename BatchSignId to SubstrateSignId
* Add SubstrateSignableId, a meta-type for either Batch or Block, and modularize around it
* Handle the CosignSubstrateBlock provided TX
* Revert substrate_signer.rs to develop (and patch to still work)
Due to SubstrateSigner moving when the prior multisig closes, yet cosigning
occurring with the most recent key, a single SubstrateSigner can be reused.
We could manage multiple SubstrateSigners, yet considering the much lower
specifications for cosigning, I'd rather treat it distinctly.
* Route cosigning through the processor
* Add note to rename SubstrateSigner post-PR
I don't want to do so now in order to preserve the diff's clarity.
* Implement cosign evaluation into the coordinator
* Get tests to compile
* Bug fixes, mark blocks without cosigners available as cosigned
* Correct the ID Batch preprocesses are saved under, add log statements
* Create a dedicated function to handle cosigns
* Correct the flow around Batch verification/queueing
Verifying `Batch`s could stall when a `Batch` was signed before its
predecessors/before the block it's contained in was cosigned (the latter being
inevitable as we can't sign a block containing a signed batch before signing
the batch).
Now, Batch verification happens on a distinct async task in order to not block
the handling of processor messages. This task is the sole caller of verify in
order to ensure last_verified_batch isn't unexpectedly mutated.
When the processor message handler needs to access it, or needs to queue a
Batch, it associates the DB TXN with a lock preventing the other task from
doing so.
This lock, as currently implemented, is a poor and inefficient design. It
should be modified to the pattern used for cosign management. Additionally, a
new primitive of a DB-backed channel may be immensely valuable.
Fixes a standing potential deadlock and a deadlock introduced with the
cosigning protocol.
* Working full-stack tests
After the last commit, this only required extending a timeout.
* Replace "co-sign" with "cosign" to make finding text easier
* Update the coordinator tests to support cosigning
* Inline prior_batch calculation to prevent panic on rotation
Noticed when doing a final review of the branch.
2023-11-15 16:57:21 -05:00
|
|
|
/// Spins if a distinct `Batch` is detected on-chain.
|
|
|
|
|
///
|
|
|
|
|
/// This has a slight malleability in that doesn't verify *who* published a `Batch` is as expected.
|
2023-10-27 23:08:31 -04:00
|
|
|
/// This is deemed fine.
|
2023-10-13 23:36:07 -04:00
|
|
|
pub(crate) async fn verify_published_batches<D: Db>(
|
|
|
|
|
txn: &mut D::Transaction<'_>,
|
|
|
|
|
network: NetworkId,
|
|
|
|
|
optimistic_up_to: u32,
|
|
|
|
|
) -> Option<u32> {
|
|
|
|
|
// TODO: Localize from MainDb to SubstrateDb
|
|
|
|
|
let last = crate::MainDb::<D>::last_verified_batch(txn, network);
|
|
|
|
|
for id in last.map(|last| last + 1).unwrap_or(0) ..= optimistic_up_to {
|
|
|
|
|
let Some(on_chain) = SubstrateDb::<D>::batch_instructions_hash(txn, network, id) else {
|
|
|
|
|
break;
|
|
|
|
|
};
|
|
|
|
|
let off_chain = crate::MainDb::<D>::expected_batch(txn, network, id).unwrap();
|
|
|
|
|
if on_chain != off_chain {
|
|
|
|
|
// Halt operations on this network and spin, as this is a critical fault
|
|
|
|
|
loop {
|
|
|
|
|
log::error!(
|
|
|
|
|
"{}! network: {:?} id: {} off-chain: {} on-chain: {}",
|
|
|
|
|
"on-chain batch doesn't match off-chain",
|
|
|
|
|
network,
|
|
|
|
|
id,
|
|
|
|
|
hex::encode(off_chain),
|
|
|
|
|
hex::encode(on_chain),
|
|
|
|
|
);
|
|
|
|
|
sleep(Duration::from_secs(60)).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
crate::MainDb::<D>::save_last_verified_batch(txn, network, id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
crate::MainDb::<D>::last_verified_batch(txn, network)
|
|
|
|
|
}
|