mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-03-26 12:13:08 +00:00
add sequencer checks
This commit is contained in:
parent
90f20a7040
commit
3324bcf391
BIN
artifacts/program_methods/clock.bin
Normal file
BIN
artifacts/program_methods/clock.bin
Normal file
Binary file not shown.
@ -43,6 +43,23 @@ impl NSSATransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the canonical Block Context Program invocation transaction.
|
||||||
|
/// Every valid block must end with exactly one occurrence of this transaction.
|
||||||
|
#[must_use]
|
||||||
|
pub fn clock_invocation() -> Self {
|
||||||
|
let message = nssa::public_transaction::Message::try_new(
|
||||||
|
nssa::program::Program::clock().id(),
|
||||||
|
vec![nssa::CLOCK_PROGRAM_ACCOUNT_ID],
|
||||||
|
vec![],
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.expect("Clock invocation message should always be constructable");
|
||||||
|
Self::Public(nssa::PublicTransaction::new(
|
||||||
|
message,
|
||||||
|
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Introduce type-safe wrapper around checked transaction, e.g. AuthenticatedTransaction
|
// TODO: Introduce type-safe wrapper around checked transaction, e.g. AuthenticatedTransaction
|
||||||
pub fn transaction_stateless_check(self) -> Result<Self, TransactionMalformationError> {
|
pub fn transaction_stateless_check(self) -> Result<Self, TransactionMalformationError> {
|
||||||
// Stateless checks here
|
// Stateless checks here
|
||||||
|
|||||||
@ -119,6 +119,30 @@ impl IndexerStore {
|
|||||||
|
|
||||||
pub async fn put_block(&self, mut block: Block, l1_header: HeaderId) -> Result<()> {
|
pub async fn put_block(&self, mut block: Block, l1_header: HeaderId) -> Result<()> {
|
||||||
{
|
{
|
||||||
|
let canonical_clock_tx = NSSATransaction::clock_invocation();
|
||||||
|
|
||||||
|
// Validate block structure: the last transaction must be the sole clock invocation.
|
||||||
|
let last_tx = block
|
||||||
|
.body
|
||||||
|
.transactions
|
||||||
|
.last()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Block must contain at least one transaction"))?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
last_tx == &canonical_clock_tx,
|
||||||
|
"Last transaction in block must be the canonical clock invocation"
|
||||||
|
);
|
||||||
|
|
||||||
|
let clock_count = block
|
||||||
|
.body
|
||||||
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.filter(|tx| *tx == &canonical_clock_tx)
|
||||||
|
.count();
|
||||||
|
anyhow::ensure!(
|
||||||
|
clock_count == 1,
|
||||||
|
"Block must contain exactly one Block Context Program invocation"
|
||||||
|
);
|
||||||
|
|
||||||
let mut state_guard = self.current_state.write().await;
|
let mut state_guard = self.current_state.write().await;
|
||||||
|
|
||||||
for transaction in &block.body.transactions {
|
for transaction in &block.body.transactions {
|
||||||
@ -208,11 +232,12 @@ mod tests {
|
|||||||
10,
|
10,
|
||||||
&sign_key,
|
&sign_key,
|
||||||
);
|
);
|
||||||
|
let clock_tx = NSSATransaction::clock_invocation();
|
||||||
|
|
||||||
let next_block = common::test_utils::produce_dummy_block(
|
let next_block = common::test_utils::produce_dummy_block(
|
||||||
u64::try_from(i).unwrap(),
|
u64::try_from(i).unwrap(),
|
||||||
Some(prev_hash),
|
Some(prev_hash),
|
||||||
vec![tx],
|
vec![tx, clock_tx],
|
||||||
);
|
);
|
||||||
prev_hash = next_block.header.hash;
|
prev_hash = next_block.header.hash;
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ pub use program_deployment_transaction::ProgramDeploymentTransaction;
|
|||||||
pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID;
|
pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID;
|
||||||
pub use public_transaction::PublicTransaction;
|
pub use public_transaction::PublicTransaction;
|
||||||
pub use signature::{PrivateKey, PublicKey, Signature};
|
pub use signature::{PrivateKey, PublicKey, Signature};
|
||||||
pub use state::V03State;
|
pub use state::{CLOCK_PROGRAM_ACCOUNT_ID, V03State};
|
||||||
|
|
||||||
pub mod encoding;
|
pub mod encoding;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|||||||
@ -154,6 +154,17 @@ impl PrivacyPreservingTransaction {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the post-state the transaction declares for `account_id`, or `None` if the account
|
||||||
|
/// is not part of this transaction's public execution.
|
||||||
|
#[must_use]
|
||||||
|
pub fn public_post_state_for(&self, account_id: &AccountId) -> Option<&Account> {
|
||||||
|
self.message
|
||||||
|
.public_account_ids
|
||||||
|
.iter()
|
||||||
|
.position(|id| id == account_id)
|
||||||
|
.map(|i| &self.message.public_post_states[i])
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn affected_public_account_ids(&self) -> Vec<AccountId> {
|
pub fn affected_public_account_ids(&self) -> Vec<AccountId> {
|
||||||
let mut acc_set = self
|
let mut acc_set = self
|
||||||
|
|||||||
@ -16,6 +16,9 @@ use crate::{
|
|||||||
|
|
||||||
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
|
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
|
||||||
|
|
||||||
|
pub const CLOCK_PROGRAM_ACCOUNT_ID: AccountId =
|
||||||
|
AccountId::new(*b"/LEZ/ClockProgramAccount/0000001");
|
||||||
|
|
||||||
#[derive(Clone, BorshSerialize, BorshDeserialize)]
|
#[derive(Clone, BorshSerialize, BorshDeserialize)]
|
||||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||||
pub struct CommitmentSet {
|
pub struct CommitmentSet {
|
||||||
@ -148,8 +151,8 @@ impl V03State {
|
|||||||
this.insert_program(Program::amm());
|
this.insert_program(Program::amm());
|
||||||
this.insert_program(Program::clock());
|
this.insert_program(Program::clock());
|
||||||
|
|
||||||
this.force_insert_account(
|
this.public_state.insert(
|
||||||
AccountId::new(b"/LEZ/ClockProgramAccount/0000001".to_owned()),
|
CLOCK_PROGRAM_ACCOUNT_ID,
|
||||||
Account {
|
Account {
|
||||||
program_owner: Program::clock().id(),
|
program_owner: Program::clock().id(),
|
||||||
data: 0_u64
|
data: 0_u64
|
||||||
|
|||||||
@ -202,9 +202,27 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
let curr_time = u64::try_from(chrono::Utc::now().timestamp_millis())
|
let curr_time = u64::try_from(chrono::Utc::now().timestamp_millis())
|
||||||
.expect("Timestamp must be positive");
|
.expect("Timestamp must be positive");
|
||||||
|
|
||||||
|
let clock_program_id = nssa::program::Program::clock().id();
|
||||||
|
let clock_account_pre = self.state.get_account_by_id(nssa::CLOCK_PROGRAM_ACCOUNT_ID);
|
||||||
|
|
||||||
while let Some(tx) = self.mempool.pop() {
|
while let Some(tx) = self.mempool.pop() {
|
||||||
let tx_hash = tx.hash();
|
let tx_hash = tx.hash();
|
||||||
|
|
||||||
|
// The Block Context Program is system-only. Reject:
|
||||||
|
// - any public tx that invokes the clock program ID, and
|
||||||
|
// - any PP tx that declares a modified post-state for the clock account.
|
||||||
|
let touches_system = match &tx {
|
||||||
|
NSSATransaction::Public(p) => p.message().program_id == clock_program_id,
|
||||||
|
NSSATransaction::PrivacyPreserving(pp) => pp
|
||||||
|
.public_post_state_for(&nssa::CLOCK_PROGRAM_ACCOUNT_ID)
|
||||||
|
.is_some_and(|post| post != &clock_account_pre),
|
||||||
|
NSSATransaction::ProgramDeployment(_) => false,
|
||||||
|
};
|
||||||
|
if touches_system {
|
||||||
|
warn!("Dropping transaction from mempool: user transactions may not modify the system clock account");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if block size exceeds limit
|
// Check if block size exceeds limit
|
||||||
let temp_valid_transactions =
|
let temp_valid_transactions =
|
||||||
[valid_transactions.as_slice(), std::slice::from_ref(&tx)].concat();
|
[valid_transactions.as_slice(), std::slice::from_ref(&tx)].concat();
|
||||||
@ -249,6 +267,16 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append the Block Context Program invocation as the mandatory last transaction.
|
||||||
|
match self.execute_check_transaction_on_state(NSSATransaction::clock_invocation()) {
|
||||||
|
Ok(clock_nssa_tx) => {
|
||||||
|
valid_transactions.push(clock_nssa_tx);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Clock transaction failed execution check: {err:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let hashable_data = HashableBlockData {
|
let hashable_data = HashableBlockData {
|
||||||
block_id: new_block_height,
|
block_id: new_block_height,
|
||||||
transactions: valid_transactions,
|
transactions: valid_transactions,
|
||||||
@ -368,7 +396,8 @@ mod tests {
|
|||||||
use base58::ToBase58 as _;
|
use base58::ToBase58 as _;
|
||||||
use bedrock_client::BackoffConfig;
|
use bedrock_client::BackoffConfig;
|
||||||
use common::{
|
use common::{
|
||||||
block::AccountInitialData, test_utils::sequencer_sign_key_for_testing,
|
block::AccountInitialData,
|
||||||
|
test_utils::sequencer_sign_key_for_testing,
|
||||||
transaction::NSSATransaction,
|
transaction::NSSATransaction,
|
||||||
};
|
};
|
||||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||||
@ -694,8 +723,8 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Only one should be included in the block
|
// Only one user tx should be included; the clock tx is always appended last.
|
||||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
assert_eq!(block.body.transactions, vec![tx.clone(), NSSATransaction::clock_invocation()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -721,7 +750,7 @@ mod tests {
|
|||||||
.get_block_at_id(sequencer.chain_height)
|
.get_block_at_id(sequencer.chain_height)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
assert_eq!(block.body.transactions, vec![tx.clone(), NSSATransaction::clock_invocation()]);
|
||||||
|
|
||||||
// Add same transaction should fail
|
// Add same transaction should fail
|
||||||
mempool_handle.push(tx.clone()).await.unwrap();
|
mempool_handle.push(tx.clone()).await.unwrap();
|
||||||
@ -733,7 +762,8 @@ mod tests {
|
|||||||
.get_block_at_id(sequencer.chain_height)
|
.get_block_at_id(sequencer.chain_height)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(block.body.transactions.is_empty());
|
// The replay is rejected, so only the clock tx is in the block.
|
||||||
|
assert_eq!(block.body.transactions, vec![NSSATransaction::clock_invocation()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -768,7 +798,7 @@ mod tests {
|
|||||||
.get_block_at_id(sequencer.chain_height)
|
.get_block_at_id(sequencer.chain_height)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
assert_eq!(block.body.transactions, vec![tx.clone(), NSSATransaction::clock_invocation()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiating a new sequencer from the same config. This should load the existing block
|
// Instantiating a new sequencer from the same config. This should load the existing block
|
||||||
@ -898,11 +928,50 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
new_block.body.transactions,
|
new_block.body.transactions,
|
||||||
vec![tx],
|
vec![tx, NSSATransaction::clock_invocation()],
|
||||||
"New block should contain the submitted transaction"
|
"New block should contain the submitted transaction and the clock invocation"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn transactions_touching_clock_account_are_dropped_from_block() {
|
||||||
|
let (mut sequencer, mempool_handle) = common_setup().await;
|
||||||
|
|
||||||
|
// Canonical clock invocation and a crafted variant with different parameters targeting
|
||||||
|
// the clock program — both must be dropped since the program_id is the clock program.
|
||||||
|
let crafted_clock_tx = {
|
||||||
|
let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id;
|
||||||
|
let message = nssa::public_transaction::Message::try_new(
|
||||||
|
nssa::program::Program::clock().id(),
|
||||||
|
vec![acc1],
|
||||||
|
vec![],
|
||||||
|
42_u64,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
NSSATransaction::Public(nssa::PublicTransaction::new(
|
||||||
|
message,
|
||||||
|
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
mempool_handle
|
||||||
|
.push(NSSATransaction::clock_invocation())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
mempool_handle.push(crafted_clock_tx).await.unwrap();
|
||||||
|
sequencer
|
||||||
|
.produce_new_block_with_mempool_transactions()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let block = sequencer
|
||||||
|
.store
|
||||||
|
.get_block_at_id(sequencer.chain_height)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Both transactions were dropped. Only the system-appended clock tx remains.
|
||||||
|
assert_eq!(block.body.transactions, vec![NSSATransaction::clock_invocation()]);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn start_from_config_uses_db_height_not_config_genesis() {
|
async fn start_from_config_uses_db_height_not_config_genesis() {
|
||||||
let mut config = setup_sequencer_config();
|
let mut config = setup_sequencer_config();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user