Merge remote-tracking branch 'origin/moudy/cross-zone-peer-cache-bound' into moudy/cross-zone-halt-operability

This commit is contained in:
moudyellaz
2026-08-19 21:13:18 +02:00
16 changed files with 262 additions and 114 deletions
Generated
+12 -12
View File
@@ -2330,7 +2330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
dependencies = [
"data-encoding",
"syn 2.0.117",
"syn 1.0.109",
]
[[package]]
@@ -2516,7 +2516,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -2884,7 +2884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -3497,9 +3497,9 @@ checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
[[package]]
name = "h2"
version = "0.4.14"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
dependencies = [
"atomic-waker",
"bytes",
@@ -3967,7 +3967,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.4",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -7206,7 +7206,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -8255,7 +8255,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.4",
"socket2 0.5.10",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -8292,7 +8292,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.4",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
]
@@ -9250,7 +9250,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -10487,7 +10487,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -11934,7 +11934,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -57,7 +57,7 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
src_program_id: programs::bridge_lock().id(),
target_program_id: wrapped_token_id,
}],
expected_block_signing_pubkey: None,
expected_block_signing_pubkeys: Vec::new(),
}],
};
+1 -1
View File
@@ -51,7 +51,7 @@ async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
src_program_id: programs::ping_sender().id(),
target_program_id: receiver_id,
}],
expected_block_signing_pubkey: None,
expected_block_signing_pubkeys: Vec::new(),
}],
};
@@ -50,7 +50,7 @@ async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
src_program_id: programs::ping_sender().id(),
target_program_id: receiver_id,
}],
expected_block_signing_pubkey: None,
expected_block_signing_pubkeys: Vec::new(),
}],
};
@@ -62,7 +62,7 @@ async fn restarted_watcher_resumes_instead_of_replaying_the_peer_channel() -> Re
src_program_id: programs::ping_sender().id(),
target_program_id: receiver_id,
}],
expected_block_signing_pubkey: None,
expected_block_signing_pubkeys: Vec::new(),
}],
};
+64 -13
View File
@@ -11,7 +11,7 @@ use common::{
HashType,
block::{Block, PeerChainTip},
};
use cross_zone_inbox_core::ZoneId;
use cross_zone_inbox_core::{CrossZonePeer, ZoneId};
use lee::{GENESIS_BLOCK_ID, PublicKey};
/// Consecutive passes a reader spends stuck on one slot before it says so as
@@ -84,9 +84,9 @@ impl Display for OffChain {
/// The channel authorizes who may write, not what they may claim. The hash
/// check is unconditional: the signature does not cover `header.hash`, and the
/// chain link compares hashes, so an unchecked one lets a peer assert links it
/// never built. The pinned key, checked only when one is configured, is what
/// says the peer's own sequencer produced the block; it subsumes nothing here,
/// since a correctly signed block may still carry a bogus hash.
/// never built. The pinned keys, checked only when any are configured, are what
/// say one of the peer's own sequencers produced the block; they subsume
/// nothing here, since a correctly signed block may still carry a bogus hash.
#[derive(Debug, PartialEq, Eq)]
pub enum ScreenRefusal {
/// `header.hash` is not the hash of the block's contents.
@@ -95,7 +95,7 @@ pub enum ScreenRefusal {
declared: HashType,
recomputed: HashType,
},
/// The block is not signed by the pinned block-signing key.
/// The block is not signed by any pinned block-signing key.
KeyMismatch { block_id: u64 },
}
@@ -112,7 +112,7 @@ impl Display for ScreenRefusal {
),
Self::KeyMismatch { block_id } => write!(
f,
"block {block_id} is not signed by the pinned block-signing key"
"block {block_id} is not signed by any pinned block-signing key"
),
}
}
@@ -244,7 +244,7 @@ pub fn link_to_tip(tip: Option<&PeerChainTip>, block: &Block, recomputed: HashTy
/// all, returning the recomputed hash every later placement has to use.
pub fn screen_peer_block(
block: &Block,
expected_pubkey: Option<&PublicKey>,
expected_pubkeys: &[PublicKey],
) -> Result<HashType, ScreenRefusal> {
let recomputed = block.recompute_hash();
if recomputed != block.header.hash {
@@ -254,7 +254,7 @@ pub fn screen_peer_block(
recomputed,
});
}
if expected_pubkey.is_some_and(|key| !block.is_signed_by(key)) {
if !signed_by_any(block, expected_pubkeys) {
return Err(ScreenRefusal::KeyMismatch {
block_id: block.header.block_id,
});
@@ -262,6 +262,29 @@ pub fn screen_peer_block(
Ok(recomputed)
}
/// Whether `block` is signed by one of the peer's pinned keys.
///
/// Vacuously true with none pinned: the check is opt-in, and every place that
/// judges a peer block's signature goes through here so they cannot diverge on
/// that rule.
#[must_use]
pub fn signed_by_any(block: &Block, pinned: &[PublicKey]) -> bool {
pinned.is_empty() || pinned.iter().any(|key| block.is_signed_by(key))
}
/// The peer's configured block-signing keys, parsed. Panics on a malformed
/// entry: both sides read this at startup, so a bad config byte fails the
/// process before it judges any block.
#[must_use]
pub fn pinned_keys(peer: &CrossZonePeer) -> Vec<PublicKey> {
peer.expected_block_signing_pubkeys
.iter()
.map(|&bytes| {
PublicKey::try_new(bytes).expect("configured peer block-signing pubkey is a valid key")
})
.collect()
}
#[cfg(test)]
mod tests {
use common::test_utils::produce_dummy_block;
@@ -290,7 +313,7 @@ mod tests {
}
fn screened(block: &Block) -> HashType {
screen_peer_block(block, None).expect("honest block passes screening")
screen_peer_block(block, &[]).expect("honest block passes screening")
}
/// Runs the stall machine over `(stuck_on, read_to)` pass results.
@@ -389,14 +412,14 @@ mod tests {
let mut tampered = chain_block(3);
tampered.header.hash = HashType([9; 32]);
assert!(matches!(
screen_peer_block(&tampered, None),
screen_peer_block(&tampered, &[]),
Err(ScreenRefusal::HashMismatch { block_id: 3, .. })
));
// And the hash verdict comes first, whatever else is wrong.
let other = lee::PublicKey::try_new([42; 32]).expect("test key");
assert!(matches!(
screen_peer_block(&tampered, Some(&other)),
screen_peer_block(&tampered, &[other]),
Err(ScreenRefusal::HashMismatch { .. })
));
}
@@ -408,14 +431,42 @@ mod tests {
);
let block = chain_block(GENESIS_BLOCK_ID);
assert_eq!(
screen_peer_block(&block, Some(&signer)),
screen_peer_block(&block, &[signer]),
Ok(chain_hash(GENESIS_BLOCK_ID)),
"produce_dummy_block signs with this key, so the pin must accept it"
);
let other = lee::PublicKey::try_new([42; 32]).expect("test key");
assert!(matches!(
screen_peer_block(&block, Some(&other)),
screen_peer_block(&block, &[other]),
Err(ScreenRefusal::KeyMismatch {
block_id: GENESIS_BLOCK_ID
})
));
}
#[test]
fn a_block_signed_by_any_key_in_the_set_is_accepted() {
// The multi-sequencer peer shape: the block's signer is one configured
// key among several, not the first one listed.
let signer = lee::PublicKey::new_from_private_key(
&lee::PrivateKey::try_new([37; 32]).expect("test key"),
);
let other = lee::PublicKey::try_new([42; 32]).expect("test key");
let block = chain_block(GENESIS_BLOCK_ID);
assert_eq!(
screen_peer_block(&block, &[other.clone(), signer]),
Ok(chain_hash(GENESIS_BLOCK_ID)),
"any listed key admits the block, whatever its position"
);
// A set none of whose keys signed refuses, exactly like a wrong single
// key.
let third = lee::PublicKey::new_from_private_key(
&lee::PrivateKey::try_new([99; 32]).expect("test key"),
);
assert!(matches!(
screen_peer_block(&block, &[other, third]),
Err(ScreenRefusal::KeyMismatch {
block_id: GENESIS_BLOCK_ID
})
+1 -1
View File
@@ -11,7 +11,7 @@
pub use acceptance::{
Link, OffChain, STUCK_SLOT_ALERT_PASSES, ScreenRefusal, StallState, alerts_at,
equivocation_report, link_to_tip, screen_peer_block,
equivocation_report, link_to_tip, pinned_keys, screen_peer_block, signed_by_any,
};
pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer};
use cross_zone_inbox_core::{
+99 -67
View File
@@ -17,7 +17,8 @@ use common::{
};
use cross_zone::{
EmissionSource, Link, OffChain, StallState, alerts_at, build_dispatch_from_emission,
equivocation_report, extract_emission, link_to_tip, screen_peer_block,
equivocation_report, extract_emission, link_to_tip, pinned_keys, screen_peer_block,
signed_by_any,
};
use cross_zone_inbox_core::{
CrossZoneMessage, Instruction as InboxInstruction, MessageKey, ZoneId, message_key,
@@ -623,12 +624,13 @@ struct TipEvidence {
#[derive(Clone)]
pub struct CrossZoneVerifier {
self_zone: ZoneId,
/// Pinned block-signing key per peer zone, enforced during re-derivation.
/// One key per peer is sufficient while a zone has a single sequencer; key
/// sets with rotation come in with decentralized sequencing. The pin is
/// largely redundant given Bedrock's turn-based write authorization, so it is
/// optional: a peer with no configured key is not signature-checked.
peer_pubkeys: HashMap<ZoneId, PublicKey>,
/// Pinned block-signing keys per peer zone, enforced during re-derivation:
/// a block is acceptable when signed by any of them, one entry per peer
/// sequencer. Rotation rules come later with decentralized sequencing. The
/// pin is largely redundant given Bedrock's turn-based write authorization,
/// so it is optional: a peer with no configured keys is not
/// signature-checked.
peer_pubkeys: HashMap<ZoneId, Vec<PublicKey>>,
peers: PeerBlocks,
/// One channel client per peer, used only for the one-shot refetch of an
/// evicted body. Built exactly like the reader's own client.
@@ -666,17 +668,14 @@ impl CrossZoneVerifier {
node.clone(),
)),
);
if let Some(bytes) = peer.expected_block_signing_pubkey {
let pubkey = PublicKey::try_new(bytes)
.expect("configured peer block-signing pubkey is a valid key");
peer_pubkeys.insert(peer.channel_id, pubkey);
}
let expected_pubkeys = pinned_keys(peer);
peer_pubkeys.insert(peer.channel_id, expected_pubkeys.clone());
watch.register(peer.channel_id);
tokio::spawn(read_peer(
ZoneIndexer::new(ChannelId::from(peer.channel_id), node),
tip_node,
peer.channel_id,
peer_pubkeys.get(&peer.channel_id).cloned(),
expected_pubkeys,
peers.clone(),
watch.clone(),
config.consensus_info_polling_interval,
@@ -832,14 +831,12 @@ impl CrossZoneVerifier {
},
})?;
// Equivocation defense: the source block must be signed by the peer's
// pinned block-signing key, not merely inscribed on the channel.
if let Some(expected) = self.peer_pubkeys.get(&msg.src_zone)
&& !peer_block.is_signed_by(expected)
{
// Equivocation defense: the source block must be signed by one of the
// peer's pinned block-signing keys, not merely inscribed on the channel.
if !signed_by_any(&peer_block, self.pinned_for(msg.src_zone)) {
return Err(forged(
msg,
"peer block is not signed by the pinned block-signing key".to_owned(),
"peer block is not signed by any pinned block-signing key".to_owned(),
));
}
@@ -1013,14 +1010,12 @@ impl CrossZoneVerifier {
.next_messages(cursor)
.await
.map_err(|err| anyhow!("channel read failed: {err}"))?;
refetched_block(
stream,
read_at,
block_id,
block_hash,
self.peer_pubkeys.get(&zone),
)
.await
refetched_block(stream, read_at, block_id, block_hash, self.pinned_for(zone)).await
}
/// The keys pinned for `zone`, empty when none are configured.
fn pinned_for(&self, zone: ZoneId) -> &[PublicKey] {
self.peer_pubkeys.get(&zone).map_or(&[], Vec::as_slice)
}
}
@@ -1095,12 +1090,8 @@ fn seen_key(msg: &CrossZoneMessage) -> SeenKey {
/// says why each check exists.
///
/// [`ScreenRefusal`]: cross_zone::ScreenRefusal
fn accept_peer_block(
block: &Block,
peer_zone: ZoneId,
expected_pubkey: Option<&PublicKey>,
) -> bool {
match screen_peer_block(block, expected_pubkey) {
fn accept_peer_block(block: &Block, peer_zone: ZoneId, expected_pubkeys: &[PublicKey]) -> bool {
match screen_peer_block(block, expected_pubkeys) {
Ok(_) => true,
Err(refusal) => {
warn!(
@@ -1122,7 +1113,7 @@ async fn read_peer(
zone_indexer: ZoneIndexer<NodeHttpClient>,
tip_node: NodeHttpClient,
peer_zone: ZoneId,
expected_pubkey: Option<PublicKey>,
expected_pubkeys: Vec<PublicKey>,
peers: PeerBlocks,
watch: PeerWatch,
poll_interval: Duration,
@@ -1139,14 +1130,8 @@ async fn read_peer(
loop {
match zone_indexer.next_messages(cursor).await {
Ok(stream) => {
let pass = consume_peer_stream(
stream,
peer_zone,
expected_pubkey.as_ref(),
&peers,
cursor,
)
.await;
let pass =
consume_peer_stream(stream, peer_zone, &expected_pubkeys, &peers, cursor).await;
cursor = pass.cursor;
if let Some((slot, attempts)) = stall.after_pass(pass.stalled_at, pass.cursor)
&& alerts_at(attempts)
@@ -1212,7 +1197,7 @@ async fn read_peer(
async fn consume_peer_stream<S>(
stream: S,
peer_zone: ZoneId,
expected_pubkey: Option<&PublicKey>,
expected_pubkeys: &[PublicKey],
peers: &PeerBlocks,
resume_from: Option<Slot>,
) -> PeerPass
@@ -1237,7 +1222,7 @@ where
Ok(block) => {
// Before caching, not when a dispatch names it: an unchecked
// block steers the prefix, and by then the damage is a halt.
if accept_peer_block(&block, peer_zone, expected_pubkey) {
if accept_peer_block(&block, peer_zone, expected_pubkeys) {
peers.insert(peer_zone, block, slot).await;
}
}
@@ -1270,8 +1255,8 @@ where
/// hashing differently is reported by name, an endpoint fault per the gate in
/// [`CrossZoneVerifier::wait_for_peer_block`].
///
/// When `pinned` holds the zone's block-signing key, a candidate that is not
/// signed by it is skipped like a wrong id. The hash preimage excludes the
/// When `pinned` holds the zone's block-signing keys, a candidate signed by
/// none of them is skipped like a wrong id. The hash preimage excludes the
/// signature, so a same-content twin with a corrupted signature would pass the
/// hash check here only to halt ingestion as Forged at [`rederive`]'s
/// pinned-key gate; skipping it lets the honest copy later in the slot match,
@@ -1283,7 +1268,7 @@ async fn refetched_block<S>(
read_at: Slot,
block_id: u64,
block_hash: HashType,
pinned: Option<&PublicKey>,
pinned: &[PublicKey],
) -> anyhow::Result<Block>
where
S: Stream<Item = (ZoneMessage, Slot)>,
@@ -1306,7 +1291,7 @@ where
if block.header.block_id != block_id {
continue;
}
if pinned.is_some_and(|key| !block.is_signed_by(key)) {
if !signed_by_any(&block, pinned) {
continue;
}
let recomputed = block.recompute_hash();
@@ -1353,7 +1338,9 @@ mod tests {
verifier_with_pinned_keys(HashMap::new())
}
fn verifier_with_pinned_keys(peer_pubkeys: HashMap<ZoneId, PublicKey>) -> CrossZoneVerifier {
fn verifier_with_pinned_keys(
peer_pubkeys: HashMap<ZoneId, Vec<PublicKey>>,
) -> CrossZoneVerifier {
CrossZoneVerifier {
self_zone: SELF_ZONE,
peer_pubkeys,
@@ -1529,7 +1516,7 @@ mod tests {
// produce_dummy_block signs with PrivateKey([37; 32]); pin its pubkey.
let signer = PublicKey::new_from_private_key(&PrivateKey::try_new([37; 32]).unwrap());
let mut keys = HashMap::new();
keys.insert(PEER_ZONE, signer);
keys.insert(PEER_ZONE, vec![signer]);
let verifier = verifier_with_pinned_keys(keys);
cache_chain(&verifier, peer_chain(b"hi")).await;
@@ -1544,7 +1531,52 @@ mod tests {
async fn rejects_dispatch_from_a_block_not_signed_by_the_pinned_key() {
// Pin a different key than the one that signed the peer block.
let mut keys = HashMap::new();
keys.insert(PEER_ZONE, PublicKey::try_new([42; 32]).unwrap());
keys.insert(PEER_ZONE, vec![PublicKey::try_new([42; 32]).unwrap()]);
let verifier = verifier_with_pinned_keys(keys);
cache_chain(&verifier, peer_chain(b"hi")).await;
let block = produce_dummy_block(9, None, vec![dispatch(b"hi")]);
let err = verifier
.verify_block(&block, Slot::from(0))
.await
.unwrap_err();
assert!(
err.to_string().contains("pinned"),
"unexpected error: {err}"
);
}
#[tokio::test]
async fn verifies_dispatch_signed_by_any_key_in_the_pinned_set() {
// The multi-sequencer peer shape: the block's signer is one configured
// key among several, not the first one listed.
let signer = PublicKey::new_from_private_key(&PrivateKey::try_new([37; 32]).unwrap());
let mut keys = HashMap::new();
keys.insert(
PEER_ZONE,
vec![PublicKey::try_new([42; 32]).unwrap(), signer],
);
let verifier = verifier_with_pinned_keys(keys);
cache_chain(&verifier, peer_chain(b"hi")).await;
let block = produce_dummy_block(9, None, vec![dispatch(b"hi")]);
verifier
.verify_block(&block, Slot::from(0))
.await
.expect("any listed key admits the source block");
}
#[tokio::test]
async fn rejects_dispatch_from_a_block_signed_by_no_key_in_the_set() {
// Neither pinned key signed the peer block.
let mut keys = HashMap::new();
keys.insert(
PEER_ZONE,
vec![
PublicKey::try_new([42; 32]).unwrap(),
PublicKey::new_from_private_key(&PrivateKey::try_new([99; 32]).unwrap()),
],
);
let verifier = verifier_with_pinned_keys(keys);
cache_chain(&verifier, peer_chain(b"hi")).await;
@@ -1661,7 +1693,7 @@ mod tests {
peer_block_msg(&chain[1], 1),
]);
let pass = consume_peer_stream(stream, PEER_ZONE, None, &peers, None).await;
let pass = consume_peer_stream(stream, PEER_ZONE, &[], &peers, None).await;
assert_eq!(pass.cursor, Some(Slot::from(1)));
assert_eq!(pass.stalled_at, None);
@@ -1754,7 +1786,7 @@ mod tests {
peer_block_msg(&chain[2], 2),
]);
let pass = consume_peer_stream(stream, PEER_ZONE, None, &peers, None).await;
let pass = consume_peer_stream(stream, PEER_ZONE, &[], &peers, None).await;
assert_eq!(pass.cursor, Some(Slot::from(0)));
assert_eq!(pass.stalled_at, Some(Slot::from(1)));
@@ -1769,7 +1801,7 @@ mod tests {
// One slot can carry several messages; the second one fails.
let stream = stream::iter(vec![peer_block_msg(&chain[0], 7), undecodable_msg(7)]);
let pass = consume_peer_stream(stream, PEER_ZONE, None, &peers, Some(Slot::from(6))).await;
let pass = consume_peer_stream(stream, PEER_ZONE, &[], &peers, Some(Slot::from(6))).await;
// Slot 7 is re-read whole next pass, not resumed past the failure.
assert_eq!(pass.cursor, Some(Slot::from(6)));
@@ -1788,7 +1820,7 @@ mod tests {
peer_block_msg(&chain[2], 2),
]);
let pass = consume_peer_stream(stream, PEER_ZONE, None, &peers, None).await;
let pass = consume_peer_stream(stream, PEER_ZONE, &[], &peers, None).await;
assert_eq!(pass.cursor, Some(Slot::from(0)), "the slot is held");
assert_eq!(pass.stalled_at, Some(Slot::from(1)));
@@ -1808,7 +1840,7 @@ mod tests {
undecodable_msg(1),
peer_block_msg(&chain[2], 2),
]);
consume_peer_stream(stream, PEER_ZONE, None, &verifier.peers, None).await;
consume_peer_stream(stream, PEER_ZONE, &[], &verifier.peers, None).await;
// Regression: block 2 used to be reported as forged, halting ingestion
// permanently, because a `max(cached ids)` high-water mark counted
@@ -1894,7 +1926,7 @@ mod tests {
let pass = consume_peer_stream(
stream::iter(vec![undecodable_msg(0)]),
PEER_ZONE,
None,
&[],
&verifier.peers,
None,
)
@@ -1908,7 +1940,7 @@ mod tests {
peer_block_msg(block, u64::try_from(index).expect("test index fits in u64"))
})),
PEER_ZONE,
None,
&[],
&verifier.peers,
pass.cursor,
)
@@ -2151,7 +2183,7 @@ mod tests {
let pass = consume_peer_stream(
stream::iter(vec![peer_block_msg(&tampered, 0)]),
PEER_ZONE,
None,
&[],
&verifier.peers,
None,
)
@@ -2178,7 +2210,7 @@ mod tests {
let pass = consume_peer_stream(
stream::iter(vec![peer_block_msg(&block, 0)]),
PEER_ZONE,
Some(&wrong_key),
&[wrong_key],
&verifier.peers,
None,
)
@@ -2196,7 +2228,7 @@ mod tests {
consume_peer_stream(
stream::iter(vec![peer_block_msg(&block, 1)]),
PEER_ZONE,
Some(&signer),
&[signer],
&verifier.peers,
None,
)
@@ -2520,7 +2552,7 @@ mod tests {
peer_block_msg(target, 4),
]);
let block = refetched_block(stream, Slot::from(4), 2, target.header.hash, None)
let block = refetched_block(stream, Slot::from(4), 2, target.header.hash, &[])
.await
.expect("the recorded slot serves the block");
assert_eq!(block.header.hash, target.header.hash);
@@ -2535,7 +2567,7 @@ mod tests {
tampered.header.hash = HashType([0xAB; 32]);
let stream = stream::iter(vec![peer_block_msg(&tampered, 4)]);
let block = refetched_block(stream, Slot::from(4), GENESIS_BLOCK_ID, certified, None)
let block = refetched_block(stream, Slot::from(4), GENESIS_BLOCK_ID, certified, &[])
.await
.expect("the recomputed hash matches the index");
assert_eq!(
@@ -2602,7 +2634,7 @@ mod tests {
let served = produce_dummy_block(2, Some(chain[0].header.hash), vec![emission(b"other")]);
let stream = stream::iter(vec![peer_block_msg(&served, 4)]);
let err = refetched_block(stream, Slot::from(4), 2, chain[1].header.hash, None)
let err = refetched_block(stream, Slot::from(4), 2, chain[1].header.hash, &[])
.await
.expect_err("different bytes than the walk saw must be refused");
assert!(
@@ -2626,7 +2658,7 @@ mod tests {
Slot::from(4),
GENESIS_BLOCK_ID,
honest.header.hash,
Some(&signer),
std::slice::from_ref(&signer),
)
.await
.expect("the honest copy later in the slot matches");
@@ -2650,7 +2682,7 @@ mod tests {
Slot::from(4),
GENESIS_BLOCK_ID,
honest.header.hash,
Some(&signer),
std::slice::from_ref(&signer),
)
.await
.expect_err("a slot with no validly signed copy has no block to return");
@@ -2667,7 +2699,7 @@ mod tests {
Slot::from(4),
2,
HashType([1; 32]),
None,
&[],
)
.await
.expect_err("nothing at the slot decodes to the block");
+10 -7
View File
@@ -18,9 +18,6 @@ const SOURCE_MARKER_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneSource/00000/"
/// Raw 32-byte zone (channel) id; the host maps it to the zone-sdk `ChannelId`.
pub type ZoneId = [u8; 32];
/// Block-signing public key pinned per peer zone.
pub type ExpectedPubkey = [u8; 32];
/// Content-addressed replay key for a delivered message.
pub type MessageKey = [u8; 32];
@@ -41,24 +38,30 @@ pub struct CrossZoneRoute {
}
/// A peer zone whose outbox a zone watches for inbound cross-zone messages.
///
/// Unknown fields are refused so a stale or misspelled key in an operator
/// config fails startup instead of silently pinning nothing.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CrossZonePeer {
/// The peer's Bedrock channel; its 32 bytes double as the peer's zone id.
pub channel_id: ZoneId,
/// The deliveries this peer may make: which of its programs may emit, and
/// what each of them may reach here.
pub allowed_routes: Vec<CrossZoneRoute>,
/// The peer's block-signing public key, pinned to reject blocks inscribed by
/// anyone other than that zone's sequencer. `None` skips the check (the
/// channel signer is still authenticated by the zone-sdk).
/// The peer's block-signing public keys, pinned to reject blocks inscribed
/// by anyone other than that zone's sequencers: a block is acceptable when
/// signed by any of them, one entry per sequencer. Empty skips the check
/// (the channel signer is still authenticated by the zone-sdk).
#[serde(default)]
pub expected_block_signing_pubkey: Option<[u8; 32]>,
pub expected_block_signing_pubkeys: Vec<[u8; 32]>,
}
/// Cross-zone configuration shared by a zone's sequencer (watcher) and indexer
/// (verifier): the peers it reads from Bedrock and, per peer, the local programs
/// they may deliver to.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CrossZoneConfig {
pub peers: Vec<CrossZonePeer>,
}
+70 -8
View File
@@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration};
use common::{HashType, block::Block, transaction::LeeTransaction};
use cross_zone::{
EmissionSource, Link, StallState, alerts_at, build_dispatch_from_emission, equivocation_report,
extract_emission, is_sequencer_only_program, link_to_tip, screen_peer_block,
extract_emission, is_sequencer_only_program, link_to_tip, pinned_keys, screen_peer_block,
};
use cross_zone_inbox_core::message_key;
use futures::{Stream, StreamExt as _};
@@ -30,7 +30,7 @@ use crate::{
struct PeerContext {
peer_zone: [u8; 32],
self_zone: [u8; 32],
expected_pubkey: Option<PublicKey>,
expected_pubkeys: Vec<PublicKey>,
}
/// Why one pass over a peer's stream ended.
@@ -147,15 +147,13 @@ pub fn spawn_watchers(
CommonHttpClient::new(bedrock_config.auth.clone().map(Into::into)),
bedrock_config.node_url.clone(),
);
let expected_pubkey = peer.expected_block_signing_pubkey.map(|bytes| {
PublicKey::try_new(bytes).expect("configured peer block-signing pubkey is a valid key")
});
let expected_pubkeys = pinned_keys(&peer);
tasks.push(tokio::spawn(watch_peer(
ZoneIndexer::new(ChannelId::from(peer.channel_id), node),
PeerContext {
peer_zone: peer.channel_id,
self_zone,
expected_pubkey,
expected_pubkeys,
},
poll_interval,
Arc::clone(dbio),
@@ -332,7 +330,7 @@ where
// block this watcher cannot place is read past rather than
// treated as the end of the chain: the peer's own next honest
// block still links to the tip.
let link = match screen_peer_block(&block, peer.expected_pubkey.as_ref()) {
let link = match screen_peer_block(&block, &peer.expected_pubkeys) {
Ok(recomputed) => link_to_tip(tip.as_ref(), &block, recomputed),
Err(refusal) => {
skipped = skipped.saturating_add(1);
@@ -577,7 +575,7 @@ mod tests {
PeerContext {
peer_zone: PEER_ZONE,
self_zone: SELF_ZONE,
expected_pubkey: None,
expected_pubkeys: Vec::new(),
}
}
@@ -1229,6 +1227,70 @@ mod tests {
assert_eq!(tip, Some(tip_at(2)));
}
#[tokio::test]
async fn watcher_delivers_from_a_block_signed_by_any_pinned_key() {
// The multi-sequencer peer shape: the block's signer is one configured
// key among several, not the first one listed.
let (_dir, dbio) = store();
let mut cursor = None;
let mut tip = None;
let signer =
PublicKey::new_from_private_key(&lee::PrivateKey::try_new([37; 32]).expect("test key"));
let peer = PeerContext {
expected_pubkeys: vec![PublicKey::try_new([42; 32]).expect("test key"), signer],
..peer_context()
};
let outcome = consume_peer_stream(
stream::iter(vec![peer_block_msg(1, 0)]),
&peer,
&dbio,
&mut cursor,
&mut tip,
)
.await;
assert_eq!(outcome, PassOutcome::Drained);
assert_eq!(
recorded_keys(&dbio),
vec![message_key(&PEER_ZONE, 1, 0)],
"any listed key admits the block, whatever its position"
);
assert_eq!(tip, Some(tip_at(1)));
}
#[tokio::test]
async fn watcher_skips_a_block_signed_by_no_pinned_key() {
let (_dir, dbio) = store();
let mut cursor = None;
let mut tip = None;
let peer = PeerContext {
expected_pubkeys: vec![
PublicKey::try_new([42; 32]).expect("test key"),
PublicKey::new_from_private_key(
&lee::PrivateKey::try_new([99; 32]).expect("test key"),
),
],
..peer_context()
};
let outcome = consume_peer_stream(
stream::iter(vec![peer_block_msg(1, 0)]),
&peer,
&dbio,
&mut cursor,
&mut tip,
)
.await;
assert_eq!(outcome, PassOutcome::Stranded);
assert!(
recorded_keys(&dbio).is_empty(),
"a block signed by none of the pinned keys is never delivered from"
);
assert_eq!(tip, None, "a screened-out block does not advance the tip");
}
#[tokio::test]
async fn a_watcher_with_no_tip_delivers_nothing_below_the_peers_genesis() {
// A fresh watcher handed a mid-chain block has nothing to link it
+1 -1
View File
@@ -183,7 +183,7 @@ fn cross_zone_test_config() -> SequencerConfig {
src_program_id: programs::ping_sender().id(),
target_program_id: programs::ping_receiver().id(),
}],
expected_block_signing_pubkey: None,
expected_block_signing_pubkeys: Vec::new(),
}],
}),
..setup_sequencer_config()
Binary file not shown.
+1 -1
View File
@@ -373,7 +373,7 @@ fn watch_peer(peer: ZoneId, receiver_id: ProgramId) -> CrossZoneConfig {
src_program_id: programs::ping_sender().id(),
target_program_id: receiver_id,
}],
expected_block_signing_pubkey: None,
expected_block_signing_pubkeys: Vec::new(),
}],
}
}