Batch transactions in complex relays (#1669)

* batch transactions in message relay: API prototype

* get rid of Box<dyn BatchTransaction> and actually submit it

* test batch transactions

* message_lane_loop_works_with_batch_transactions

* removed logger

* BatchConfirmationTransaction + BatchDeliveryTransaction

* more prototyping

* fmt

* continue with batch calls

* impl BatchCallBuilder for ()

* BatchDeliveryTransaction impl

* BundledBatchCallBuilder

* proper impl of BundledBatchCallBuilder + use it in RialtoParachain -> Millau

* impl prove_header in OnDemandHeadersRelay

* impl OnDemandParachainsRelay::prove_header (needs extensive tests)

* added a couple of TODOs

* return Result<Option<BatchTx>> when asking for more headers

* prove headers when reauire_* is called && return proper headers from required_header_id

* split parachains::prove_header and test select_headers_to_prove

* more traces and leave TODOs

* use finality stream in SubstrateFinalitySource::prove_block_finality

* prove parachain head at block, selected by headers relay

* const ANCIENT_BLOCK_THRESHOLD

* TODO -> proof

* clippy and spelling

* BatchCallBuilder::build_batch_call() returns Result

* read first proof from two streams

* FailedToFindFinalityProof -> FinalityProofNotFound

* changed select_headers_to_prove to version from PR review
This commit is contained in:
Svyatoslav Nikolsky
2022-12-16 15:04:36 +03:00
committed by Bastian Köcher
parent a732a04ed4
commit be27bd5e97
28 changed files with 1190 additions and 152 deletions
+232 -29
View File
@@ -109,9 +109,28 @@ pub struct NoncesSubmitArtifacts<T> {
pub tx_tracker: T,
}
/// Batch transaction that already submit some headers and needs to be extended with
/// messages/delivery proof before sending.
#[async_trait]
pub trait BatchTransaction<HeaderId, Proof, TransactionTracker, Error>: Send {
/// Header that was required in the original call and which is bundled within this
/// batch transaction.
fn required_header_id(&self) -> HeaderId;
/// Append proof and send transaction to the connected node.
async fn append_proof_and_send(self, proof: Proof) -> Result<TransactionTracker, Error>;
}
/// Source client trait.
#[async_trait]
pub trait SourceClient<P: MessageLane>: RelayClient {
/// Type of batch transaction that submits finality and message receiving proof.
type BatchTransaction: BatchTransaction<
TargetHeaderIdOf<P>,
P::MessagesReceivingProof,
Self::TransactionTracker,
Self::Error,
>;
/// Transaction tracker to track submitted transactions.
type TransactionTracker: TransactionTracker<HeaderId = SourceHeaderIdOf<P>>;
@@ -156,12 +175,31 @@ pub trait SourceClient<P: MessageLane>: RelayClient {
) -> Result<Self::TransactionTracker, Self::Error>;
/// We need given finalized target header on source to continue synchronization.
async fn require_target_header_on_source(&self, id: TargetHeaderIdOf<P>);
///
/// We assume that the absence of header `id` has already been checked by caller.
///
/// The client may return `Some(_)`, which means that nothing has happened yet and
/// the caller must generate and append message receiving proof to the batch transaction
/// to actually send it (along with required header) to the node.
///
/// If function has returned `None`, it means that the caller now must wait for the
/// appearance of the target header `id` at the source client.
async fn require_target_header_on_source(
&self,
id: TargetHeaderIdOf<P>,
) -> Result<Option<Self::BatchTransaction>, Self::Error>;
}
/// Target client trait.
#[async_trait]
pub trait TargetClient<P: MessageLane>: RelayClient {
/// Type of batch transaction that submits finality and messages proof.
type BatchTransaction: BatchTransaction<
SourceHeaderIdOf<P>,
P::MessagesProof,
Self::TransactionTracker,
Self::Error,
>;
/// Transaction tracker to track submitted transactions.
type TransactionTracker: TransactionTracker<HeaderId = TargetHeaderIdOf<P>>;
@@ -201,7 +239,17 @@ pub trait TargetClient<P: MessageLane>: RelayClient {
) -> Result<NoncesSubmitArtifacts<Self::TransactionTracker>, Self::Error>;
/// We need given finalized source header on target to continue synchronization.
async fn require_source_header_on_target(&self, id: SourceHeaderIdOf<P>);
///
/// The client may return `Some(_)`, which means that nothing has happened yet and
/// the caller must generate and append messages proof to the batch transaction
/// to actually send it (along with required header) to the node.
///
/// If function has returned `None`, it means that the caller now must wait for the
/// appearance of the source header `id` at the target client.
async fn require_source_header_on_target(
&self,
id: SourceHeaderIdOf<P>,
) -> Result<Option<Self::BatchTransaction>, Self::Error>;
}
/// State of the client.
@@ -483,6 +531,61 @@ pub(crate) mod tests {
type TargetHeaderHash = TestTargetHeaderHash;
}
#[derive(Clone, Debug)]
pub struct TestMessagesBatchTransaction {
data: Arc<Mutex<TestClientData>>,
required_header_id: TestSourceHeaderId,
tx_tracker: TestTransactionTracker,
}
#[async_trait]
impl BatchTransaction<TestSourceHeaderId, TestMessagesProof, TestTransactionTracker, TestError>
for TestMessagesBatchTransaction
{
fn required_header_id(&self) -> TestSourceHeaderId {
self.required_header_id
}
async fn append_proof_and_send(
self,
proof: TestMessagesProof,
) -> Result<TestTransactionTracker, TestError> {
let mut data = self.data.lock();
data.receive_messages(proof);
Ok(self.tx_tracker)
}
}
#[derive(Clone, Debug)]
pub struct TestConfirmationBatchTransaction {
data: Arc<Mutex<TestClientData>>,
required_header_id: TestTargetHeaderId,
tx_tracker: TestTransactionTracker,
}
#[async_trait]
impl
BatchTransaction<
TestTargetHeaderId,
TestMessagesReceivingProof,
TestTransactionTracker,
TestError,
> for TestConfirmationBatchTransaction
{
fn required_header_id(&self) -> TestTargetHeaderId {
self.required_header_id
}
async fn append_proof_and_send(
self,
proof: TestMessagesReceivingProof,
) -> Result<TestTransactionTracker, TestError> {
let mut data = self.data.lock();
data.receive_messages_delivery_proof(proof);
Ok(self.tx_tracker)
}
}
#[derive(Clone, Debug)]
pub struct TestTransactionTracker(TrackedTransactionStatus<TestTargetHeaderId>);
@@ -517,8 +620,10 @@ pub(crate) mod tests {
target_latest_confirmed_received_nonce: MessageNonce,
target_tracked_transaction_status: TrackedTransactionStatus<TestTargetHeaderId>,
submitted_messages_proofs: Vec<TestMessagesProof>,
target_to_source_batch_transaction: Option<TestConfirmationBatchTransaction>,
target_to_source_header_required: Option<TestTargetHeaderId>,
target_to_source_header_requirements: Vec<TestTargetHeaderId>,
source_to_target_batch_transaction: Option<TestMessagesBatchTransaction>,
source_to_target_header_required: Option<TestSourceHeaderId>,
source_to_target_header_requirements: Vec<TestSourceHeaderId>,
}
@@ -546,14 +651,38 @@ pub(crate) mod tests {
Default::default(),
)),
submitted_messages_proofs: Vec::new(),
target_to_source_batch_transaction: None,
target_to_source_header_required: None,
target_to_source_header_requirements: Vec::new(),
source_to_target_batch_transaction: None,
source_to_target_header_required: None,
source_to_target_header_requirements: Vec::new(),
}
}
}
impl TestClientData {
fn receive_messages(&mut self, proof: TestMessagesProof) {
self.target_state.best_self =
HeaderId(self.target_state.best_self.0 + 1, self.target_state.best_self.1 + 1);
self.target_state.best_finalized_self = self.target_state.best_self;
self.target_latest_received_nonce = *proof.0.end();
if let Some(target_latest_confirmed_received_nonce) = proof.1 {
self.target_latest_confirmed_received_nonce =
target_latest_confirmed_received_nonce;
}
self.submitted_messages_proofs.push(proof);
}
fn receive_messages_delivery_proof(&mut self, proof: TestMessagesReceivingProof) {
self.source_state.best_self =
HeaderId(self.source_state.best_self.0 + 1, self.source_state.best_self.1 + 1);
self.source_state.best_finalized_self = self.source_state.best_self;
self.submitted_messages_receiving_proofs.push(proof);
self.source_latest_confirmed_received_nonce = proof;
}
}
#[derive(Clone)]
pub struct TestSourceClient {
data: Arc<Mutex<TestClientData>>,
@@ -588,6 +717,7 @@ pub(crate) mod tests {
#[async_trait]
impl SourceClient<TestMessageLane> for TestSourceClient {
type BatchTransaction = TestConfirmationBatchTransaction;
type TransactionTracker = TestTransactionTracker;
async fn state(&self) -> Result<SourceClientState<TestMessageLane>, TestError> {
@@ -675,21 +805,25 @@ pub(crate) mod tests {
) -> Result<Self::TransactionTracker, TestError> {
let mut data = self.data.lock();
(self.tick)(&mut data);
data.source_state.best_self =
HeaderId(data.source_state.best_self.0 + 1, data.source_state.best_self.1 + 1);
data.source_state.best_finalized_self = data.source_state.best_self;
data.submitted_messages_receiving_proofs.push(proof);
data.source_latest_confirmed_received_nonce = proof;
data.receive_messages_delivery_proof(proof);
(self.post_tick)(&mut data);
Ok(TestTransactionTracker(data.source_tracked_transaction_status))
}
async fn require_target_header_on_source(&self, id: TargetHeaderIdOf<TestMessageLane>) {
async fn require_target_header_on_source(
&self,
id: TargetHeaderIdOf<TestMessageLane>,
) -> Result<Option<Self::BatchTransaction>, Self::Error> {
let mut data = self.data.lock();
data.target_to_source_header_required = Some(id);
data.target_to_source_header_requirements.push(id);
(self.tick)(&mut data);
(self.post_tick)(&mut data);
Ok(data.target_to_source_batch_transaction.take().map(|mut tx| {
tx.required_header_id = id;
tx
}))
}
}
@@ -727,6 +861,7 @@ pub(crate) mod tests {
#[async_trait]
impl TargetClient<TestMessageLane> for TestTargetClient {
type BatchTransaction = TestMessagesBatchTransaction;
type TransactionTracker = TestTransactionTracker;
async fn state(&self) -> Result<TargetClientState<TestMessageLane>, TestError> {
@@ -798,15 +933,7 @@ pub(crate) mod tests {
if data.is_target_fails {
return Err(TestError)
}
data.target_state.best_self =
HeaderId(data.target_state.best_self.0 + 1, data.target_state.best_self.1 + 1);
data.target_state.best_finalized_self = data.target_state.best_self;
data.target_latest_received_nonce = *proof.0.end();
if let Some(target_latest_confirmed_received_nonce) = proof.1 {
data.target_latest_confirmed_received_nonce =
target_latest_confirmed_received_nonce;
}
data.submitted_messages_proofs.push(proof);
data.receive_messages(proof);
(self.post_tick)(&mut data);
Ok(NoncesSubmitArtifacts {
nonces,
@@ -814,17 +941,25 @@ pub(crate) mod tests {
})
}
async fn require_source_header_on_target(&self, id: SourceHeaderIdOf<TestMessageLane>) {
async fn require_source_header_on_target(
&self,
id: SourceHeaderIdOf<TestMessageLane>,
) -> Result<Option<Self::BatchTransaction>, Self::Error> {
let mut data = self.data.lock();
data.source_to_target_header_required = Some(id);
data.source_to_target_header_requirements.push(id);
(self.tick)(&mut data);
(self.post_tick)(&mut data);
Ok(data.source_to_target_batch_transaction.take().map(|mut tx| {
tx.required_header_id = id;
tx
}))
}
}
fn run_loop_test(
data: TestClientData,
data: Arc<Mutex<TestClientData>>,
source_tick: Arc<dyn Fn(&mut TestClientData) + Send + Sync>,
source_post_tick: Arc<dyn Fn(&mut TestClientData) + Send + Sync>,
target_tick: Arc<dyn Fn(&mut TestClientData) + Send + Sync>,
@@ -832,8 +967,6 @@ pub(crate) mod tests {
exit_signal: impl Future<Output = ()> + 'static + Send,
) -> TestClientData {
async_std::task::block_on(async {
let data = Arc::new(Mutex::new(data));
let source_client = TestSourceClient {
data: data.clone(),
tick: source_tick,
@@ -876,7 +1009,7 @@ pub(crate) mod tests {
// able to deliver messages.
let (exit_sender, exit_receiver) = unbounded();
let result = run_loop_test(
TestClientData {
Arc::new(Mutex::new(TestClientData {
is_source_fails: true,
source_state: ClientState {
best_self: HeaderId(0, 0),
@@ -893,7 +1026,7 @@ pub(crate) mod tests {
},
target_latest_received_nonce: 0,
..Default::default()
},
})),
Arc::new(|data: &mut TestClientData| {
if data.is_source_reconnected {
data.is_source_fails = false;
@@ -929,7 +1062,7 @@ pub(crate) mod tests {
let (source_exit_sender, exit_receiver) = unbounded();
let target_exit_sender = source_exit_sender.clone();
let result = run_loop_test(
TestClientData {
Arc::new(Mutex::new(TestClientData {
source_state: ClientState {
best_self: HeaderId(0, 0),
best_finalized_self: HeaderId(0, 0),
@@ -947,7 +1080,7 @@ pub(crate) mod tests {
target_latest_received_nonce: 0,
target_tracked_transaction_status: TrackedTransactionStatus::Lost,
..Default::default()
},
})),
Arc::new(move |data: &mut TestClientData| {
if data.is_source_reconnected {
data.source_tracked_transaction_status =
@@ -980,7 +1113,7 @@ pub(crate) mod tests {
// their corresponding nonce won't be udpated => reconnect will happen
let (exit_sender, exit_receiver) = unbounded();
let result = run_loop_test(
TestClientData {
Arc::new(Mutex::new(TestClientData {
source_state: ClientState {
best_self: HeaderId(0, 0),
best_finalized_self: HeaderId(0, 0),
@@ -996,7 +1129,7 @@ pub(crate) mod tests {
},
target_latest_received_nonce: 0,
..Default::default()
},
})),
Arc::new(move |data: &mut TestClientData| {
// blocks are produced on every tick
data.source_state.best_self =
@@ -1054,7 +1187,7 @@ pub(crate) mod tests {
fn message_lane_loop_works() {
let (exit_sender, exit_receiver) = unbounded();
let result = run_loop_test(
TestClientData {
Arc::new(Mutex::new(TestClientData {
source_state: ClientState {
best_self: HeaderId(10, 10),
best_finalized_self: HeaderId(10, 10),
@@ -1070,7 +1203,7 @@ pub(crate) mod tests {
},
target_latest_received_nonce: 0,
..Default::default()
},
})),
Arc::new(|data: &mut TestClientData| {
// blocks are produced on every tick
data.source_state.best_self =
@@ -1133,4 +1266,74 @@ pub(crate) mod tests {
assert!(!result.target_to_source_header_requirements.is_empty());
assert!(!result.source_to_target_header_requirements.is_empty());
}
#[test]
fn message_lane_loop_works_with_batch_transactions() {
let (exit_sender, exit_receiver) = unbounded();
let original_data = Arc::new(Mutex::new(TestClientData {
source_state: ClientState {
best_self: HeaderId(10, 10),
best_finalized_self: HeaderId(10, 10),
best_finalized_peer_at_best_self: HeaderId(0, 0),
actual_best_finalized_peer_at_best_self: HeaderId(0, 0),
},
source_latest_generated_nonce: 10,
target_state: ClientState {
best_self: HeaderId(0, 0),
best_finalized_self: HeaderId(0, 0),
best_finalized_peer_at_best_self: HeaderId(0, 0),
actual_best_finalized_peer_at_best_self: HeaderId(0, 0),
},
target_latest_received_nonce: 0,
..Default::default()
}));
let target_original_data = original_data.clone();
let source_original_data = original_data.clone();
let result = run_loop_test(
original_data,
Arc::new(|_| {}),
Arc::new(move |data: &mut TestClientData| {
if let Some(target_to_source_header_required) =
data.target_to_source_header_required.take()
{
data.target_to_source_batch_transaction =
Some(TestConfirmationBatchTransaction {
data: source_original_data.clone(),
required_header_id: target_to_source_header_required,
tx_tracker: TestTransactionTracker::default(),
})
}
}),
Arc::new(|_| {}),
Arc::new(move |data: &mut TestClientData| {
if let Some(source_to_target_header_required) =
data.source_to_target_header_required.take()
{
data.source_to_target_batch_transaction = Some(TestMessagesBatchTransaction {
data: target_original_data.clone(),
required_header_id: source_to_target_header_required,
tx_tracker: TestTransactionTracker::default(),
})
}
if data.source_latest_confirmed_received_nonce == 10 {
exit_sender.unbounded_send(()).unwrap();
}
}),
exit_receiver.into_future().map(|(_, _)| ()),
);
// there are no strict restrictions on when reward confirmation should come
// (because `max_unconfirmed_nonces_at_target` is `100` in tests and this confirmation
// depends on the state of both clients)
// => we do not check it here
assert_eq!(result.submitted_messages_proofs[0].0, 1..=4);
assert_eq!(result.submitted_messages_proofs[1].0, 5..=8);
assert_eq!(result.submitted_messages_proofs[2].0, 9..=10);
assert!(!result.submitted_messages_receiving_proofs.is_empty());
// check that we have at least once required new source->target or target->source headers
assert!(!result.target_to_source_header_requirements.is_empty());
assert!(!result.source_to_target_header_requirements.is_empty());
}
}