Skip to content

Commit fae592e

Browse files
authored
[testnet] When staging, verify the inboxes. (#4923) (#4925)
Backport of #4923. ## Motivation We currently verify the correct order of incoming messages, and their presence in the inboxes, only when handling a block proposal or confirmed block, but not when _staging_ a block. This can result in the client putting an invalid pending block into the wallet. (Thanks @kikakkz for reporting this!) ## Proposal Do the check also when staging. ## Test Plan A regression test was added. (Mostly by Claude.) ## Release Plan - These changes should be released in a new SDK. ## Links - PR to main: #4923 - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 36717ed commit fae592e

File tree

2 files changed

+109
-0
lines changed

2 files changed

+109
-0
lines changed

linera-core/src/chain_worker/state.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,9 @@ where
13391339
let (_, committee) = self.chain.current_committee()?;
13401340
block.check_proposal_size(committee.policy().maximum_block_proposal_size)?;
13411341

1342+
self.chain
1343+
.remove_bundles_from_inboxes(block.timestamp, true, block.incoming_bundles())
1344+
.await?;
13421345
let outcome =
13431346
Box::pin(self.execute_block(&block, local_time, round, published_blobs)).await?;
13441347

linera-core/src/unit_tests/worker_tests.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4226,3 +4226,109 @@ where
42264226

42274227
Ok(())
42284228
}
4229+
#[test_case(MemoryStorageBuilder::default(); "memory")]
4230+
#[cfg_attr(feature = "rocksdb", test_case(RocksDbStorageBuilder::new().await; "rocks_db"))]
4231+
#[cfg_attr(feature = "dynamodb", test_case(DynamoDbStorageBuilder::default(); "dynamo_db"))]
4232+
#[cfg_attr(feature = "scylladb", test_case(ScyllaDbStorageBuilder::default(); "scylla_db"))]
4233+
#[test_log::test(tokio::test)]
4234+
async fn test_stage_block_with_message_earlier_than_cursor<B>(
4235+
mut storage_builder: B,
4236+
) -> anyhow::Result<()>
4237+
where
4238+
B: StorageBuilder,
4239+
{
4240+
let mut signer = InMemorySigner::new(None);
4241+
let receiver_public_key = signer.generate_new();
4242+
let owner = receiver_public_key.into();
4243+
let mut env = TestEnvironment::new(storage_builder.build().await?, false, false).await;
4244+
let chain_1_desc = env.add_root_chain(1, owner, Amount::from_tokens(10)).await;
4245+
let chain_2_desc = env.add_root_chain(2, owner, Amount::ZERO).await;
4246+
let chain_1 = chain_1_desc.id();
4247+
let chain_2 = chain_2_desc.id();
4248+
4249+
// Simulate a certificate sending two messages from chain_1 to chain_2.
4250+
let sender_hash = CryptoHash::test_hash("sender block");
4251+
4252+
// Process the second message bundle on chain_2. This advances next_cursor_to_remove
4253+
// to height=0, index=1.
4254+
let block_proposal = make_first_block(chain_2)
4255+
.with_incoming_bundle(IncomingBundle {
4256+
origin: chain_1,
4257+
bundle: MessageBundle {
4258+
certificate_hash: sender_hash,
4259+
height: BlockHeight::ZERO,
4260+
timestamp: Timestamp::from(0),
4261+
transaction_index: 1,
4262+
messages: vec![system_credit_message(Amount::from_tokens(2))
4263+
.to_posted(0, MessageKind::Tracked)],
4264+
},
4265+
action: MessageAction::Accept,
4266+
})
4267+
.into_first_proposal(owner, &signer)
4268+
.await
4269+
.unwrap();
4270+
4271+
let certificate_chain_2 = env.make_certificate(ConfirmedBlock::new(
4272+
BlockExecutionOutcome {
4273+
messages: vec![Vec::new()],
4274+
previous_message_blocks: BTreeMap::new(),
4275+
previous_event_blocks: BTreeMap::new(),
4276+
events: vec![Vec::new()],
4277+
blobs: vec![Vec::new()],
4278+
state_hash: SystemExecutionState {
4279+
balance: Amount::from_tokens(2),
4280+
..env.system_execution_state(&chain_2_desc.id())
4281+
}
4282+
.into_hash()
4283+
.await,
4284+
oracle_responses: vec![Vec::new()],
4285+
operation_results: vec![],
4286+
}
4287+
.with(block_proposal.content.block),
4288+
));
4289+
4290+
env.worker()
4291+
.handle_confirmed_certificate(certificate_chain_2.clone(), None)
4292+
.await?;
4293+
4294+
// Now try to stage a block with the earlier message (transaction_index: 0).
4295+
// This should fail with IncorrectMessageOrder because next_cursor_to_remove
4296+
// is now at index 1, but we're trying to process index 0.
4297+
let bad_proposed_block = make_child_block(&certificate_chain_2.into_value())
4298+
.with_incoming_bundle(IncomingBundle {
4299+
origin: chain_1,
4300+
bundle: MessageBundle {
4301+
certificate_hash: sender_hash,
4302+
height: BlockHeight::ZERO,
4303+
timestamp: Timestamp::from(0),
4304+
transaction_index: 0,
4305+
messages: vec![
4306+
system_credit_message(Amount::ONE).to_posted(0, MessageKind::Tracked)
4307+
],
4308+
},
4309+
action: MessageAction::Accept,
4310+
});
4311+
4312+
// Test stage_block_execution directly - this should fail with IncorrectMessageOrder.
4313+
assert_matches!(
4314+
env.worker()
4315+
.stage_block_execution(bad_proposed_block.clone(), None, vec![])
4316+
.await,
4317+
Err(WorkerError::ChainError(chain_error))
4318+
if matches!(*chain_error, ChainError::IncorrectMessageOrder { .. })
4319+
);
4320+
4321+
// Also test handle_block_proposal for completeness.
4322+
let bad_proposal = bad_proposed_block
4323+
.into_first_proposal(owner, &signer)
4324+
.await
4325+
.unwrap();
4326+
4327+
assert_matches!(
4328+
env.worker().handle_block_proposal(bad_proposal).await,
4329+
Err(WorkerError::ChainError(chain_error))
4330+
if matches!(*chain_error, ChainError::IncorrectMessageOrder { .. })
4331+
);
4332+
4333+
Ok(())
4334+
}

0 commit comments

Comments
 (0)