mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-02 11:03:14 +00:00
feat(cross-zone): reject user-origin calls to the sequencer-only inbox
This commit is contained in:
parent
4fd465b529
commit
fef7ce2c38
@ -25,6 +25,7 @@ faucet_core.workspace = true
|
|||||||
bridge_core.workspace = true
|
bridge_core.workspace = true
|
||||||
ping_core.workspace = true
|
ping_core.workspace = true
|
||||||
cross_zone_outbox_core.workspace = true
|
cross_zone_outbox_core.workspace = true
|
||||||
|
cross_zone_inbox_core.workspace = true
|
||||||
bridge_lock_core.workspace = true
|
bridge_lock_core.workspace = true
|
||||||
wrapped_token_core.workspace = true
|
wrapped_token_core.workspace = true
|
||||||
risc0-zkvm.workspace = true
|
risc0-zkvm.workspace = true
|
||||||
|
|||||||
81
integration_tests/tests/cross_zone_ingress_guard.rs
Normal file
81
integration_tests/tests/cross_zone_ingress_guard.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#![expect(
|
||||||
|
clippy::tests_outside_test_module,
|
||||||
|
reason = "We don't care about these in tests"
|
||||||
|
)]
|
||||||
|
|
||||||
|
//! M6 ingress guard: the cross-zone inbox is sequencer-only. Only the watcher
|
||||||
|
//! injects inbox dispatches; a user must not be able to invoke the inbox through
|
||||||
|
//! the public RPC, or anyone could forge an inbound cross-zone delivery. The
|
||||||
|
//! inbox guest's caller-is-none assertion passes for a top-level user tx, so the
|
||||||
|
//! sequencer ingress guard is the only thing that stops this.
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use common::transaction::LeeTransaction;
|
||||||
|
use cross_zone_inbox_core::{
|
||||||
|
CrossZoneMessage, Instruction, inbox_config_account_id, inbox_seen_shard_account_id,
|
||||||
|
};
|
||||||
|
use integration_tests::{
|
||||||
|
config::{self, SequencerPartialConfig},
|
||||||
|
setup::{setup_bedrock_node, setup_sequencer},
|
||||||
|
};
|
||||||
|
use lee::{
|
||||||
|
PublicTransaction,
|
||||||
|
program::Program,
|
||||||
|
public_transaction::{Message, WitnessSet},
|
||||||
|
};
|
||||||
|
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
|
||||||
|
use tokio::test;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
async fn user_origin_inbox_call_rejected() -> Result<()> {
|
||||||
|
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||||
|
.await
|
||||||
|
.context("Failed to set up Bedrock node")?;
|
||||||
|
let partial = SequencerPartialConfig::default();
|
||||||
|
let channel = config::bedrock_channel_id();
|
||||||
|
let (seq, _seq_home) = setup_sequencer(partial, bedrock_addr, vec![], channel, None)
|
||||||
|
.await
|
||||||
|
.context("Failed to set up sequencer")?;
|
||||||
|
|
||||||
|
// A user hand-builds a top-level inbox Dispatch and submits it via RPC.
|
||||||
|
let inbox_id = Program::cross_zone_inbox().id();
|
||||||
|
let msg = CrossZoneMessage {
|
||||||
|
src_zone: [2; 32],
|
||||||
|
src_block_id: 1,
|
||||||
|
src_tx_index: 0,
|
||||||
|
src_program_id: [9; 8],
|
||||||
|
target_program_id: Program::ping_receiver().id(),
|
||||||
|
payload: vec![],
|
||||||
|
l1_inclusion_witness: None,
|
||||||
|
};
|
||||||
|
let seen_id = inbox_seen_shard_account_id(inbox_id, &msg.src_zone, msg.src_block_id);
|
||||||
|
let message = Message::try_new(
|
||||||
|
inbox_id,
|
||||||
|
vec![inbox_config_account_id(inbox_id), seen_id],
|
||||||
|
vec![],
|
||||||
|
Instruction::Dispatch(msg),
|
||||||
|
)
|
||||||
|
.expect("build dispatch message");
|
||||||
|
let tx = LeeTransaction::Public(PublicTransaction::new(
|
||||||
|
message,
|
||||||
|
WitnessSet::from_raw_parts(vec![]),
|
||||||
|
));
|
||||||
|
|
||||||
|
let result = sequencer_client(seq.addr())?.send_transaction(tx).await;
|
||||||
|
let err = result.expect_err("the sequencer must reject a user-origin inbox call");
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("sequencer-only"),
|
||||||
|
"rejection should cite the sequencer-only guard, got: {err}"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sequencer_client(addr: SocketAddr) -> Result<SequencerClient> {
|
||||||
|
let url = config::addr_to_url(config::UrlProtocol::Http, addr)
|
||||||
|
.context("Failed to build sequencer URL")?;
|
||||||
|
SequencerClientBuilder::default()
|
||||||
|
.build(url)
|
||||||
|
.context("Failed to build sequencer client")
|
||||||
|
}
|
||||||
@ -44,6 +44,15 @@ pub enum TransactionOrigin {
|
|||||||
Sequencer,
|
Sequencer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a program may only be invoked by sequencer-origin transactions. The
|
||||||
|
/// cross-zone inbox is injected solely by the watcher; a user-submitted call
|
||||||
|
/// must be rejected at ingress, because `TransactionOrigin` is not carried in
|
||||||
|
/// the block and so cannot be re-checked later by the indexer.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_sequencer_only_program(program_id: lee::ProgramId) -> bool {
|
||||||
|
program_id == Program::cross_zone_inbox().id()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, BorshDeserialize)]
|
#[derive(Clone, Debug, BorshDeserialize)]
|
||||||
struct DepositMetadata {
|
struct DepositMetadata {
|
||||||
recipient_id: lee::AccountId,
|
recipient_id: lee::AccountId,
|
||||||
@ -1725,3 +1734,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod sequencer_only_program_tests {
|
||||||
|
use lee::program::Program;
|
||||||
|
|
||||||
|
use super::is_sequencer_only_program;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_the_cross_zone_inbox_is_sequencer_only() {
|
||||||
|
assert!(is_sequencer_only_program(Program::cross_zone_inbox().id()));
|
||||||
|
assert!(!is_sequencer_only_program(Program::cross_zone_outbox().id()));
|
||||||
|
assert!(!is_sequencer_only_program(Program::wrapped_token().id()));
|
||||||
|
assert!(!is_sequencer_only_program(Program::ping_sender().id()));
|
||||||
|
assert!(!is_sequencer_only_program(Program::clock().id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -73,6 +73,21 @@ impl<BC: BlockPublisherTrait + Send + 'static> sequencer_service_rpc::RpcServer
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Sequencer-only programs (the cross-zone inbox) are injected by the
|
||||||
|
// watcher; a user must not invoke them top-level, or anyone could forge
|
||||||
|
// an inbound cross-zone delivery. Chained user calls are already rejected
|
||||||
|
// by the inbox guest's caller-is-none assertion.
|
||||||
|
if let LeeTransaction::Public(public_tx) = &authenticated_tx {
|
||||||
|
if sequencer_core::is_sequencer_only_program(public_tx.message().program_id) {
|
||||||
|
return Err(ErrorObjectOwned::owned(
|
||||||
|
ErrorCode::InvalidParams.code(),
|
||||||
|
"Program is sequencer-only and cannot be invoked by a user transaction"
|
||||||
|
.to_string(),
|
||||||
|
None::<()>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.mempool_handle
|
self.mempool_handle
|
||||||
.push((TransactionOrigin::User, authenticated_tx))
|
.push((TransactionOrigin::User, authenticated_tx))
|
||||||
.await
|
.await
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user