Skip to content

Commit 6c87f81

Browse files
authored
When staging, verify the inboxes. (#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 backported to `testnet_conway`, then - be released in a new SDK. ## Links - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 0a0ba6b commit 6c87f81

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
@@ -1349,6 +1349,9 @@ where
13491349
let (_, committee) = self.chain.current_committee()?;
13501350
block.check_proposal_size(committee.policy().maximum_block_proposal_size)?;
13511351

1352+
self.chain
1353+
.remove_bundles_from_inboxes(block.timestamp, true, block.incoming_bundles())
1354+
.await?;
13521355
let executed_block =
13531356
Box::pin(self.execute_block(&block, local_time, round, published_blobs)).await?;
13541357

linera-core/src/unit_tests/worker_tests.rs

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

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

0 commit comments

Comments
 (0)