mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-25 03:11:21 +00:00
Merge remote-tracking branch 'origin/moudy/cross-zone-peer-cache-bound' into moudy/cross-zone-halt-operability
This commit is contained in:
Generated
+12
-12
@@ -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.
@@ -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(),
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
@@ -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(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user