mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-29 10:29:32 +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
|
||||
ping_core.workspace = true
|
||||
cross_zone_outbox_core.workspace = true
|
||||
cross_zone_inbox_core.workspace = true
|
||||
bridge_lock_core.workspace = true
|
||||
wrapped_token_core.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,
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
struct DepositMetadata {
|
||||
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
|
||||
.push((TransactionOrigin::User, authenticated_tx))
|
||||
.await
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user