Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,9 @@ bool WorldStateWrapper::sync_block(msgpack::object& obj, msgpack::sbuffer& buf)
request.value.paddedNoteHashes,
request.value.paddedL1ToL2Messages,
request.value.paddedNullifiers,
request.value.publicDataWrites);
request.value.publicDataWrites,
request.value.expectedArchiveRoot,
request.value.expectedPreviousArchiveRoot);

MsgHeader header(request.header.messageId);
messaging::TypedMessage<WorldStateStatusFull> resp_msg(WorldStateMessageType::SYNC_BLOCK, header, { status });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,17 @@ struct SyncBlockRequest {
block_number_t blockNumber;
StateReference blockStateRef;
bb::fr blockHeaderHash;
bb::fr expectedArchiveRoot;
bb::fr expectedPreviousArchiveRoot;
std::vector<bb::fr> paddedNoteHashes, paddedL1ToL2Messages;
std::vector<crypto::merkle_tree::NullifierLeafValue> paddedNullifiers;
std::vector<crypto::merkle_tree::PublicDataLeafValue> publicDataWrites;

SERIALIZATION_FIELDS(blockNumber,
blockStateRef,
blockHeaderHash,
expectedArchiveRoot,
expectedPreviousArchiveRoot,
paddedNoteHashes,
paddedL1ToL2Messages,
paddedNullifiers,
Expand Down
39 changes: 38 additions & 1 deletion barretenberg/cpp/src/barretenberg/world_state/world_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -602,11 +602,33 @@ WorldStateStatusFull WorldState::sync_block(const StateReference& block_state_re
const std::vector<bb::fr>& notes,
const std::vector<bb::fr>& l1_to_l2_messages,
const std::vector<crypto::merkle_tree::NullifierLeafValue>& nullifiers,
const std::vector<crypto::merkle_tree::PublicDataLeafValue>& public_writes)
const std::vector<crypto::merkle_tree::PublicDataLeafValue>& public_writes,
const std::optional<bb::fr>& expected_archive_root,
const std::optional<bb::fr>& expected_previous_archive_root)
{
validate_trees_are_equally_synched();
rollback();

// The archive tree is an append-only accumulator of block header hashes, so a single bad leaf (e.g. from a
// mishandled reorg) is never self-corrected: every later root stays noncanonical while the other state trees
// can re-converge from block effects. The checks further down only verify the appended leaf is the tip and
// that the four non-archive trees match the block state reference — neither catches a divergent archive root.
// So verify the local archive root against canonical both before appending (the parent root must equal the
// block's lastArchive) and after (the resulting root must equal the block's archive), failing before commit
// so the divergence is never persisted.
if (expected_previous_archive_root.has_value()) {
const bb::fr actual_previous_archive_root =
get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;
if (actual_previous_archive_root != expected_previous_archive_root.value()) {
throw std::runtime_error(
format("Can't sync block: local archive root ",
actual_previous_archive_root,
" does not match the block's previous archive root ",
expected_previous_archive_root.value(),
"; world state has diverged from the canonical chain and must be resynced"));
}
}

Fork::SharedPtr fork = retrieve_fork(CANONICAL_FORK_ID);
Signal signal(static_cast<uint32_t>(fork->_trees.size()));
std::atomic_bool success = true;
Expand Down Expand Up @@ -681,6 +703,21 @@ WorldStateStatusFull WorldState::sync_block(const StateReference& block_state_re
throw std::runtime_error("Can't synch block: block state does not match world state");
}

// The archive tree is not part of the block state reference (see is_same_state_reference), so verify the
// resulting archive root against the canonical block's archive root explicitly.
if (expected_archive_root.has_value()) {
const bb::fr actual_archive_root =
get_tree_info(WorldStateRevision::uncommitted(), MerkleTreeId::ARCHIVE).meta.root;
if (actual_archive_root != expected_archive_root.value()) {
throw std::runtime_error(
format("Can't sync block: resulting archive root ",
actual_archive_root,
" does not match the block's archive root ",
expected_archive_root.value(),
"; world state has diverged from the canonical chain and must be resynced"));
}
}

std::pair<bool, std::string> result = commit(status);
if (!result.first) {
throw std::runtime_error(result.second);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ class WorldState {
const std::vector<bb::fr>& notes,
const std::vector<bb::fr>& l1_to_l2_messages,
const std::vector<crypto::merkle_tree::NullifierLeafValue>& nullifiers,
const std::vector<crypto::merkle_tree::PublicDataLeafValue>& public_writes);
const std::vector<crypto::merkle_tree::PublicDataLeafValue>& public_writes,
const std::optional<bb::fr>& expected_archive_root = std::nullopt,
const std::optional<bb::fr>& expected_previous_archive_root = std::nullopt);

uint32_t checkpoint(const uint64_t& forkId);
void commit_checkpoint(const uint64_t& forkId);
Expand Down
136 changes: 136 additions & 0 deletions barretenberg/cpp/src/barretenberg/world_state/world_state.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,74 @@ TEST_F(WorldStateTest, SyncExternalBlockFromEmpty)
std::runtime_error);
}

TEST_F(WorldStateTest, SyncBlockRejectsDivergentArchiveRoot)
{
StateReference block_state_ref = {
{ MerkleTreeId::NULLIFIER_TREE,
{ fr("0x2e2e2d8b72294a440c728a646f01476624063f0b50dcfe293cc0fc26bef9e311"), 129 } },
{ MerkleTreeId::NOTE_HASH_TREE,
{ fr("0x25c4ef02ba2bec9490376d5b56b8f1a8e5bcf5ecff91636e76660b68c2a9952d"), 1 } },
{ MerkleTreeId::PUBLIC_DATA_TREE,
{ fr("0x1e2d8d1c3ea2449b3e4787d8295df3f137e08b56e891c006b3d93faef56ca3df"), 129 } },
{ MerkleTreeId::L1_TO_L2_MESSAGE_TREE,
{ fr("0x22c6f7877092ecea5b313b22515e31f2e1e37349b787da10eff298800e3c7c0c"), 1 } },
};

// Learn the canonical previous (genesis) and resulting archive roots from an untracked sync.
bb::fr previous_root;
bb::fr resulting_root;
{
WorldState scratch(
thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point);
previous_root = scratch.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;
scratch.sync_block(
block_state_ref, fr(1), { 42 }, { 43 }, { NullifierLeafValue(144) }, { { PublicDataLeafValue(145, 1) } });
resulting_root = scratch.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;
}

std::string data_dir2 = random_temp_directory();
std::filesystem::create_directories(data_dir2);
WorldState ws(thread_pool_size, data_dir2, map_size, tree_heights, tree_prefill, initial_header_generator_point);

// A wrong previous archive root is rejected before any leaves are appended.
EXPECT_THROW(ws.sync_block(block_state_ref,
fr(1),
{ 42 },
{ 43 },
{ NullifierLeafValue(144) },
{ { PublicDataLeafValue(145, 1) } },
resulting_root,
previous_root + fr(1)),
std::runtime_error);

// A wrong resulting archive root is rejected before commit.
EXPECT_THROW(ws.sync_block(block_state_ref,
fr(1),
{ 42 },
{ 43 },
{ NullifierLeafValue(144) },
{ { PublicDataLeafValue(145, 1) } },
resulting_root + fr(1),
previous_root),
std::runtime_error);

// Both rejections rolled back cleanly: world state is still at the genesis archive root.
EXPECT_EQ(ws.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root, previous_root);

// Matching roots are accepted and advance the chain.
WorldStateStatusFull status = ws.sync_block(block_state_ref,
fr(1),
{ 42 },
{ 43 },
{ NullifierLeafValue(144) },
{ { PublicDataLeafValue(145, 1) } },
resulting_root,
previous_root);
WorldStateStatusSummary expected(1, 0, 1, true);
EXPECT_EQ(status.summary, expected);
EXPECT_EQ(ws.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root, resulting_root);
}

TEST_F(WorldStateTest, SyncBlockFromDirtyState)
{
WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point);
Expand Down Expand Up @@ -947,3 +1015,71 @@ TEST_F(WorldStateTest, GetBlockForIndex)
EXPECT_EQ(blockNumbers[0].value(), 1);
}
}

// Demonstrates the bug: syncing an empty block with a bogus block_header_hash succeeds when the optional
// expected-archive-root arguments are omitted. The 4-tree state-ref check passes (nothing changed), but
// the wrong hash is committed to the ARCHIVE, silently diverging from the canonical chain.
TEST_F(WorldStateTest, SyncEmptyBlockAcceptsBogusHashWithoutArchiveCheck)
{
// Learn the canonical previous and resulting archive roots from an empty-block sync on a scratch instance.
bb::fr previous_archive_root;
bb::fr canonical_archive_root;
{
WorldState scratch(
thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point);
previous_archive_root = scratch.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;
StateReference genesis_state_ref = scratch.get_state_reference(WorldStateRevision::committed());
scratch.sync_block(genesis_state_ref, fr(1), {}, {}, {}, {});
canonical_archive_root =
scratch.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;
}

std::string data_dir2 = random_temp_directory();
std::filesystem::create_directories(data_dir2);
WorldState ws(thread_pool_size, data_dir2, map_size, tree_heights, tree_prefill, initial_header_generator_point);

StateReference genesis_state_ref = ws.get_state_reference(WorldStateRevision::committed());

// Sync an empty block using a bogus hash — no expected-archive-root arguments provided.
EXPECT_NO_THROW(ws.sync_block(genesis_state_ref, fr(42), {}, {}, {}, {}));

bb::fr actual_archive_root = ws.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;

// The bogus hash committed successfully, so the resulting archive root differs from the canonical one.
EXPECT_NE(actual_archive_root, canonical_archive_root);
EXPECT_NE(actual_archive_root, previous_archive_root);
}

// Demonstrates the fix: supplying the canonical expected archive roots causes sync_block to reject the bogus
// block_header_hash for an empty block, and rolls back cleanly.
TEST_F(WorldStateTest, SyncEmptyBlockRejectsBogusHashWhenArchiveRootsAreChecked)
{
// Learn the canonical previous and resulting archive roots from an empty-block sync on a scratch instance.
bb::fr previous_archive_root;
bb::fr canonical_archive_root;
{
WorldState scratch(
thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point);
previous_archive_root = scratch.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;
StateReference genesis_state_ref = scratch.get_state_reference(WorldStateRevision::committed());
scratch.sync_block(genesis_state_ref, fr(1), {}, {}, {}, {});
canonical_archive_root =
scratch.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root;
}

std::string data_dir2 = random_temp_directory();
std::filesystem::create_directories(data_dir2);
WorldState ws(thread_pool_size, data_dir2, map_size, tree_heights, tree_prefill, initial_header_generator_point);

StateReference genesis_state_ref = ws.get_state_reference(WorldStateRevision::committed());

// Sync an empty block using a bogus hash, but supply the canonical archive roots for validation.
// The resulting archive root will not match canonical_archive_root, so this must throw.
EXPECT_THROW(
ws.sync_block(genesis_state_ref, fr(42), {}, {}, {}, {}, canonical_archive_root, previous_archive_root),
std::runtime_error);

// The committed archive root must be unchanged (rollback was clean).
EXPECT_EQ(ws.get_tree_info(WorldStateRevision::committed(), MerkleTreeId::ARCHIVE).meta.root,
previous_archive_root);
}
4 changes: 4 additions & 0 deletions barretenberg/cpp/src/barretenberg/wsdb/wsdb_commands.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ struct WsdbSyncBlock {
block_number_t blockNumber;
StateReference blockStateRef;
bb::fr blockHeaderHash;
bb::fr expectedArchiveRoot;
bb::fr expectedPreviousArchiveRoot;
std::vector<bb::fr> paddedNoteHashes;
std::vector<bb::fr> paddedL1ToL2Messages;
std::vector<NullifierLeafValue> paddedNullifiers;
Expand All @@ -328,6 +330,8 @@ struct WsdbSyncBlock {
SERIALIZATION_FIELDS(blockNumber,
blockStateRef,
blockHeaderHash,
expectedArchiveRoot,
expectedPreviousArchiveRoot,
paddedNoteHashes,
paddedL1ToL2Messages,
paddedNullifiers,
Expand Down
10 changes: 8 additions & 2 deletions barretenberg/cpp/src/barretenberg/wsdb/wsdb_execute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,14 @@ WsdbRollback::Response WsdbRollback::execute(WsdbRequest& request) &&

WsdbSyncBlock::Response WsdbSyncBlock::execute(WsdbRequest& request) &&
{
WorldStateStatusFull status = request.world_state.sync_block(
blockStateRef, blockHeaderHash, paddedNoteHashes, paddedL1ToL2Messages, paddedNullifiers, publicDataWrites);
WorldStateStatusFull status = request.world_state.sync_block(blockStateRef,
blockHeaderHash,
paddedNoteHashes,
paddedL1ToL2Messages,
paddedNullifiers,
publicDataWrites,
expectedArchiveRoot,
expectedPreviousArchiveRoot);
return Response{ .status = status };
}

Expand Down
2 changes: 2 additions & 0 deletions spartan/aztec-node/templates/_pod-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ spec:
value: "{{ .Values.node.proverRealProofs }}"
- name: SENTINEL_ENABLED
value: "{{ .Values.node.sentinel.enabled }}"
- name: OFFENSE_COLLECTION_ENABLED
value: "{{ .Values.node.offenseCollection.enabled }}"
{{- if .Values.node.slash.validatorsAlways }}
- name: SLASH_VALIDATORS_ALWAYS
value: {{ join "," .Values.node.slash.validatorsAlways | quote }}
Expand Down
2 changes: 2 additions & 0 deletions spartan/aztec-node/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ node:

sentinel:
enabled: true
offenseCollection:
enabled: true
slash:
# Validator allowlists/denylists
validatorsAlways: []
Expand Down
1 change: 1 addition & 0 deletions spartan/environments/block-capacity.env
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ REDEPLOY_ROLLUP_CONTRACTS=true
ETHEREUM_CHAIN_ID=1337
LABS_INFRA_MNEMONIC="test test test test test test test test test test test junk"
FUNDING_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
SPONSORED_FPC=true

OTEL_COLLECTOR_ENDPOINT=REPLACE_WITH_GCP_SECRET

Expand Down
2 changes: 2 additions & 0 deletions spartan/environments/network-defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ _prodlike: &prodlike
#---------------------------------------------------------------------------
# Enable sentinel monitoring.
SENTINEL_ENABLED: true
# Enable offense collection (watchers + read-only slasher) on non-validator nodes.
OFFENSE_COLLECTION_ENABLED: true

# Network presets selected via NETWORK env var; individual values can still be overridden.
networks:
Expand Down
1 change: 1 addition & 0 deletions spartan/scripts/deploy_network.sh
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ PROVER_MNEMONIC = "${LABS_INFRA_MNEMONIC}"
PROVER_PUBLISHER_MNEMONIC_START_INDEX = ${PROVER_PUBLISHER_MNEMONIC_START_INDEX}
PROVER_PUBLISHERS_PER_PROVER = ${PUBLISHERS_PER_PROVER}
SENTINEL_ENABLED = ${SENTINEL_ENABLED:-null}
OFFENSE_COLLECTION_ENABLED = ${OFFENSE_COLLECTION_ENABLED:-null}
SLASH_INACTIVITY_TARGET_PERCENTAGE = ${SLASH_INACTIVITY_TARGET_PERCENTAGE:-null}
SLASH_INACTIVITY_PENALTY = ${SLASH_INACTIVITY_PENALTY:-null}
SLASH_DATA_WITHHOLDING_PENALTY = ${SLASH_DATA_WITHHOLDING_PENALTY:-null}
Expand Down
1 change: 1 addition & 0 deletions spartan/terraform/deploy-aztec-infra/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ locals {
"validator.publisherMnemonicStartIndex" = var.VALIDATOR_PUBLISHER_MNEMONIC_START_INDEX
"validator.node.env.COINBASE" = var.VALIDATOR_COINBASE
"validator.sentinel.enabled" = var.SENTINEL_ENABLED
"validator.offenseCollection.enabled" = var.OFFENSE_COLLECTION_ENABLED
"validator.slash.inactivityTargetPercentage" = var.SLASH_INACTIVITY_TARGET_PERCENTAGE
"validator.slash.inactivityPenalty" = var.SLASH_INACTIVITY_PENALTY
"validator.slash.dataWithholdingPenalty" = var.SLASH_DATA_WITHHOLDING_PENALTY
Expand Down
6 changes: 6 additions & 0 deletions spartan/terraform/deploy-aztec-infra/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,12 @@ variable "SENTINEL_ENABLED" {
default = true
}

variable "OFFENSE_COLLECTION_ENABLED" {
description = "Whether to enable offense collection (watchers + read-only slasher) on non-validator nodes"
type = string
default = true
}

variable "SLASH_INACTIVITY_TARGET_PERCENTAGE" {
description = "The slash inactivity target percentage"
type = string
Expand Down
Loading
Loading