mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-25 23:23:11 +00:00
refactor: move sequencer_ directories into sequencer
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "sequencer_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
common.workspace = true
|
||||
storage.workspace = true
|
||||
mempool.workspace = true
|
||||
bedrock_client.workspace = true
|
||||
|
||||
base58.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
humantime-serde.workspace = true
|
||||
tempfile.workspace = true
|
||||
chrono.workspace = true
|
||||
log.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
rand.workspace = true
|
||||
borsh.workspace = true
|
||||
bytesize.workspace = true
|
||||
url.workspace = true
|
||||
jsonrpsee = { workspace = true, features = ["ws-client"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testnet = []
|
||||
# Generate mock external clients implementations for testing
|
||||
mock = []
|
||||
|
||||
[dev-dependencies]
|
||||
futures.workspace = true
|
||||
@@ -0,0 +1,127 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use bedrock_client::BedrockClient;
|
||||
pub use common::block::Block;
|
||||
pub use logos_blockchain_core::mantle::{MantleTx, SignedMantleTx, ops::channel::MsgId};
|
||||
use logos_blockchain_core::mantle::{
|
||||
Op, OpProof, Transaction as _, TxHash, ledger,
|
||||
ops::channel::{ChannelId, inscribe::InscriptionOp},
|
||||
};
|
||||
pub use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||
use logos_blockchain_key_management_system_service::keys::Ed25519PublicKey;
|
||||
|
||||
use crate::config::BedrockConfig;
|
||||
|
||||
#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")]
|
||||
pub trait BlockSettlementClientTrait: Clone {
|
||||
//// Create a new client.
|
||||
fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result<Self>;
|
||||
|
||||
/// Get the bedrock channel ID used by this client.
|
||||
fn bedrock_channel_id(&self) -> ChannelId;
|
||||
|
||||
/// Get the bedrock signing key used by this client.
|
||||
fn bedrock_signing_key(&self) -> &Ed25519Key;
|
||||
|
||||
/// Post a transaction to the node.
|
||||
async fn submit_inscribe_tx_to_bedrock(&self, tx: SignedMantleTx) -> Result<()>;
|
||||
|
||||
/// Create and sign a transaction for inscribing data.
|
||||
fn create_inscribe_tx(&self, block: &Block) -> Result<(SignedMantleTx, MsgId)> {
|
||||
let inscription_data = borsh::to_vec(block)?;
|
||||
log::debug!(
|
||||
"The size of the block {} is {} bytes",
|
||||
block.header.block_id,
|
||||
inscription_data.len()
|
||||
);
|
||||
let verifying_key_bytes = self.bedrock_signing_key().public_key().to_bytes();
|
||||
let verifying_key =
|
||||
Ed25519PublicKey::from_bytes(&verifying_key_bytes).expect("valid ed25519 public key");
|
||||
|
||||
let inscribe_op = InscriptionOp {
|
||||
channel_id: self.bedrock_channel_id(),
|
||||
inscription: inscription_data,
|
||||
parent: block.bedrock_parent_id.into(),
|
||||
signer: verifying_key,
|
||||
};
|
||||
let inscribe_op_id = inscribe_op.id();
|
||||
|
||||
let ledger_tx = ledger::Tx::new(vec![], vec![]);
|
||||
|
||||
let inscribe_tx = MantleTx {
|
||||
ops: vec![Op::ChannelInscribe(inscribe_op)],
|
||||
ledger_tx,
|
||||
// Altruistic test config
|
||||
storage_gas_price: 0,
|
||||
execution_gas_price: 0,
|
||||
};
|
||||
|
||||
let tx_hash = inscribe_tx.hash();
|
||||
let signature_bytes = self
|
||||
.bedrock_signing_key()
|
||||
.sign_payload(tx_hash.as_signing_bytes().as_ref())
|
||||
.to_bytes();
|
||||
let signature =
|
||||
logos_blockchain_key_management_system_service::keys::Ed25519Signature::from_bytes(
|
||||
&signature_bytes,
|
||||
);
|
||||
|
||||
let signed_mantle_tx = SignedMantleTx {
|
||||
ops_proofs: vec![OpProof::Ed25519Sig(signature)],
|
||||
ledger_tx_proof: empty_ledger_signature(&tx_hash),
|
||||
mantle_tx: inscribe_tx,
|
||||
};
|
||||
Ok((signed_mantle_tx, inscribe_op_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// A component that posts block data to logos blockchain.
|
||||
#[derive(Clone)]
|
||||
pub struct BlockSettlementClient {
|
||||
client: BedrockClient,
|
||||
signing_key: Ed25519Key,
|
||||
channel_id: ChannelId,
|
||||
}
|
||||
|
||||
impl BlockSettlementClientTrait for BlockSettlementClient {
|
||||
fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result<Self> {
|
||||
let client =
|
||||
BedrockClient::new(config.backoff, config.node_url.clone(), config.auth.clone())
|
||||
.context("Failed to initialize bedrock client")?;
|
||||
Ok(Self {
|
||||
client,
|
||||
signing_key,
|
||||
channel_id: config.channel_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn submit_inscribe_tx_to_bedrock(&self, tx: SignedMantleTx) -> Result<()> {
|
||||
let (parent_id, msg_id) = match tx.mantle_tx.ops.first() {
|
||||
Some(Op::ChannelInscribe(inscribe)) => (inscribe.parent, inscribe.id()),
|
||||
_ => panic!("Expected ChannelInscribe op"),
|
||||
};
|
||||
self.client
|
||||
.post_transaction(tx)
|
||||
.await
|
||||
.context("Failed to post transaction to Bedrock after retries")?
|
||||
.context("Failed to post transaction to Bedrock with non-retryable error")?;
|
||||
|
||||
log::debug!("Posted block to Bedrock with parent id {parent_id:?} and msg id: {msg_id:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bedrock_channel_id(&self) -> ChannelId {
|
||||
self.channel_id
|
||||
}
|
||||
|
||||
fn bedrock_signing_key(&self) -> &Ed25519Key {
|
||||
&self.signing_key
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_ledger_signature(
|
||||
tx_hash: &TxHash,
|
||||
) -> logos_blockchain_key_management_system_service::keys::ZkSignature {
|
||||
logos_blockchain_key_management_system_service::keys::ZkKey::multi_sign(&[], tx_hash.as_ref())
|
||||
.expect("multi-sign with empty key set works")
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use common::{
|
||||
HashType,
|
||||
block::{Block, BlockMeta, MantleMsgId},
|
||||
transaction::NSSATransaction,
|
||||
};
|
||||
use nssa::V02State;
|
||||
use storage::sequencer::RocksDBIO;
|
||||
|
||||
pub struct SequencerStore {
|
||||
dbio: RocksDBIO,
|
||||
// TODO: Consider adding the hashmap to the database for faster recovery.
|
||||
tx_hash_to_block_map: HashMap<HashType, u64>,
|
||||
genesis_id: u64,
|
||||
signing_key: nssa::PrivateKey,
|
||||
}
|
||||
|
||||
impl SequencerStore {
|
||||
/// Starting database at the start of new chain.
|
||||
/// Creates files if necessary.
|
||||
///
|
||||
/// ATTENTION: Will overwrite genesis block.
|
||||
pub fn open_db_with_genesis(
|
||||
location: &Path,
|
||||
genesis_block: &Block,
|
||||
genesis_msg_id: MantleMsgId,
|
||||
signing_key: nssa::PrivateKey,
|
||||
) -> Result<Self> {
|
||||
let tx_hash_to_block_map = block_to_transactions_map(genesis_block);
|
||||
|
||||
let dbio = RocksDBIO::open_or_create(location, genesis_block, genesis_msg_id)?;
|
||||
|
||||
let genesis_id = dbio.get_meta_first_block_in_db()?;
|
||||
|
||||
Ok(Self {
|
||||
dbio,
|
||||
tx_hash_to_block_map,
|
||||
genesis_id,
|
||||
signing_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_block_at_id(&self, id: u64) -> Result<Block> {
|
||||
Ok(self.dbio.get_block(id)?)
|
||||
}
|
||||
|
||||
pub fn delete_block_at_id(&mut self, block_id: u64) -> Result<()> {
|
||||
Ok(self.dbio.delete_block(block_id)?)
|
||||
}
|
||||
|
||||
pub fn mark_block_as_finalized(&mut self, block_id: u64) -> Result<()> {
|
||||
Ok(self.dbio.mark_block_as_finalized(block_id)?)
|
||||
}
|
||||
|
||||
/// Returns the transaction corresponding to the given hash, if it exists in the blockchain.
|
||||
pub fn get_transaction_by_hash(&self, hash: HashType) -> Option<NSSATransaction> {
|
||||
let block_id = self.tx_hash_to_block_map.get(&hash);
|
||||
let block = block_id.map(|&id| self.get_block_at_id(id));
|
||||
if let Some(Ok(block)) = block {
|
||||
for transaction in block.body.transactions {
|
||||
if transaction.hash() == hash {
|
||||
return Some(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn latest_block_meta(&self) -> Result<BlockMeta> {
|
||||
Ok(self.dbio.latest_block_meta()?)
|
||||
}
|
||||
|
||||
pub const fn genesis_id(&self) -> u64 {
|
||||
self.genesis_id
|
||||
}
|
||||
|
||||
pub const fn signing_key(&self) -> &nssa::PrivateKey {
|
||||
&self.signing_key
|
||||
}
|
||||
|
||||
pub fn get_all_blocks(&self) -> impl Iterator<Item = Result<Block>> {
|
||||
self.dbio.get_all_blocks().map(|res| Ok(res?))
|
||||
}
|
||||
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
msg_id: MantleMsgId,
|
||||
state: &V02State,
|
||||
) -> Result<()> {
|
||||
let new_transactions_map = block_to_transactions_map(block);
|
||||
self.dbio.atomic_update(block, msg_id, state)?;
|
||||
self.tx_hash_to_block_map.extend(new_transactions_map);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_nssa_state(&self) -> Option<V02State> {
|
||||
self.dbio.get_nssa_state().ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap<HashType, u64> {
|
||||
block
|
||||
.body
|
||||
.transactions
|
||||
.iter()
|
||||
.map(|transaction| (transaction.hash(), block.header.block_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use common::{block::HashableBlockData, test_utils::sequencer_sign_key_for_testing};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn get_transaction_by_hash() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
let signing_key = sequencer_sign_key_for_testing();
|
||||
|
||||
let genesis_block_hashable_data = HashableBlockData {
|
||||
block_id: 0,
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
transactions: vec![],
|
||||
};
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
// Start an empty node store
|
||||
let mut node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||
|
||||
// Try retrieve a tx that's not in the chain yet.
|
||||
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
||||
assert_eq!(None, retrieved_tx);
|
||||
// Add the block with the transaction
|
||||
let dummy_state = V02State::new_with_genesis_accounts(&[], &[]);
|
||||
node_store.update(&block, [1; 32], &dummy_state).unwrap();
|
||||
// Try again
|
||||
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
||||
assert_eq!(Some(tx), retrieved_tx);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_block_meta_returns_genesis_meta_initially() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
let signing_key = sequencer_sign_key_for_testing();
|
||||
|
||||
let genesis_block_hashable_data = HashableBlockData {
|
||||
block_id: 0,
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
transactions: vec![],
|
||||
};
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
let genesis_hash = genesis_block.header.hash;
|
||||
|
||||
let node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
|
||||
// Verify that initially the latest block hash equals genesis hash
|
||||
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||
assert_eq!(latest_meta.hash, genesis_hash);
|
||||
assert_eq!(latest_meta.msg_id, [0; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_block_meta_updates_after_new_block() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
let signing_key = sequencer_sign_key_for_testing();
|
||||
|
||||
let genesis_block_hashable_data = HashableBlockData {
|
||||
block_id: 0,
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
transactions: vec![],
|
||||
};
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
let mut node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
|
||||
// Add a new block
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx]);
|
||||
let block_hash = block.header.hash;
|
||||
let block_msg_id = [1; 32];
|
||||
|
||||
let dummy_state = V02State::new_with_genesis_accounts(&[], &[]);
|
||||
node_store
|
||||
.update(&block, block_msg_id, &dummy_state)
|
||||
.unwrap();
|
||||
|
||||
// Verify that the latest block meta now equals the new block's hash and msg_id
|
||||
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||
assert_eq!(latest_meta.hash, block_hash);
|
||||
assert_eq!(latest_meta.msg_id, block_msg_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_block_finalized() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
let signing_key = sequencer_sign_key_for_testing();
|
||||
|
||||
let genesis_block_hashable_data = HashableBlockData {
|
||||
block_id: 0,
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
transactions: vec![],
|
||||
};
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
let mut node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
|
||||
// Add a new block with Pending status
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx]);
|
||||
let block_id = block.header.block_id;
|
||||
|
||||
let dummy_state = V02State::new_with_genesis_accounts(&[], &[]);
|
||||
node_store.update(&block, [1; 32], &dummy_state).unwrap();
|
||||
|
||||
// Verify initial status is Pending
|
||||
let retrieved_block = node_store.get_block_at_id(block_id).unwrap();
|
||||
assert!(matches!(
|
||||
retrieved_block.bedrock_status,
|
||||
common::block::BedrockStatus::Pending
|
||||
));
|
||||
|
||||
// Mark block as finalized
|
||||
node_store.mark_block_as_finalized(block_id).unwrap();
|
||||
|
||||
// Verify status is now Finalized
|
||||
let finalized_block = node_store.get_block_at_id(block_id).unwrap();
|
||||
assert!(matches!(
|
||||
finalized_block.bedrock_status,
|
||||
common::block::BedrockStatus::Finalized
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use bedrock_client::BackoffConfig;
|
||||
use bytesize::ByteSize;
|
||||
use common::{
|
||||
block::{AccountInitialData, CommitmentsInitialData},
|
||||
config::BasicAuth,
|
||||
};
|
||||
use humantime_serde;
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
// TODO: Provide default values
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SequencerConfig {
|
||||
/// Home dir of sequencer storage.
|
||||
pub home: PathBuf,
|
||||
/// Override rust log (env var logging level).
|
||||
pub override_rust_log: Option<String>,
|
||||
/// Genesis id.
|
||||
pub genesis_id: u64,
|
||||
/// If `True`, then adds random sequence of bytes to genesis block.
|
||||
pub is_genesis_random: bool,
|
||||
/// Maximum number of transactions in block.
|
||||
pub max_num_tx_in_block: usize,
|
||||
/// Maximum block size (includes header and transactions).
|
||||
#[serde(default = "default_max_block_size")]
|
||||
pub max_block_size: ByteSize,
|
||||
/// Mempool maximum size.
|
||||
pub mempool_max_size: usize,
|
||||
/// Interval in which blocks produced.
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub block_create_timeout: Duration,
|
||||
/// Interval in which pending blocks are retried.
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub retry_pending_blocks_timeout: Duration,
|
||||
/// Port to listen.
|
||||
pub port: u16,
|
||||
/// List of initial accounts data.
|
||||
pub initial_accounts: Vec<AccountInitialData>,
|
||||
/// List of initial commitments.
|
||||
pub initial_commitments: Vec<CommitmentsInitialData>,
|
||||
/// Sequencer own signing key.
|
||||
pub signing_key: [u8; 32],
|
||||
/// Bedrock configuration options.
|
||||
pub bedrock_config: BedrockConfig,
|
||||
/// Indexer RPC URL.
|
||||
pub indexer_rpc_url: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct BedrockConfig {
|
||||
/// Fibonacci backoff retry strategy configuration.
|
||||
#[serde(default)]
|
||||
pub backoff: BackoffConfig,
|
||||
/// Bedrock channel ID.
|
||||
pub channel_id: ChannelId,
|
||||
/// Bedrock Url.
|
||||
pub node_url: Url,
|
||||
/// Bedrock auth.
|
||||
pub auth: Option<BasicAuth>,
|
||||
}
|
||||
|
||||
impl SequencerConfig {
|
||||
pub fn from_path(config_home: &Path) -> Result<Self> {
|
||||
let file = File::open(config_home)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
Ok(serde_json::from_reader(reader)?)
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_max_block_size() -> ByteSize {
|
||||
ByteSize::mib(1)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use log::info;
|
||||
pub use url::Url;
|
||||
|
||||
#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")]
|
||||
pub trait IndexerClientTrait: Clone {
|
||||
async fn new(indexer_url: &Url) -> Result<Self>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexerClient(Arc<jsonrpsee::ws_client::WsClient>);
|
||||
|
||||
impl IndexerClientTrait for IndexerClient {
|
||||
async fn new(indexer_url: &Url) -> Result<Self> {
|
||||
info!("Connecting to Indexer at {indexer_url}");
|
||||
|
||||
let client = jsonrpsee::ws_client::WsClientBuilder::default()
|
||||
.build(indexer_url)
|
||||
.await
|
||||
.context("Failed to create websocket client")?;
|
||||
|
||||
Ok(Self(Arc::new(client)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IndexerClient {
|
||||
type Target = jsonrpsee::ws_client::WsClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,951 @@
|
||||
use std::{path::Path, time::Instant};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use bedrock_client::SignedMantleTx;
|
||||
#[cfg(feature = "testnet")]
|
||||
use common::PINATA_BASE58;
|
||||
use common::{
|
||||
HashType,
|
||||
block::{BedrockStatus, Block, HashableBlockData},
|
||||
transaction::NSSATransaction,
|
||||
};
|
||||
use config::SequencerConfig;
|
||||
use log::{error, info, warn};
|
||||
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
|
||||
use mempool::{MemPool, MemPoolHandle};
|
||||
#[cfg(feature = "mock")]
|
||||
pub use mock::SequencerCoreWithMockClients;
|
||||
|
||||
use crate::{
|
||||
block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait, MsgId},
|
||||
block_store::SequencerStore,
|
||||
indexer_client::{IndexerClient, IndexerClientTrait},
|
||||
};
|
||||
|
||||
pub mod block_settlement_client;
|
||||
pub mod block_store;
|
||||
pub mod config;
|
||||
pub mod indexer_client;
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub mod mock;
|
||||
|
||||
pub struct SequencerCore<
|
||||
BC: BlockSettlementClientTrait = BlockSettlementClient,
|
||||
IC: IndexerClientTrait = IndexerClient,
|
||||
> {
|
||||
state: nssa::V02State,
|
||||
store: SequencerStore,
|
||||
mempool: MemPool<NSSATransaction>,
|
||||
sequencer_config: SequencerConfig,
|
||||
chain_height: u64,
|
||||
block_settlement_client: BC,
|
||||
indexer_client: IC,
|
||||
}
|
||||
|
||||
impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, IC> {
|
||||
/// Starts the sequencer using the provided configuration.
|
||||
/// If an existing database is found, the sequencer state is loaded from it and
|
||||
/// assumed to represent the correct latest state consistent with Bedrock-finalized data.
|
||||
/// If no database is found, the sequencer performs a fresh start from genesis,
|
||||
/// initializing its state with the accounts defined in the configuration file.
|
||||
pub async fn start_from_config(
|
||||
config: SequencerConfig,
|
||||
) -> (Self, MemPoolHandle<NSSATransaction>) {
|
||||
let hashable_data = HashableBlockData {
|
||||
block_id: config.genesis_id,
|
||||
transactions: vec![],
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap();
|
||||
let genesis_parent_msg_id = [0; 32];
|
||||
let genesis_block = hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id);
|
||||
|
||||
let bedrock_signing_key =
|
||||
load_or_create_signing_key(&config.home.join("bedrock_signing_key"))
|
||||
.expect("Failed to load or create bedrock signing key");
|
||||
|
||||
let block_settlement_client = BC::new(&config.bedrock_config, bedrock_signing_key)
|
||||
.expect("Failed to initialize Block Settlement Client");
|
||||
|
||||
let indexer_client = IC::new(&config.indexer_rpc_url)
|
||||
.await
|
||||
.expect("Failed to create Indexer Client");
|
||||
|
||||
let (_tx, genesis_msg_id) = block_settlement_client
|
||||
.create_inscribe_tx(&genesis_block)
|
||||
.expect("Failed to create inscribe tx for genesis block");
|
||||
|
||||
// Sequencer should panic if unable to open db,
|
||||
// as fixing this issue may require actions non-native to program scope
|
||||
let store = SequencerStore::open_db_with_genesis(
|
||||
&config.home.join("rocksdb"),
|
||||
&genesis_block,
|
||||
genesis_msg_id.into(),
|
||||
signing_key,
|
||||
)
|
||||
.unwrap();
|
||||
let latest_block_meta = store
|
||||
.latest_block_meta()
|
||||
.expect("Failed to read latest block meta from store");
|
||||
|
||||
#[cfg_attr(not(feature = "testnet"), allow(unused_mut))]
|
||||
let mut state = if let Some(state) = store.get_nssa_state() {
|
||||
info!("Found local database. Loading state and pending blocks from it.");
|
||||
state
|
||||
} else {
|
||||
info!(
|
||||
"No database found when starting the sequencer. Creating a fresh new with the initial data in config"
|
||||
);
|
||||
let initial_commitments: Vec<nssa_core::Commitment> = config
|
||||
.initial_commitments
|
||||
.iter()
|
||||
.map(|init_comm_data| {
|
||||
let npk = &init_comm_data.npk;
|
||||
|
||||
let mut acc = init_comm_data.account.clone();
|
||||
|
||||
acc.program_owner =
|
||||
nssa::program::Program::authenticated_transfer_program().id();
|
||||
|
||||
nssa_core::Commitment::new(npk, &acc)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = config
|
||||
.initial_accounts
|
||||
.iter()
|
||||
.map(|acc_data| (acc_data.account_id, acc_data.balance))
|
||||
.collect();
|
||||
|
||||
nssa::V02State::new_with_genesis_accounts(&init_accs, &initial_commitments)
|
||||
};
|
||||
|
||||
#[cfg(feature = "testnet")]
|
||||
state.add_pinata_program(PINATA_BASE58.parse().unwrap());
|
||||
|
||||
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
||||
|
||||
let sequencer_core = Self {
|
||||
state,
|
||||
store,
|
||||
mempool,
|
||||
chain_height: latest_block_meta.id,
|
||||
sequencer_config: config,
|
||||
block_settlement_client,
|
||||
indexer_client,
|
||||
};
|
||||
|
||||
(sequencer_core, mempool_handle)
|
||||
}
|
||||
|
||||
fn execute_check_transaction_on_state(
|
||||
&mut self,
|
||||
tx: NSSATransaction,
|
||||
) -> Result<NSSATransaction, nssa::error::NssaError> {
|
||||
match &tx {
|
||||
NSSATransaction::Public(tx) => self.state.transition_from_public_transaction(tx),
|
||||
NSSATransaction::PrivacyPreserving(tx) => self
|
||||
.state
|
||||
.transition_from_privacy_preserving_transaction(tx),
|
||||
NSSATransaction::ProgramDeployment(tx) => self
|
||||
.state
|
||||
.transition_from_program_deployment_transaction(tx),
|
||||
}
|
||||
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn produce_new_block(&mut self) -> Result<u64> {
|
||||
let (tx, _msg_id) = self
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.context("Failed to produce new block with mempool transactions")?;
|
||||
match self
|
||||
.block_settlement_client
|
||||
.submit_inscribe_tx_to_bedrock(tx)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
error!("Failed to post block data to Bedrock with error: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.chain_height)
|
||||
}
|
||||
|
||||
/// Produces new block from transactions in mempool and packs it into a `SignedMantleTx`.
|
||||
pub fn produce_new_block_with_mempool_transactions(
|
||||
&mut self,
|
||||
) -> Result<(SignedMantleTx, MsgId)> {
|
||||
let now = Instant::now();
|
||||
|
||||
let new_block_height = self
|
||||
.chain_height
|
||||
.checked_add(1)
|
||||
.with_context(|| format!("Max block height reached: {}", self.chain_height))?;
|
||||
|
||||
let mut valid_transactions = vec![];
|
||||
|
||||
let max_block_size = usize::try_from(self.sequencer_config.max_block_size.as_u64())
|
||||
.expect("`max_block_size` should fit into usize");
|
||||
|
||||
let latest_block_meta = self
|
||||
.store
|
||||
.latest_block_meta()
|
||||
.context("Failed to get latest block meta from store")?;
|
||||
|
||||
let curr_time = u64::try_from(chrono::Utc::now().timestamp_millis())
|
||||
.expect("Timestamp must be positive");
|
||||
|
||||
while let Some(tx) = self.mempool.pop() {
|
||||
let tx_hash = tx.hash();
|
||||
|
||||
// Check if block size exceeds limit
|
||||
let temp_valid_transactions =
|
||||
[valid_transactions.as_slice(), std::slice::from_ref(&tx)].concat();
|
||||
let temp_hashable_data = HashableBlockData {
|
||||
block_id: new_block_height,
|
||||
transactions: temp_valid_transactions,
|
||||
prev_block_hash: latest_block_meta.hash,
|
||||
timestamp: curr_time,
|
||||
};
|
||||
|
||||
let block_size = borsh::to_vec(&temp_hashable_data)
|
||||
.context("Failed to serialize block for size check")?
|
||||
.len();
|
||||
|
||||
if block_size > max_block_size {
|
||||
// Block would exceed size limit, remove last transaction and push back
|
||||
warn!(
|
||||
"Transaction with hash {tx_hash} deferred to next block: \
|
||||
block size {block_size} bytes would exceed limit of {max_block_size} bytes",
|
||||
);
|
||||
|
||||
self.mempool.push_front(tx);
|
||||
break;
|
||||
}
|
||||
|
||||
match self.execute_check_transaction_on_state(tx) {
|
||||
Ok(valid_tx) => {
|
||||
valid_transactions.push(valid_tx);
|
||||
|
||||
info!("Validated transaction with hash {tx_hash}, including it in block");
|
||||
|
||||
if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it",
|
||||
);
|
||||
// TODO: Probably need to handle unsuccessful transaction execution?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hashable_data = HashableBlockData {
|
||||
block_id: new_block_height,
|
||||
transactions: valid_transactions,
|
||||
prev_block_hash: latest_block_meta.hash,
|
||||
timestamp: curr_time,
|
||||
};
|
||||
|
||||
let block = hashable_data
|
||||
.clone()
|
||||
.into_pending_block(self.store.signing_key(), latest_block_meta.msg_id);
|
||||
|
||||
let (tx, msg_id) = self
|
||||
.block_settlement_client
|
||||
.create_inscribe_tx(&block)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to create inscribe transaction for block with id {}",
|
||||
block.header.block_id
|
||||
)
|
||||
})?;
|
||||
|
||||
self.store.update(&block, msg_id.into(), &self.state)?;
|
||||
|
||||
self.chain_height = new_block_height;
|
||||
|
||||
log::info!(
|
||||
"Created block with {} transactions in {} seconds",
|
||||
hashable_data.transactions.len(),
|
||||
now.elapsed().as_secs()
|
||||
);
|
||||
Ok((tx, msg_id))
|
||||
}
|
||||
|
||||
pub const fn state(&self) -> &nssa::V02State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub const fn block_store(&self) -> &SequencerStore {
|
||||
&self.store
|
||||
}
|
||||
|
||||
pub const fn chain_height(&self) -> u64 {
|
||||
self.chain_height
|
||||
}
|
||||
|
||||
pub const fn sequencer_config(&self) -> &SequencerConfig {
|
||||
&self.sequencer_config
|
||||
}
|
||||
|
||||
/// Deletes finalized blocks from the sequencer's pending block list.
|
||||
/// This method must be called when new blocks are finalized on Bedrock.
|
||||
/// All pending blocks with an ID less than or equal to `last_finalized_block_id`
|
||||
/// are removed from the database.
|
||||
pub fn clean_finalized_blocks_from_db(&mut self, last_finalized_block_id: u64) -> Result<()> {
|
||||
self.get_pending_blocks()?
|
||||
.iter()
|
||||
.map(|block| block.header.block_id)
|
||||
.min()
|
||||
.map_or(Ok(()), |first_pending_block_id| {
|
||||
info!("Clearing pending blocks up to id: {last_finalized_block_id}");
|
||||
// TODO: Delete blocks instead of marking them as finalized.
|
||||
// Current approach is used because we still have `GetBlockDataRequest`.
|
||||
(first_pending_block_id..=last_finalized_block_id)
|
||||
.try_for_each(|id| self.store.mark_block_as_finalized(id))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the list of stored pending blocks.
|
||||
pub fn get_pending_blocks(&self) -> Result<Vec<Block>> {
|
||||
Ok(self
|
||||
.store
|
||||
.get_all_blocks()
|
||||
.collect::<Result<Vec<Block>>>()?
|
||||
.into_iter()
|
||||
.filter(|block| matches!(block.bedrock_status, BedrockStatus::Pending))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn block_settlement_client(&self) -> BC {
|
||||
self.block_settlement_client.clone()
|
||||
}
|
||||
|
||||
pub fn indexer_client(&self) -> IC {
|
||||
self.indexer_client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Load signing key from file or generate a new one if it doesn't exist.
|
||||
fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
|
||||
if path.exists() {
|
||||
let key_bytes = std::fs::read(path)?;
|
||||
|
||||
let key_array: [u8; ED25519_SECRET_KEY_SIZE] = key_bytes
|
||||
.try_into()
|
||||
.map_err(|_bytes| anyhow!("Found key with incorrect length"))?;
|
||||
|
||||
Ok(Ed25519Key::from_bytes(&key_array))
|
||||
} else {
|
||||
let mut key_bytes = [0_u8; ED25519_SECRET_KEY_SIZE];
|
||||
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes);
|
||||
// Create parent directory if it doesn't exist
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(path, key_bytes)?;
|
||||
Ok(Ed25519Key::from_bytes(&key_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "mock")]
|
||||
mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use std::{pin::pin, str::FromStr as _, time::Duration};
|
||||
|
||||
use base58::ToBase58 as _;
|
||||
use bedrock_client::BackoffConfig;
|
||||
use common::{
|
||||
block::AccountInitialData, test_utils::sequencer_sign_key_for_testing,
|
||||
transaction::NSSATransaction,
|
||||
};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use mempool::MemPoolHandle;
|
||||
use nssa::{AccountId, PrivateKey};
|
||||
|
||||
use crate::{
|
||||
config::{BedrockConfig, SequencerConfig},
|
||||
mock::SequencerCoreWithMockClients,
|
||||
};
|
||||
|
||||
fn setup_sequencer_config_variable_initial_accounts(
|
||||
initial_accounts: Vec<AccountInitialData>,
|
||||
) -> SequencerConfig {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let home = tempdir.path().to_path_buf();
|
||||
|
||||
SequencerConfig {
|
||||
home,
|
||||
override_rust_log: Some("info".to_owned()),
|
||||
genesis_id: 1,
|
||||
is_genesis_random: false,
|
||||
max_num_tx_in_block: 10,
|
||||
max_block_size: bytesize::ByteSize::mib(1),
|
||||
mempool_max_size: 10000,
|
||||
block_create_timeout: Duration::from_secs(1),
|
||||
port: 8080,
|
||||
initial_accounts,
|
||||
initial_commitments: vec![],
|
||||
signing_key: *sequencer_sign_key_for_testing().value(),
|
||||
bedrock_config: BedrockConfig {
|
||||
backoff: BackoffConfig {
|
||||
start_delay: Duration::from_millis(100),
|
||||
max_retries: 5,
|
||||
},
|
||||
channel_id: ChannelId::from([0; 32]),
|
||||
node_url: "http://not-used-in-unit-tests".parse().unwrap(),
|
||||
auth: None,
|
||||
},
|
||||
retry_pending_blocks_timeout: Duration::from_secs(60 * 4),
|
||||
indexer_rpc_url: "ws://localhost:8779".parse().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_sequencer_config() -> SequencerConfig {
|
||||
let acc1_account_id: Vec<u8> = vec![
|
||||
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83,
|
||||
153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
|
||||
];
|
||||
|
||||
let acc2_account_id: Vec<u8> = vec![
|
||||
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92,
|
||||
94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
|
||||
];
|
||||
|
||||
let initial_acc1 = AccountInitialData {
|
||||
account_id: AccountId::from_str(&acc1_account_id.to_base58()).unwrap(),
|
||||
balance: 10000,
|
||||
};
|
||||
|
||||
let initial_acc2 = AccountInitialData {
|
||||
account_id: AccountId::from_str(&acc2_account_id.to_base58()).unwrap(),
|
||||
balance: 20000,
|
||||
};
|
||||
|
||||
let initial_accounts = vec![initial_acc1, initial_acc2];
|
||||
|
||||
setup_sequencer_config_variable_initial_accounts(initial_accounts)
|
||||
}
|
||||
|
||||
fn create_signing_key_for_account1() -> nssa::PrivateKey {
|
||||
nssa::PrivateKey::try_new([1; 32]).unwrap()
|
||||
}
|
||||
|
||||
fn create_signing_key_for_account2() -> nssa::PrivateKey {
|
||||
nssa::PrivateKey::try_new([2; 32]).unwrap()
|
||||
}
|
||||
|
||||
async fn common_setup() -> (SequencerCoreWithMockClients, MemPoolHandle<NSSATransaction>) {
|
||||
let config = setup_sequencer_config();
|
||||
common_setup_with_config(config).await
|
||||
}
|
||||
|
||||
async fn common_setup_with_config(
|
||||
config: SequencerConfig,
|
||||
) -> (SequencerCoreWithMockClients, MemPoolHandle<NSSATransaction>) {
|
||||
let (mut sequencer, mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config).await;
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
(sequencer, mempool_handle)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_from_config() {
|
||||
let config = setup_sequencer_config();
|
||||
let (sequencer, _mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
|
||||
assert_eq!(sequencer.chain_height, config.genesis_id);
|
||||
assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10);
|
||||
assert_eq!(sequencer.sequencer_config.port, 8080);
|
||||
|
||||
let acc1_account_id = config.initial_accounts[0].account_id;
|
||||
let acc2_account_id = config.initial_accounts[1].account_id;
|
||||
|
||||
let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance;
|
||||
let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance;
|
||||
|
||||
assert_eq!(10000, balance_acc_1);
|
||||
assert_eq!(20000, balance_acc_2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_different_intial_accounts_balances() {
|
||||
let acc1_account_id: Vec<u8> = vec![
|
||||
27, 132, 197, 86, 123, 18, 100, 64, 153, 93, 62, 213, 170, 186, 5, 101, 215, 30, 24,
|
||||
52, 96, 72, 25, 255, 156, 23, 245, 233, 213, 221, 7, 143,
|
||||
];
|
||||
|
||||
let acc2_account_id: Vec<u8> = vec![
|
||||
77, 75, 108, 209, 54, 16, 50, 202, 155, 210, 174, 185, 217, 0, 170, 77, 69, 217, 234,
|
||||
216, 10, 201, 66, 51, 116, 196, 81, 167, 37, 77, 7, 102,
|
||||
];
|
||||
|
||||
let initial_acc1 = AccountInitialData {
|
||||
account_id: AccountId::from_str(&acc1_account_id.to_base58()).unwrap(),
|
||||
balance: 10000,
|
||||
};
|
||||
|
||||
let initial_acc2 = AccountInitialData {
|
||||
account_id: AccountId::from_str(&acc2_account_id.to_base58()).unwrap(),
|
||||
balance: 20000,
|
||||
};
|
||||
|
||||
let initial_accounts = vec![initial_acc1, initial_acc2];
|
||||
|
||||
let config = setup_sequencer_config_variable_initial_accounts(initial_accounts);
|
||||
let (sequencer, _mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
|
||||
let acc1_account_id = config.initial_accounts[0].account_id;
|
||||
let acc2_account_id = config.initial_accounts[1].account_id;
|
||||
|
||||
assert_eq!(
|
||||
10000,
|
||||
sequencer.state.get_account_by_id(acc1_account_id).balance
|
||||
);
|
||||
assert_eq!(
|
||||
20000,
|
||||
sequencer.state.get_account_by_id(acc2_account_id).balance
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_pre_check_pass() {
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
let result = tx.transaction_stateless_check();
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_pre_check_native_transfer_valid() {
|
||||
let (sequencer, _mempool_handle) = common_setup().await;
|
||||
|
||||
let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id;
|
||||
let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id;
|
||||
|
||||
let sign_key1 = create_signing_key_for_account1();
|
||||
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1, 0, acc2, 10, &sign_key1,
|
||||
);
|
||||
let result = tx.transaction_stateless_check();
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_pre_check_native_transfer_other_signature() {
|
||||
let (mut sequencer, _mempool_handle) = common_setup().await;
|
||||
|
||||
let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id;
|
||||
let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id;
|
||||
|
||||
let sign_key2 = create_signing_key_for_account2();
|
||||
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1, 0, acc2, 10, &sign_key2,
|
||||
);
|
||||
|
||||
// Signature is valid, stateless check pass
|
||||
let tx = tx.transaction_stateless_check().unwrap();
|
||||
|
||||
// Signature is not from sender. Execution fails
|
||||
let result = sequencer.execute_check_transaction_on_state(tx);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(nssa::error::NssaError::ProgramExecutionFailed(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_pre_check_native_transfer_sent_too_much() {
|
||||
let (mut sequencer, _mempool_handle) = common_setup().await;
|
||||
|
||||
let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id;
|
||||
let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id;
|
||||
|
||||
let sign_key1 = create_signing_key_for_account1();
|
||||
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1, 0, acc2, 10_000_000, &sign_key1,
|
||||
);
|
||||
|
||||
let result = tx.transaction_stateless_check();
|
||||
|
||||
// Passed pre-check
|
||||
assert!(result.is_ok());
|
||||
|
||||
let result = sequencer.execute_check_transaction_on_state(result.unwrap());
|
||||
let is_failed_at_balance_mismatch = matches!(
|
||||
result.err().unwrap(),
|
||||
nssa::error::NssaError::ProgramExecutionFailed(_)
|
||||
);
|
||||
|
||||
assert!(is_failed_at_balance_mismatch);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_execute_native_transfer() {
|
||||
let (mut sequencer, _mempool_handle) = common_setup().await;
|
||||
|
||||
let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id;
|
||||
let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id;
|
||||
|
||||
let sign_key1 = create_signing_key_for_account1();
|
||||
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1, 0, acc2, 100, &sign_key1,
|
||||
);
|
||||
|
||||
sequencer.execute_check_transaction_on_state(tx).unwrap();
|
||||
|
||||
let bal_from = sequencer.state.get_account_by_id(acc1).balance;
|
||||
let bal_to = sequencer.state.get_account_by_id(acc2).balance;
|
||||
|
||||
assert_eq!(bal_from, 9900);
|
||||
assert_eq!(bal_to, 20100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn push_tx_into_mempool_blocks_until_mempool_is_full() {
|
||||
let config = SequencerConfig {
|
||||
mempool_max_size: 1,
|
||||
..setup_sequencer_config()
|
||||
};
|
||||
let (mut sequencer, mempool_handle) = common_setup_with_config(config).await;
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
|
||||
// Fill the mempool
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
|
||||
// Check that pushing another transaction will block
|
||||
let mut push_fut = pin!(mempool_handle.push(tx.clone()));
|
||||
let poll = futures::poll!(push_fut.as_mut());
|
||||
assert!(poll.is_pending());
|
||||
|
||||
// Empty the mempool by producing a block
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
// Resolve the pending push
|
||||
assert!(push_fut.await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn produce_new_block_with_mempool_transactions() {
|
||||
let (mut sequencer, mempool_handle) = common_setup().await;
|
||||
let genesis_height = sequencer.chain_height;
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
|
||||
let result = sequencer.produce_new_block_with_mempool_transactions();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(sequencer.chain_height, genesis_height + 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replay_transactions_are_rejected_in_the_same_block() {
|
||||
let (mut sequencer, mempool_handle) = common_setup().await;
|
||||
|
||||
let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id;
|
||||
let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id;
|
||||
|
||||
let sign_key1 = create_signing_key_for_account1();
|
||||
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1, 0, acc2, 100, &sign_key1,
|
||||
);
|
||||
|
||||
let tx_original = tx.clone();
|
||||
let tx_replay = tx.clone();
|
||||
// Pushing two copies of the same tx to the mempool
|
||||
mempool_handle.push(tx_original).await.unwrap();
|
||||
mempool_handle.push(tx_replay).await.unwrap();
|
||||
|
||||
// Create block
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
let block = sequencer
|
||||
.store
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap();
|
||||
|
||||
// Only one should be included in the block
|
||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replay_transactions_are_rejected_in_different_blocks() {
|
||||
let (mut sequencer, mempool_handle) = common_setup().await;
|
||||
|
||||
let acc1 = sequencer.sequencer_config.initial_accounts[0].account_id;
|
||||
let acc2 = sequencer.sequencer_config.initial_accounts[1].account_id;
|
||||
|
||||
let sign_key1 = create_signing_key_for_account1();
|
||||
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1, 0, acc2, 100, &sign_key1,
|
||||
);
|
||||
|
||||
// The transaction should be included the first time
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
let block = sequencer
|
||||
.store
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap();
|
||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||
|
||||
// Add same transaction should fail
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
let block = sequencer
|
||||
.store
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap();
|
||||
assert!(block.body.transactions.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restart_from_storage() {
|
||||
let config = setup_sequencer_config();
|
||||
let acc1_account_id = config.initial_accounts[0].account_id;
|
||||
let acc2_account_id = config.initial_accounts[1].account_id;
|
||||
let balance_to_move = 13;
|
||||
|
||||
// In the following code block a transaction will be processed that moves `balance_to_move`
|
||||
// from `acc_1` to `acc_2`. The block created with that transaction will be kept stored in
|
||||
// the temporary directory for the block storage of this test.
|
||||
{
|
||||
let (mut sequencer, mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
let signing_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1_account_id,
|
||||
0,
|
||||
acc2_account_id,
|
||||
balance_to_move,
|
||||
&signing_key,
|
||||
);
|
||||
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
let block = sequencer
|
||||
.store
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap();
|
||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||
}
|
||||
|
||||
// Instantiating a new sequencer from the same config. This should load the existing block
|
||||
// with the above transaction and update the state to reflect that.
|
||||
let (sequencer, _mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance;
|
||||
let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance;
|
||||
|
||||
// Balances should be consistent with the stored block
|
||||
assert_eq!(
|
||||
balance_acc_1,
|
||||
config.initial_accounts[0].balance - balance_to_move
|
||||
);
|
||||
assert_eq!(
|
||||
balance_acc_2,
|
||||
config.initial_accounts[1].balance + balance_to_move
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_pending_blocks() {
|
||||
let config = setup_sequencer_config();
|
||||
let (mut sequencer, _mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config).await;
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 4);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_blocks() {
|
||||
let config = setup_sequencer_config();
|
||||
let (mut sequencer, _mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config).await;
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
let last_finalized_block = 3;
|
||||
sequencer
|
||||
.clean_finalized_blocks_from_db(last_finalized_block)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn produce_block_with_correct_prev_meta_after_restart() {
|
||||
let config = setup_sequencer_config();
|
||||
let acc1_account_id = config.initial_accounts[0].account_id;
|
||||
let acc2_account_id = config.initial_accounts[1].account_id;
|
||||
|
||||
// Step 1: Create initial database with some block metadata
|
||||
let expected_prev_meta = {
|
||||
let (mut sequencer, mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
|
||||
let signing_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
|
||||
// Add a transaction and produce a block to set up block metadata
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1_account_id,
|
||||
0,
|
||||
acc2_account_id,
|
||||
100,
|
||||
&signing_key,
|
||||
);
|
||||
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
// Get the metadata of the last block produced
|
||||
sequencer.store.latest_block_meta().unwrap()
|
||||
};
|
||||
|
||||
// Step 2: Restart sequencer from the same storage
|
||||
let (mut sequencer, mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
|
||||
// Step 3: Submit a new transaction
|
||||
let signing_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
acc1_account_id,
|
||||
1, // Next nonce
|
||||
acc2_account_id,
|
||||
50,
|
||||
&signing_key,
|
||||
);
|
||||
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
|
||||
// Step 4: Produce new block
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
// Step 5: Verify the new block has correct previous block metadata
|
||||
let new_block = sequencer
|
||||
.store
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
new_block.header.prev_block_hash, expected_prev_meta.hash,
|
||||
"New block's prev_block_hash should match the stored metadata hash"
|
||||
);
|
||||
assert_eq!(
|
||||
new_block.bedrock_parent_id, expected_prev_meta.msg_id,
|
||||
"New block's bedrock_parent_id should match the stored metadata msg_id"
|
||||
);
|
||||
assert_eq!(
|
||||
new_block.body.transactions,
|
||||
vec![tx],
|
||||
"New block should contain the submitted transaction"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_from_config_uses_db_height_not_config_genesis() {
|
||||
let mut config = setup_sequencer_config();
|
||||
let original_genesis_id = config.genesis_id;
|
||||
|
||||
// Step 1: Create initial database and produce some blocks
|
||||
let expected_chain_height = {
|
||||
let (mut sequencer, mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
|
||||
// Verify we start with the genesis_id from config
|
||||
assert_eq!(sequencer.chain_height, original_genesis_id);
|
||||
|
||||
// Produce multiple blocks to advance chain height
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
// Return the current chain height (should be genesis_id + 2)
|
||||
sequencer.chain_height
|
||||
};
|
||||
|
||||
// Step 2: Modify the config to have a DIFFERENT genesis_id
|
||||
let different_genesis_id = original_genesis_id + 100;
|
||||
config.genesis_id = different_genesis_id;
|
||||
|
||||
// Step 3: Restart sequencer with the modified config (different genesis_id)
|
||||
let (sequencer, _mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
|
||||
// Step 4: Verify chain_height comes from database, NOT from the new config.genesis_id
|
||||
assert_eq!(
|
||||
sequencer.chain_height, expected_chain_height,
|
||||
"Chain height should be loaded from database metadata, not config.genesis_id"
|
||||
);
|
||||
assert_ne!(
|
||||
sequencer.chain_height, different_genesis_id,
|
||||
"Chain height should NOT match the modified config.genesis_id"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use bedrock_client::SignedMantleTx;
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
block_settlement_client::BlockSettlementClientTrait, config::BedrockConfig,
|
||||
indexer_client::IndexerClientTrait,
|
||||
};
|
||||
|
||||
pub type SequencerCoreWithMockClients =
|
||||
crate::SequencerCore<MockBlockSettlementClient, MockIndexerClient>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MockBlockSettlementClient {
|
||||
bedrock_channel_id: ChannelId,
|
||||
bedrock_signing_key: Ed25519Key,
|
||||
}
|
||||
|
||||
impl BlockSettlementClientTrait for MockBlockSettlementClient {
|
||||
fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result<Self> {
|
||||
Ok(Self {
|
||||
bedrock_channel_id: config.channel_id,
|
||||
bedrock_signing_key: signing_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn bedrock_channel_id(&self) -> ChannelId {
|
||||
self.bedrock_channel_id
|
||||
}
|
||||
|
||||
fn bedrock_signing_key(&self) -> &Ed25519Key {
|
||||
&self.bedrock_signing_key
|
||||
}
|
||||
|
||||
async fn submit_inscribe_tx_to_bedrock(&self, _tx: SignedMantleTx) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MockBlockSettlementClientWithError {
|
||||
bedrock_channel_id: ChannelId,
|
||||
bedrock_signing_key: Ed25519Key,
|
||||
}
|
||||
|
||||
impl BlockSettlementClientTrait for MockBlockSettlementClientWithError {
|
||||
fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result<Self> {
|
||||
Ok(Self {
|
||||
bedrock_channel_id: config.channel_id,
|
||||
bedrock_signing_key: signing_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn bedrock_channel_id(&self) -> ChannelId {
|
||||
self.bedrock_channel_id
|
||||
}
|
||||
|
||||
fn bedrock_signing_key(&self) -> &Ed25519Key {
|
||||
&self.bedrock_signing_key
|
||||
}
|
||||
|
||||
async fn submit_inscribe_tx_to_bedrock(&self, _tx: SignedMantleTx) -> Result<()> {
|
||||
Err(anyhow!("Mock error"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct MockIndexerClient;
|
||||
|
||||
impl IndexerClientTrait for MockIndexerClient {
|
||||
async fn new(_indexer_url: &Url) -> Result<Self> {
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "sequencer_service"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
common.workspace = true
|
||||
sequencer_core = { workspace = true, features = ["testnet"] }
|
||||
sequencer_rpc.workspace = true
|
||||
indexer_service_rpc = { workspace = true, features = ["client"] }
|
||||
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
anyhow.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
actix.workspace = true
|
||||
actix-web.workspace = true
|
||||
tokio.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Runs the sequencer in standalone mode without depending on Bedrock and Indexer services.
|
||||
standalone = ["sequencer_core/mock", "sequencer_rpc/standalone"]
|
||||
@@ -0,0 +1,128 @@
|
||||
# Chef stage - uses pre-built cargo-chef image
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.94.0-slim-trixie AS chef
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libclang-dev \
|
||||
clang \
|
||||
cmake \
|
||||
ninja-build \
|
||||
curl \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install r0vm
|
||||
# Use quick install for x86-64 (risczero provides binaries only for this linux platform)
|
||||
# Manual build for other platforms (including arm64 Linux)
|
||||
RUN ARCH=$(uname -m); \
|
||||
if [ "$ARCH" = "x86_64" ]; then \
|
||||
echo "Using quick install for $ARCH"; \
|
||||
curl -L https://risczero.com/install | bash; \
|
||||
export PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}"; \
|
||||
rzup install; \
|
||||
else \
|
||||
echo "Using manual build for $ARCH"; \
|
||||
git clone --depth 1 --branch release-3.0 https://github.com/risc0/risc0.git; \
|
||||
git clone --depth 1 --branch r0.1.94.0 https://github.com/risc0/rust.git; \
|
||||
cd /risc0; \
|
||||
cargo install --path rzup; \
|
||||
rzup build --path /rust rust --verbose; \
|
||||
cargo install --path risc0/cargo-risczero; \
|
||||
fi
|
||||
ENV PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}"
|
||||
RUN cp "$(which r0vm)" /usr/local/bin/r0vm
|
||||
RUN test -x /usr/local/bin/r0vm
|
||||
RUN r0vm --version
|
||||
|
||||
# Install logos blockchain circuits
|
||||
RUN curl -sSL https://raw.githubusercontent.com/logos-blockchain/logos-blockchain/main/scripts/setup-logos-blockchain-circuits.sh | bash
|
||||
|
||||
WORKDIR /sequencer_service
|
||||
|
||||
# Build argument to enable standalone feature (defaults to false)
|
||||
ARG STANDALONE=false
|
||||
|
||||
# Planner stage - generates dependency recipe
|
||||
FROM chef AS planner
|
||||
COPY . .
|
||||
RUN cargo chef prepare --bin sequencer_service --recipe-path recipe.json
|
||||
|
||||
# Builder stage - builds dependencies and application
|
||||
FROM chef AS builder
|
||||
ARG STANDALONE
|
||||
COPY --from=planner /sequencer_service/recipe.json recipe.json
|
||||
# Build dependencies only (this layer will be cached)
|
||||
RUN if [ "$STANDALONE" = "true" ]; then \
|
||||
cargo chef cook --bin sequencer_service --features standalone --release --recipe-path recipe.json; \
|
||||
else \
|
||||
cargo chef cook --bin sequencer_service --release --recipe-path recipe.json; \
|
||||
fi
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the actual application
|
||||
RUN if [ "$STANDALONE" = "true" ]; then \
|
||||
cargo build --release --features standalone --bin sequencer_service; \
|
||||
else \
|
||||
cargo build --release --bin sequencer_service; \
|
||||
fi
|
||||
|
||||
# Strip debug symbols to reduce binary size
|
||||
RUN strip /sequencer_service/target/release/sequencer_service
|
||||
|
||||
# Runtime stage - minimal image
|
||||
FROM debian:trixie-slim
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y gosu jq \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user for security
|
||||
RUN useradd -m -u 1000 -s /bin/bash sequencer_user && \
|
||||
mkdir -p /sequencer_service /etc/sequencer_service && \
|
||||
chown -R sequencer_user:sequencer_user /sequencer_service /etc/sequencer_service
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder --chown=sequencer_user:sequencer_user /sequencer_service/target/release/sequencer_service /usr/local/bin/sequencer_service
|
||||
|
||||
# Copy r0vm binary from builder
|
||||
COPY --from=builder --chown=sequencer_user:sequencer_user /usr/local/bin/r0vm /usr/local/bin/r0vm
|
||||
|
||||
# Copy logos blockchain circuits from builder
|
||||
COPY --from=builder --chown=sequencer_user:sequencer_user /root/.logos-blockchain-circuits /home/sequencer_user/.logos-blockchain-circuits
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY sequencer_service/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
# Expose default port
|
||||
EXPOSE 3040
|
||||
|
||||
# Health check (TODO #244: Replace when a real health endpoint is available)
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl http://localhost:3040 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{ \
|
||||
\"jsonrpc\": \"2.0\", \
|
||||
\"method\": \"hello\", \
|
||||
\"params\": {}, \
|
||||
\"id\": 1 \
|
||||
}" || exit 1
|
||||
|
||||
# Run the application
|
||||
ENV RUST_LOG=info
|
||||
|
||||
# Set explicit location for r0vm binary
|
||||
ENV RISC0_SERVER_PATH=/usr/local/bin/r0vm
|
||||
|
||||
USER root
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
WORKDIR /sequencer_service
|
||||
CMD ["sequencer_service", "/etc/sequencer_service"]
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"home": ".",
|
||||
"override_rust_log": null,
|
||||
"genesis_id": 1,
|
||||
"is_genesis_random": true,
|
||||
"max_num_tx_in_block": 20,
|
||||
"max_block_size": "1 MiB",
|
||||
"mempool_max_size": 1000,
|
||||
"block_create_timeout": "15s",
|
||||
"retry_pending_blocks_timeout": "5s",
|
||||
"port": 3040,
|
||||
"bedrock_config": {
|
||||
"backoff": {
|
||||
"start_delay": "100ms",
|
||||
"max_retries": 5
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"node_url": "http://localhost:8080"
|
||||
},
|
||||
"indexer_rpc_url": "ws://localhost:8779",
|
||||
"initial_accounts": [
|
||||
{
|
||||
"account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r",
|
||||
"balance": 10000
|
||||
},
|
||||
{
|
||||
"account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2",
|
||||
"balance": 20000
|
||||
}
|
||||
],
|
||||
"initial_commitments": [
|
||||
{
|
||||
"npk": [
|
||||
139,
|
||||
19,
|
||||
158,
|
||||
11,
|
||||
155,
|
||||
231,
|
||||
85,
|
||||
206,
|
||||
132,
|
||||
228,
|
||||
220,
|
||||
114,
|
||||
145,
|
||||
89,
|
||||
113,
|
||||
156,
|
||||
238,
|
||||
142,
|
||||
242,
|
||||
74,
|
||||
182,
|
||||
91,
|
||||
43,
|
||||
100,
|
||||
6,
|
||||
190,
|
||||
31,
|
||||
15,
|
||||
31,
|
||||
88,
|
||||
96,
|
||||
204
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"npk": [
|
||||
173,
|
||||
134,
|
||||
33,
|
||||
223,
|
||||
54,
|
||||
226,
|
||||
10,
|
||||
71,
|
||||
215,
|
||||
254,
|
||||
143,
|
||||
172,
|
||||
24,
|
||||
244,
|
||||
243,
|
||||
208,
|
||||
65,
|
||||
112,
|
||||
118,
|
||||
70,
|
||||
217,
|
||||
240,
|
||||
69,
|
||||
100,
|
||||
129,
|
||||
3,
|
||||
121,
|
||||
25,
|
||||
213,
|
||||
132,
|
||||
42,
|
||||
45
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"signing_key": [
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"home": "/var/lib/sequencer_service",
|
||||
"override_rust_log": null,
|
||||
"genesis_id": 1,
|
||||
"is_genesis_random": true,
|
||||
"max_num_tx_in_block": 20,
|
||||
"max_block_size": "1 MiB",
|
||||
"mempool_max_size": 10000,
|
||||
"block_create_timeout": "10s",
|
||||
"port": 3040,
|
||||
"retry_pending_blocks_timeout": "7s",
|
||||
"bedrock_config": {
|
||||
"backoff": {
|
||||
"start_delay": "100ms",
|
||||
"max_retries": 5
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"node_url": "http://localhost:18080"
|
||||
},
|
||||
"indexer_rpc_url": "ws://localhost:8779",
|
||||
"initial_accounts": [
|
||||
{
|
||||
"account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r",
|
||||
"balance": 10000
|
||||
},
|
||||
{
|
||||
"account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2",
|
||||
"balance": 20000
|
||||
}
|
||||
],
|
||||
"initial_commitments": [
|
||||
{
|
||||
"npk": [
|
||||
139,
|
||||
19,
|
||||
158,
|
||||
11,
|
||||
155,
|
||||
231,
|
||||
85,
|
||||
206,
|
||||
132,
|
||||
228,
|
||||
220,
|
||||
114,
|
||||
145,
|
||||
89,
|
||||
113,
|
||||
156,
|
||||
238,
|
||||
142,
|
||||
242,
|
||||
74,
|
||||
182,
|
||||
91,
|
||||
43,
|
||||
100,
|
||||
6,
|
||||
190,
|
||||
31,
|
||||
15,
|
||||
31,
|
||||
88,
|
||||
96,
|
||||
204
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"npk": [
|
||||
173,
|
||||
134,
|
||||
33,
|
||||
223,
|
||||
54,
|
||||
226,
|
||||
10,
|
||||
71,
|
||||
215,
|
||||
254,
|
||||
143,
|
||||
172,
|
||||
24,
|
||||
244,
|
||||
243,
|
||||
208,
|
||||
65,
|
||||
112,
|
||||
118,
|
||||
70,
|
||||
217,
|
||||
240,
|
||||
69,
|
||||
100,
|
||||
129,
|
||||
3,
|
||||
121,
|
||||
25,
|
||||
213,
|
||||
132,
|
||||
42,
|
||||
45
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"signing_key": [
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
sequencer_service:
|
||||
image: lssa/sequencer_service
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: sequencer/service/Dockerfile
|
||||
container_name: sequencer_service
|
||||
ports:
|
||||
- "3040:3040"
|
||||
volumes:
|
||||
# Mount configuration folder
|
||||
- ./configs/docker:/etc/sequencer_service
|
||||
# Mount data folder
|
||||
- ./data:/var/lib/sequencer_service
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This is an entrypoint script for the sequencer_service Docker container,
|
||||
# it's not meant to be executed outside of the container.
|
||||
|
||||
set -e
|
||||
|
||||
CONFIG="/etc/sequencer/service/sequencer_config.json"
|
||||
|
||||
# Check config file exists
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
echo "Config file not found: $CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse home dir
|
||||
HOME_DIR=$(jq -r '.home' "$CONFIG")
|
||||
|
||||
if [ -z "$HOME_DIR" ] || [ "$HOME_DIR" = "null" ]; then
|
||||
echo "'home' key missing in config" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Give permissions to the data directory and switch to non-root user
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
mkdir -p "$HOME_DIR"
|
||||
chown -R sequencer_user:sequencer_user "$HOME_DIR"
|
||||
exec gosu sequencer_user "$@"
|
||||
fi
|
||||
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "sequencer_rpc"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
common.workspace = true
|
||||
mempool.workspace = true
|
||||
sequencer_core = { workspace = true }
|
||||
bedrock_client.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
actix-cors.workspace = true
|
||||
futures.workspace = true
|
||||
base58.workspace = true
|
||||
hex.workspace = true
|
||||
tempfile.workspace = true
|
||||
base64.workspace = true
|
||||
itertools.workspace = true
|
||||
actix-web.workspace = true
|
||||
tokio.workspace = true
|
||||
borsh.workspace = true
|
||||
bytesize.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
sequencer_core = { workspace = true, features = ["mock"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Includes types to run the sequencer in standalone mode
|
||||
standalone = ["sequencer_core/mock"]
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::{
|
||||
rpc_primitives::errors::{RpcError, RpcErrorKind},
|
||||
transaction::NSSATransaction,
|
||||
};
|
||||
use mempool::MemPoolHandle;
|
||||
pub use net_utils::*;
|
||||
#[cfg(feature = "standalone")]
|
||||
use sequencer_core::mock::{MockBlockSettlementClient, MockIndexerClient};
|
||||
use sequencer_core::{
|
||||
SequencerCore,
|
||||
block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait},
|
||||
indexer_client::{IndexerClient, IndexerClientTrait},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use self::types::err_rpc::RpcErr;
|
||||
|
||||
pub mod net_utils;
|
||||
pub mod process;
|
||||
pub mod types;
|
||||
|
||||
#[cfg(feature = "standalone")]
|
||||
pub type JsonHandlerWithMockClients = JsonHandler<MockBlockSettlementClient, MockIndexerClient>;
|
||||
|
||||
// ToDo: Add necessary fields
|
||||
pub struct JsonHandler<
|
||||
BC: BlockSettlementClientTrait = BlockSettlementClient,
|
||||
IC: IndexerClientTrait = IndexerClient,
|
||||
> {
|
||||
sequencer_state: Arc<Mutex<SequencerCore<BC, IC>>>,
|
||||
mempool_handle: MemPoolHandle<NSSATransaction>,
|
||||
max_block_size: usize,
|
||||
}
|
||||
|
||||
fn respond<T: Serialize>(val: T) -> Result<Value, RpcErr> {
|
||||
Ok(serde_json::to_value(val)?)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn rpc_error_responce_inverter(err: RpcError) -> RpcError {
|
||||
let content = err.error_struct.map(|error| match error {
|
||||
RpcErrorKind::HandlerError(val) | RpcErrorKind::InternalError(val) => val,
|
||||
RpcErrorKind::RequestValidationError(vall) => serde_json::to_value(vall).unwrap(),
|
||||
});
|
||||
RpcError {
|
||||
error_struct: None,
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
data: content,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::{io, net::SocketAddr, sync::Arc};
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, Error as HttpError, HttpResponse, HttpServer, http, middleware, web};
|
||||
use common::{
|
||||
rpc_primitives::{RpcConfig, message::Message},
|
||||
transaction::NSSATransaction,
|
||||
};
|
||||
use futures::{Future, FutureExt as _};
|
||||
use log::info;
|
||||
use mempool::MemPoolHandle;
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
use sequencer_core::SequencerCore;
|
||||
#[cfg(feature = "standalone")]
|
||||
use sequencer_core::SequencerCoreWithMockClients as SequencerCore;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
use super::JsonHandler;
|
||||
use crate::process::Process;
|
||||
|
||||
pub const SHUTDOWN_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
pub const NETWORK: &str = "network";
|
||||
|
||||
#[cfg(feature = "standalone")]
|
||||
type JsonHandler = super::JsonHandlerWithMockClients;
|
||||
|
||||
pub(crate) fn rpc_handler<P: Process>(
|
||||
message: web::Json<Message>,
|
||||
handler: web::Data<P>,
|
||||
) -> impl Future<Output = Result<HttpResponse, HttpError>> {
|
||||
let response = async move {
|
||||
let message = handler.process(message.0).await?;
|
||||
Ok(HttpResponse::Ok().json(&message))
|
||||
};
|
||||
response.boxed()
|
||||
}
|
||||
|
||||
fn get_cors(cors_allowed_origins: &[String]) -> Cors {
|
||||
let mut cors = Cors::permissive();
|
||||
if cors_allowed_origins != ["*".to_owned()] {
|
||||
for origin in cors_allowed_origins {
|
||||
cors = cors.allowed_origin(origin);
|
||||
}
|
||||
}
|
||||
cors.allowed_methods(vec!["GET", "POST"])
|
||||
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||
.allowed_header(http::header::CONTENT_TYPE)
|
||||
.max_age(3600)
|
||||
}
|
||||
|
||||
pub async fn new_http_server(
|
||||
config: RpcConfig,
|
||||
seuquencer_core: Arc<Mutex<SequencerCore>>,
|
||||
mempool_handle: MemPoolHandle<NSSATransaction>,
|
||||
) -> io::Result<(actix_web::dev::Server, SocketAddr)> {
|
||||
let RpcConfig {
|
||||
addr,
|
||||
cors_allowed_origins,
|
||||
limits_config,
|
||||
} = config;
|
||||
info!(target:NETWORK, "Starting HTTP server at {addr}");
|
||||
let max_block_size = seuquencer_core
|
||||
.lock()
|
||||
.await
|
||||
.sequencer_config()
|
||||
.max_block_size
|
||||
.as_u64()
|
||||
.try_into()
|
||||
.expect("`max_block_size` is expected to fit into usize");
|
||||
let handler = web::Data::new(JsonHandler {
|
||||
sequencer_state: Arc::clone(&seuquencer_core),
|
||||
mempool_handle,
|
||||
max_block_size,
|
||||
});
|
||||
|
||||
// HTTP server
|
||||
let http_server = HttpServer::new(move || {
|
||||
let json_limit = limits_config
|
||||
.json_payload_max_size
|
||||
.as_u64()
|
||||
.try_into()
|
||||
.expect("`json_payload_max_size` is expected to fit into usize");
|
||||
App::new()
|
||||
.wrap(get_cors(&cors_allowed_origins))
|
||||
.app_data(handler.clone())
|
||||
.app_data(web::JsonConfig::default().limit(json_limit))
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(web::resource("/").route(web::post().to(rpc_handler::<JsonHandler>)))
|
||||
})
|
||||
.bind(addr)?
|
||||
.shutdown_timeout(SHUTDOWN_TIMEOUT_SECS)
|
||||
.disable_signals();
|
||||
|
||||
let [final_addr] = http_server
|
||||
.addrs()
|
||||
.try_into()
|
||||
.expect("Exactly one address bound is expected for sequencer HTTP server");
|
||||
|
||||
info!(target:NETWORK, "HTTP server started at {final_addr}");
|
||||
|
||||
Ok((http_server.run(), final_addr))
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::Error as HttpError;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use common::{
|
||||
block::{AccountInitialData, HashableBlockData},
|
||||
rpc_primitives::{
|
||||
errors::RpcError,
|
||||
message::{Message, Request},
|
||||
parser::RpcRequest as _,
|
||||
requests::{
|
||||
GetAccountBalanceRequest, GetAccountBalanceResponse, GetAccountRequest,
|
||||
GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse,
|
||||
GetBlockDataRequest, GetBlockDataResponse, GetBlockRangeDataRequest,
|
||||
GetBlockRangeDataResponse, GetGenesisIdRequest, GetGenesisIdResponse,
|
||||
GetInitialTestnetAccountsRequest, GetLastBlockRequest, GetLastBlockResponse,
|
||||
GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest,
|
||||
GetProofForCommitmentResponse, GetTransactionByHashRequest,
|
||||
GetTransactionByHashResponse, HelloRequest, HelloResponse, SendTxRequest,
|
||||
SendTxResponse,
|
||||
},
|
||||
},
|
||||
transaction::{NSSATransaction, TransactionMalformationError},
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use log::warn;
|
||||
use nssa::{self, program::Program};
|
||||
use sequencer_core::{
|
||||
block_settlement_client::BlockSettlementClientTrait, indexer_client::IndexerClientTrait,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{JsonHandler, respond, types::err_rpc::RpcErr};
|
||||
|
||||
pub const HELLO: &str = "hello";
|
||||
pub const SEND_TX: &str = "send_tx";
|
||||
pub const GET_BLOCK: &str = "get_block";
|
||||
pub const GET_BLOCK_RANGE: &str = "get_block_range";
|
||||
pub const GET_GENESIS: &str = "get_genesis";
|
||||
pub const GET_LAST_BLOCK: &str = "get_last_block";
|
||||
pub const GET_ACCOUNT_BALANCE: &str = "get_account_balance";
|
||||
pub const GET_TRANSACTION_BY_HASH: &str = "get_transaction_by_hash";
|
||||
pub const GET_ACCOUNTS_NONCES: &str = "get_accounts_nonces";
|
||||
pub const GET_ACCOUNT: &str = "get_account";
|
||||
pub const GET_PROOF_FOR_COMMITMENT: &str = "get_proof_for_commitment";
|
||||
pub const GET_PROGRAM_IDS: &str = "get_program_ids";
|
||||
|
||||
pub const HELLO_FROM_SEQUENCER: &str = "HELLO_FROM_SEQUENCER";
|
||||
|
||||
pub const TRANSACTION_SUBMITTED: &str = "Transaction submitted";
|
||||
|
||||
pub const GET_INITIAL_TESTNET_ACCOUNTS: &str = "get_initial_testnet_accounts";
|
||||
|
||||
pub trait Process: Send + Sync + 'static {
|
||||
fn process(&self, message: Message) -> impl Future<Output = Result<Message, HttpError>> + Send;
|
||||
}
|
||||
|
||||
impl<
|
||||
BC: BlockSettlementClientTrait + Send + Sync + 'static,
|
||||
IC: IndexerClientTrait + Send + Sync + 'static,
|
||||
> Process for JsonHandler<BC, IC>
|
||||
{
|
||||
async fn process(&self, message: Message) -> Result<Message, HttpError> {
|
||||
let id = message.id();
|
||||
if let Message::Request(request) = message {
|
||||
let message_inner = self
|
||||
.process_request_internal(request)
|
||||
.await
|
||||
.map_err(|e| e.0);
|
||||
Ok(Message::response(id, message_inner))
|
||||
} else {
|
||||
Ok(Message::error(RpcError::parse_error(
|
||||
"JSON RPC Request format was expected".to_owned(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> JsonHandler<BC, IC> {
|
||||
/// Example of request processing.
|
||||
fn process_temp_hello(request: Request) -> Result<Value, RpcErr> {
|
||||
let _hello_request = HelloRequest::parse(Some(request.params))?;
|
||||
|
||||
let response = HelloResponse {
|
||||
greeting: HELLO_FROM_SEQUENCER.to_owned(),
|
||||
};
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
async fn process_send_tx(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
// Check transaction size against block size limit
|
||||
// Reserve ~200 bytes for block header overhead
|
||||
const BLOCK_HEADER_OVERHEAD: usize = 200;
|
||||
|
||||
let send_tx_req = SendTxRequest::parse(Some(request.params))?;
|
||||
let tx = borsh::from_slice::<NSSATransaction>(&send_tx_req.transaction).unwrap();
|
||||
|
||||
let tx_hash = tx.hash();
|
||||
|
||||
let tx_size = send_tx_req.transaction.len();
|
||||
|
||||
let max_tx_size = self.max_block_size.saturating_sub(BLOCK_HEADER_OVERHEAD);
|
||||
|
||||
if tx_size > max_tx_size {
|
||||
return Err(TransactionMalformationError::TransactionTooLarge {
|
||||
size: tx_size,
|
||||
max: max_tx_size,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let authenticated_tx = tx
|
||||
.transaction_stateless_check()
|
||||
.inspect_err(|err| warn!("Error at pre_check {err:#?}"))?;
|
||||
|
||||
// TODO: Do we need a timeout here? It will be usable if we have too many transactions to
|
||||
// process
|
||||
self.mempool_handle
|
||||
.push(authenticated_tx)
|
||||
.await
|
||||
.expect("Mempool is closed, this is a bug");
|
||||
|
||||
let response = SendTxResponse {
|
||||
status: TRANSACTION_SUBMITTED.to_owned(),
|
||||
tx_hash,
|
||||
};
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
async fn process_get_block_data(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_block_req = GetBlockDataRequest::parse(Some(request.params))?;
|
||||
|
||||
let block = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
|
||||
state
|
||||
.block_store()
|
||||
.get_block_at_id(get_block_req.block_id)?
|
||||
};
|
||||
|
||||
let response = GetBlockDataResponse {
|
||||
block: borsh::to_vec(&HashableBlockData::from(block)).unwrap(),
|
||||
};
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
async fn process_get_block_range_data(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_block_req = GetBlockRangeDataRequest::parse(Some(request.params))?;
|
||||
|
||||
let blocks = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
(get_block_req.start_block_id..=get_block_req.end_block_id)
|
||||
.map(|block_id| state.block_store().get_block_at_id(block_id))
|
||||
.map_ok(|block| {
|
||||
borsh::to_vec(&HashableBlockData::from(block))
|
||||
.expect("derived BorshSerialize should never fail")
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
};
|
||||
|
||||
let response = GetBlockRangeDataResponse { blocks };
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
async fn process_get_genesis(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let _get_genesis_req = GetGenesisIdRequest::parse(Some(request.params))?;
|
||||
|
||||
let genesis_id = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
|
||||
state.block_store().genesis_id()
|
||||
};
|
||||
|
||||
let response = GetGenesisIdResponse { genesis_id };
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
async fn process_get_last_block(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let _get_last_block_req = GetLastBlockRequest::parse(Some(request.params))?;
|
||||
|
||||
let last_block = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
|
||||
state.chain_height()
|
||||
};
|
||||
|
||||
let response = GetLastBlockResponse { last_block };
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
/// Returns the initial accounts for testnet.
|
||||
/// `ToDo`: Useful only for testnet and needs to be removed later.
|
||||
async fn get_initial_testnet_accounts(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let _get_initial_testnet_accounts_request =
|
||||
GetInitialTestnetAccountsRequest::parse(Some(request.params))?;
|
||||
|
||||
let initial_accounts: Vec<AccountInitialData> = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
|
||||
state.sequencer_config().initial_accounts.clone()
|
||||
};
|
||||
|
||||
respond(initial_accounts)
|
||||
}
|
||||
|
||||
/// Returns the balance of the account at the given `account_id`.
|
||||
/// The `account_id` must be a valid hex string of the correct length.
|
||||
async fn process_get_account_balance(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_account_req = GetAccountBalanceRequest::parse(Some(request.params))?;
|
||||
let account_id = get_account_req.account_id;
|
||||
|
||||
let balance = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
let account = state.state().get_account_by_id(account_id);
|
||||
account.balance
|
||||
};
|
||||
|
||||
let response = GetAccountBalanceResponse { balance };
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
/// Returns the nonces of the accounts at the given `account_ids`.
|
||||
/// Each `account_id` must be a valid hex string of the correct length.
|
||||
async fn process_get_accounts_nonces(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_account_nonces_req = GetAccountsNoncesRequest::parse(Some(request.params))?;
|
||||
let account_ids = get_account_nonces_req.account_ids;
|
||||
|
||||
let nonces = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
|
||||
account_ids
|
||||
.into_iter()
|
||||
.map(|account_id| state.state().get_account_by_id(account_id).nonce.0)
|
||||
.collect()
|
||||
};
|
||||
|
||||
let response = GetAccountsNoncesResponse { nonces };
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
/// Returns account struct for given `account_id`.
|
||||
/// `AccountId` must be a valid hex string of the correct length.
|
||||
async fn process_get_account(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_account_nonces_req = GetAccountRequest::parse(Some(request.params))?;
|
||||
|
||||
let account_id = get_account_nonces_req.account_id;
|
||||
|
||||
let account = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
|
||||
state.state().get_account_by_id(account_id)
|
||||
};
|
||||
|
||||
let response = GetAccountResponse { account };
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
/// Returns the transaction corresponding to the given hash, if it exists in the blockchain.
|
||||
/// The hash must be a valid hex string of the correct length.
|
||||
async fn process_get_transaction_by_hash(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_transaction_req = GetTransactionByHashRequest::parse(Some(request.params))?;
|
||||
let hash = get_transaction_req.hash;
|
||||
|
||||
let transaction = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
state
|
||||
.block_store()
|
||||
.get_transaction_by_hash(hash)
|
||||
.map(|tx| borsh::to_vec(&tx).unwrap())
|
||||
};
|
||||
let base64_encoded = transaction.map(|tx| general_purpose::STANDARD.encode(tx));
|
||||
let response = GetTransactionByHashResponse {
|
||||
transaction: base64_encoded,
|
||||
};
|
||||
respond(response)
|
||||
}
|
||||
|
||||
/// Returns the commitment proof, corresponding to commitment.
|
||||
async fn process_get_proof_by_commitment(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_proof_req = GetProofForCommitmentRequest::parse(Some(request.params))?;
|
||||
|
||||
let membership_proof = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
state
|
||||
.state()
|
||||
.get_proof_for_commitment(&get_proof_req.commitment)
|
||||
};
|
||||
let response = GetProofForCommitmentResponse { membership_proof };
|
||||
respond(response)
|
||||
}
|
||||
|
||||
fn process_get_program_ids(request: Request) -> Result<Value, RpcErr> {
|
||||
let _get_proof_req = GetProgramIdsRequest::parse(Some(request.params))?;
|
||||
|
||||
let mut program_ids = HashMap::new();
|
||||
program_ids.insert(
|
||||
"authenticated_transfer".to_owned(),
|
||||
Program::authenticated_transfer_program().id(),
|
||||
);
|
||||
program_ids.insert("token".to_owned(), Program::token().id());
|
||||
program_ids.insert("pinata".to_owned(), Program::pinata().id());
|
||||
program_ids.insert("amm".to_owned(), Program::amm().id());
|
||||
program_ids.insert(
|
||||
"privacy_preserving_circuit".to_owned(),
|
||||
nssa::PRIVACY_PRESERVING_CIRCUIT_ID,
|
||||
);
|
||||
let response = GetProgramIdsResponse { program_ids };
|
||||
respond(response)
|
||||
}
|
||||
|
||||
pub async fn process_request_internal(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
match request.method.as_ref() {
|
||||
HELLO => Self::process_temp_hello(request),
|
||||
SEND_TX => self.process_send_tx(request).await,
|
||||
GET_BLOCK => self.process_get_block_data(request).await,
|
||||
GET_BLOCK_RANGE => self.process_get_block_range_data(request).await,
|
||||
GET_GENESIS => self.process_get_genesis(request).await,
|
||||
GET_LAST_BLOCK => self.process_get_last_block(request).await,
|
||||
GET_INITIAL_TESTNET_ACCOUNTS => self.get_initial_testnet_accounts(request).await,
|
||||
GET_ACCOUNT_BALANCE => self.process_get_account_balance(request).await,
|
||||
GET_ACCOUNTS_NONCES => self.process_get_accounts_nonces(request).await,
|
||||
GET_ACCOUNT => self.process_get_account(request).await,
|
||||
GET_TRANSACTION_BY_HASH => self.process_get_transaction_by_hash(request).await,
|
||||
GET_PROOF_FOR_COMMITMENT => self.process_get_proof_by_commitment(request).await,
|
||||
GET_PROGRAM_IDS => Self::process_get_program_ids(request),
|
||||
_ => Err(RpcErr(RpcError::method_not_found(request.method))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{str::FromStr as _, sync::Arc, time::Duration};
|
||||
|
||||
use base58::ToBase58 as _;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use bedrock_client::BackoffConfig;
|
||||
use common::{
|
||||
block::AccountInitialData, config::BasicAuth, test_utils::sequencer_sign_key_for_testing,
|
||||
transaction::NSSATransaction,
|
||||
};
|
||||
use nssa::AccountId;
|
||||
use sequencer_core::{
|
||||
config::{BedrockConfig, SequencerConfig},
|
||||
mock::{MockBlockSettlementClient, MockIndexerClient, SequencerCoreWithMockClients},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tempfile::tempdir;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::rpc_handler;
|
||||
|
||||
type JsonHandlerWithMockClients =
|
||||
crate::JsonHandler<MockBlockSettlementClient, MockIndexerClient>;
|
||||
|
||||
fn sequencer_config_for_tests() -> SequencerConfig {
|
||||
let tempdir = tempdir().unwrap();
|
||||
let home = tempdir.path().to_path_buf();
|
||||
let acc1_id: Vec<u8> = vec![
|
||||
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83,
|
||||
153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
|
||||
];
|
||||
|
||||
let acc2_id: Vec<u8> = vec![
|
||||
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92,
|
||||
94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
|
||||
];
|
||||
|
||||
let initial_acc1 = AccountInitialData {
|
||||
account_id: AccountId::from_str(&acc1_id.to_base58()).unwrap(),
|
||||
balance: 10000,
|
||||
};
|
||||
|
||||
let initial_acc2 = AccountInitialData {
|
||||
account_id: AccountId::from_str(&acc2_id.to_base58()).unwrap(),
|
||||
balance: 20000,
|
||||
};
|
||||
|
||||
let initial_accounts = vec![initial_acc1, initial_acc2];
|
||||
|
||||
SequencerConfig {
|
||||
home,
|
||||
override_rust_log: Some("info".to_owned()),
|
||||
genesis_id: 1,
|
||||
is_genesis_random: false,
|
||||
max_num_tx_in_block: 10,
|
||||
max_block_size: bytesize::ByteSize::mib(1),
|
||||
mempool_max_size: 1000,
|
||||
block_create_timeout: Duration::from_secs(1),
|
||||
port: 8080,
|
||||
initial_accounts,
|
||||
initial_commitments: vec![],
|
||||
signing_key: *sequencer_sign_key_for_testing().value(),
|
||||
retry_pending_blocks_timeout: Duration::from_secs(60 * 4),
|
||||
bedrock_config: BedrockConfig {
|
||||
backoff: BackoffConfig {
|
||||
start_delay: Duration::from_millis(100),
|
||||
max_retries: 5,
|
||||
},
|
||||
channel_id: [42; 32].into(),
|
||||
node_url: "http://localhost:8080".parse().unwrap(),
|
||||
auth: Some(BasicAuth {
|
||||
username: "user".to_owned(),
|
||||
password: None,
|
||||
}),
|
||||
},
|
||||
indexer_rpc_url: "ws://localhost:8779".parse().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn components_for_tests() -> (
|
||||
JsonHandlerWithMockClients,
|
||||
Vec<AccountInitialData>,
|
||||
NSSATransaction,
|
||||
) {
|
||||
let config = sequencer_config_for_tests();
|
||||
|
||||
let (mut sequencer_core, mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config).await;
|
||||
let initial_accounts = sequencer_core.sequencer_config().initial_accounts.clone();
|
||||
|
||||
let signing_key = nssa::PrivateKey::try_new([1; 32]).unwrap();
|
||||
let balance_to_move = 10;
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
AccountId::from_str(
|
||||
&[
|
||||
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237,
|
||||
112, 83, 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
|
||||
]
|
||||
.to_base58(),
|
||||
)
|
||||
.unwrap(),
|
||||
0,
|
||||
AccountId::from_str(&[2; 32].to_base58()).unwrap(),
|
||||
balance_to_move,
|
||||
&signing_key,
|
||||
);
|
||||
|
||||
mempool_handle
|
||||
.push(tx.clone())
|
||||
.await
|
||||
.expect("Mempool is closed, this is a bug");
|
||||
|
||||
sequencer_core
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
let max_block_size =
|
||||
usize::try_from(sequencer_core.sequencer_config().max_block_size.as_u64())
|
||||
.expect("`max_block_size` is expected to fit in usize");
|
||||
let sequencer_core = Arc::new(Mutex::new(sequencer_core));
|
||||
|
||||
(
|
||||
JsonHandlerWithMockClients {
|
||||
sequencer_state: sequencer_core,
|
||||
mempool_handle,
|
||||
max_block_size,
|
||||
},
|
||||
initial_accounts,
|
||||
tx,
|
||||
)
|
||||
}
|
||||
|
||||
async fn call_rpc_handler_with_json(
|
||||
handler: JsonHandlerWithMockClients,
|
||||
request_json: Value,
|
||||
) -> Value {
|
||||
use actix_web::{App, test, web};
|
||||
|
||||
let app = test::init_service(App::new().app_data(web::Data::new(handler)).route(
|
||||
"/",
|
||||
web::post().to(rpc_handler::<JsonHandlerWithMockClients>),
|
||||
))
|
||||
.await;
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/")
|
||||
.set_json(request_json)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&app, req).await;
|
||||
let body = test::read_body(resp).await;
|
||||
|
||||
serde_json::from_slice(&body).unwrap()
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_account_balance_for_non_existent_account() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_account_balance",
|
||||
"params": { "account_id": "11".repeat(16) },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"balance": 0
|
||||
}
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_account_balance_for_invalid_base58() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_account_balance",
|
||||
"params": { "account_id": "not_a_valid_base58" },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"cause": {
|
||||
"info": {
|
||||
"error_message": "Failed parsing args: invalid base58: InvalidBase58Character('_', 3)"
|
||||
},
|
||||
"name": "PARSE_ERROR"
|
||||
},
|
||||
"code": -32700,
|
||||
"data": "Failed parsing args: invalid base58: InvalidBase58Character('_', 3)",
|
||||
"message": "Parse error",
|
||||
"name": "REQUEST_VALIDATION_ERROR"
|
||||
},
|
||||
});
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_account_balance_for_invalid_length() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_account_balance",
|
||||
"params": { "account_id": "cafecafe" },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"cause": {
|
||||
"info": {
|
||||
"error_message": "Failed parsing args: invalid length: expected 32 bytes, got 6"
|
||||
},
|
||||
"name": "PARSE_ERROR"
|
||||
},
|
||||
"code": -32700,
|
||||
"data": "Failed parsing args: invalid length: expected 32 bytes, got 6",
|
||||
"message": "Parse error",
|
||||
"name": "REQUEST_VALIDATION_ERROR"
|
||||
},
|
||||
});
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_account_balance_for_existing_account() {
|
||||
let (json_handler, initial_accounts, _) = components_for_tests().await;
|
||||
|
||||
let acc1_id = initial_accounts[0].account_id;
|
||||
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_account_balance",
|
||||
"params": { "account_id": acc1_id },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"balance": 10000 - 10
|
||||
}
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_accounts_nonces_for_non_existent_account() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_accounts_nonces",
|
||||
"params": { "account_ids": ["11".repeat(16)] },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"nonces": [ 0 ]
|
||||
}
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_accounts_nonces_for_existent_account() {
|
||||
let (json_handler, initial_accounts, _) = components_for_tests().await;
|
||||
|
||||
let acc1_id = initial_accounts[0].account_id;
|
||||
let acc2_id = initial_accounts[1].account_id;
|
||||
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_accounts_nonces",
|
||||
"params": { "account_ids": [acc1_id, acc2_id] },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"nonces": [ 1, 0 ]
|
||||
}
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_account_data_for_non_existent_account() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_account",
|
||||
"params": { "account_id": "11".repeat(16) },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"account": {
|
||||
"balance": 0,
|
||||
"nonce": 0,
|
||||
"program_owner": [ 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"data": [],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_transaction_by_hash_for_non_existent_hash() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_transaction_by_hash",
|
||||
"params": { "hash": "cafe".repeat(16) },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"transaction": null
|
||||
}
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_transaction_by_hash_for_invalid_hex() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_transaction_by_hash",
|
||||
"params": { "hash": "not_a_valid_hex" },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"cause": {
|
||||
"info": {
|
||||
"error_message": "Failed parsing args: Odd number of digits"
|
||||
},
|
||||
"name": "PARSE_ERROR"
|
||||
},
|
||||
"code": -32700,
|
||||
"data": "Failed parsing args: Odd number of digits",
|
||||
"message": "Parse error",
|
||||
"name": "REQUEST_VALIDATION_ERROR"
|
||||
},
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_transaction_by_hash_for_invalid_length() {
|
||||
let (json_handler, _, _) = components_for_tests().await;
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_transaction_by_hash",
|
||||
"params": { "hash": "cafecafe" },
|
||||
"id": 1
|
||||
});
|
||||
let expected_response = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"cause": {
|
||||
"info": {
|
||||
"error_message": "Failed parsing args: Invalid string length"
|
||||
},
|
||||
"name": "PARSE_ERROR"
|
||||
},
|
||||
"code": -32700,
|
||||
"data": "Failed parsing args: Invalid string length",
|
||||
"message": "Parse error",
|
||||
"name": "REQUEST_VALIDATION_ERROR"
|
||||
}
|
||||
});
|
||||
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_transaction_by_hash_for_existing_transaction() {
|
||||
let (json_handler, _, tx) = components_for_tests().await;
|
||||
let tx_hash_hex = hex::encode(tx.hash());
|
||||
let expected_base64_encoded = general_purpose::STANDARD.encode(borsh::to_vec(&tx).unwrap());
|
||||
|
||||
let request = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_transaction_by_hash",
|
||||
"params": { "hash": tx_hash_hex},
|
||||
"id": 1
|
||||
});
|
||||
|
||||
let expected_response = serde_json::json!({
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"transaction": expected_base64_encoded,
|
||||
}
|
||||
});
|
||||
let response = call_rpc_handler_with_json(json_handler, request).await;
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use common::{
|
||||
rpc_primitives::errors::{RpcError, RpcParseError},
|
||||
transaction::TransactionMalformationError,
|
||||
};
|
||||
|
||||
macro_rules! standard_rpc_err_kind {
|
||||
($type_name:path) => {
|
||||
impl RpcErrKind for $type_name {
|
||||
fn into_rpc_err(self) -> RpcError {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct RpcErr(pub RpcError);
|
||||
|
||||
pub type RpcErrInternal = anyhow::Error;
|
||||
|
||||
pub trait RpcErrKind: 'static {
|
||||
fn into_rpc_err(self) -> RpcError;
|
||||
}
|
||||
|
||||
impl<T: RpcErrKind> From<T> for RpcErr {
|
||||
fn from(e: T) -> Self {
|
||||
Self(e.into_rpc_err())
|
||||
}
|
||||
}
|
||||
|
||||
standard_rpc_err_kind!(RpcError);
|
||||
standard_rpc_err_kind!(RpcParseError);
|
||||
|
||||
impl RpcErrKind for serde_json::Error {
|
||||
fn into_rpc_err(self) -> RpcError {
|
||||
RpcError::serialization_error(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcErrKind for RpcErrInternal {
|
||||
fn into_rpc_err(self) -> RpcError {
|
||||
RpcError::new_internal_error(None, &format!("{self:#?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcErrKind for TransactionMalformationError {
|
||||
fn into_rpc_err(self) -> RpcError {
|
||||
RpcError::invalid_params(Some(serde_json::to_value(self).unwrap()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod err_rpc;
|
||||
@@ -0,0 +1,319 @@
|
||||
use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use actix_web::dev::ServerHandle;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Parser;
|
||||
use common::rpc_primitives::RpcConfig;
|
||||
use futures::{FutureExt as _, never::Never};
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
use log::warn;
|
||||
use log::{error, info};
|
||||
#[cfg(feature = "standalone")]
|
||||
use sequencer_core::SequencerCoreWithMockClients as SequencerCore;
|
||||
use sequencer_core::config::SequencerConfig;
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
use sequencer_core::{SequencerCore, block_settlement_client::BlockSettlementClientTrait as _};
|
||||
use sequencer_rpc::new_http_server;
|
||||
use tokio::{sync::Mutex, task::JoinHandle};
|
||||
|
||||
pub const RUST_LOG: &str = "RUST_LOG";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version)]
|
||||
struct Args {
|
||||
/// Path to configs.
|
||||
home_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Handle to manage the sequencer and its tasks.
|
||||
///
|
||||
/// Implements `Drop` to ensure all tasks are aborted and the HTTP server is stopped when dropped.
|
||||
pub struct SequencerHandle {
|
||||
addr: SocketAddr,
|
||||
http_server_handle: ServerHandle,
|
||||
main_loop_handle: JoinHandle<Result<Never>>,
|
||||
retry_pending_blocks_loop_handle: JoinHandle<Result<Never>>,
|
||||
listen_for_bedrock_blocks_loop_handle: JoinHandle<Result<Never>>,
|
||||
}
|
||||
|
||||
impl SequencerHandle {
|
||||
/// Runs the sequencer indefinitely, monitoring its tasks.
|
||||
///
|
||||
/// If no error occurs, this function will never return.
|
||||
#[expect(
|
||||
clippy::integer_division_remainder_used,
|
||||
reason = "Generated by select! macro, can't be easily rewritten to avoid this lint"
|
||||
)]
|
||||
pub async fn run_forever(&mut self) -> Result<Never> {
|
||||
let Self {
|
||||
addr: _,
|
||||
http_server_handle: _,
|
||||
main_loop_handle,
|
||||
retry_pending_blocks_loop_handle,
|
||||
listen_for_bedrock_blocks_loop_handle,
|
||||
} = self;
|
||||
|
||||
tokio::select! {
|
||||
res = main_loop_handle => {
|
||||
res
|
||||
.context("Main loop task panicked")?
|
||||
.context("Main loop exited unexpectedly")
|
||||
}
|
||||
res = retry_pending_blocks_loop_handle => {
|
||||
res
|
||||
.context("Retry pending blocks loop task panicked")?
|
||||
.context("Retry pending blocks loop exited unexpectedly")
|
||||
}
|
||||
res = listen_for_bedrock_blocks_loop_handle => {
|
||||
res
|
||||
.context("Listen for bedrock blocks loop task panicked")?
|
||||
.context("Listen for bedrock blocks loop exited unexpectedly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.main_loop_handle.is_finished()
|
||||
|| self.retry_pending_blocks_loop_handle.is_finished()
|
||||
|| self.listen_for_bedrock_blocks_loop_handle.is_finished()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn addr(&self) -> SocketAddr {
|
||||
self.addr
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SequencerHandle {
|
||||
fn drop(&mut self) {
|
||||
let Self {
|
||||
addr: _,
|
||||
http_server_handle,
|
||||
main_loop_handle,
|
||||
retry_pending_blocks_loop_handle,
|
||||
listen_for_bedrock_blocks_loop_handle,
|
||||
} = self;
|
||||
|
||||
main_loop_handle.abort();
|
||||
retry_pending_blocks_loop_handle.abort();
|
||||
listen_for_bedrock_blocks_loop_handle.abort();
|
||||
|
||||
// Can't wait here as Drop can't be async, but anyway stop signal should be sent
|
||||
http_server_handle.stop(true).now_or_never();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn startup_sequencer(app_config: SequencerConfig) -> Result<SequencerHandle> {
|
||||
let block_timeout = app_config.block_create_timeout;
|
||||
let retry_pending_blocks_timeout = app_config.retry_pending_blocks_timeout;
|
||||
let port = app_config.port;
|
||||
|
||||
let (sequencer_core, mempool_handle) = SequencerCore::start_from_config(app_config).await;
|
||||
|
||||
info!("Sequencer core set up");
|
||||
|
||||
let seq_core_wrapped = Arc::new(Mutex::new(sequencer_core));
|
||||
|
||||
let (http_server, addr) = new_http_server(
|
||||
RpcConfig::with_port(port),
|
||||
Arc::clone(&seq_core_wrapped),
|
||||
mempool_handle,
|
||||
)
|
||||
.await?;
|
||||
info!("HTTP server started");
|
||||
let http_server_handle = http_server.handle();
|
||||
tokio::spawn(http_server);
|
||||
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
{
|
||||
info!("Submitting stored pending blocks");
|
||||
retry_pending_blocks(&seq_core_wrapped)
|
||||
.await
|
||||
.expect("Failed to submit pending blocks on startup");
|
||||
}
|
||||
|
||||
info!("Starting main sequencer loop");
|
||||
let main_loop_handle = tokio::spawn(main_loop(Arc::clone(&seq_core_wrapped), block_timeout));
|
||||
|
||||
info!("Starting pending block retry loop");
|
||||
let retry_pending_blocks_loop_handle = tokio::spawn(retry_pending_blocks_loop(
|
||||
Arc::clone(&seq_core_wrapped),
|
||||
retry_pending_blocks_timeout,
|
||||
));
|
||||
|
||||
info!("Starting bedrock block listening loop");
|
||||
let listen_for_bedrock_blocks_loop_handle =
|
||||
tokio::spawn(listen_for_bedrock_blocks_loop(seq_core_wrapped));
|
||||
|
||||
Ok(SequencerHandle {
|
||||
addr,
|
||||
http_server_handle,
|
||||
main_loop_handle,
|
||||
retry_pending_blocks_loop_handle,
|
||||
listen_for_bedrock_blocks_loop_handle,
|
||||
})
|
||||
}
|
||||
|
||||
async fn main_loop(seq_core: Arc<Mutex<SequencerCore>>, block_timeout: Duration) -> Result<Never> {
|
||||
loop {
|
||||
tokio::time::sleep(block_timeout).await;
|
||||
|
||||
info!("Collecting transactions from mempool, block creation");
|
||||
|
||||
let id = {
|
||||
let mut state = seq_core.lock().await;
|
||||
|
||||
state.produce_new_block().await?
|
||||
};
|
||||
|
||||
info!("Block with id {id} created");
|
||||
|
||||
info!("Waiting for new transactions");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
async fn retry_pending_blocks(seq_core: &Arc<Mutex<SequencerCore>>) -> Result<()> {
|
||||
use std::time::Instant;
|
||||
|
||||
use log::debug;
|
||||
|
||||
let (mut pending_blocks, block_settlement_client) = {
|
||||
let sequencer_core = seq_core.lock().await;
|
||||
let client = sequencer_core.block_settlement_client();
|
||||
let pending_blocks = sequencer_core
|
||||
.get_pending_blocks()
|
||||
.expect("Sequencer should be able to retrieve pending blocks");
|
||||
(pending_blocks, client)
|
||||
};
|
||||
|
||||
pending_blocks.sort_by(|block1, block2| block1.header.block_id.cmp(&block2.header.block_id));
|
||||
|
||||
if !pending_blocks.is_empty() {
|
||||
info!(
|
||||
"Resubmitting blocks from {} to {}",
|
||||
pending_blocks.first().unwrap().header.block_id,
|
||||
pending_blocks.last().unwrap().header.block_id
|
||||
);
|
||||
}
|
||||
|
||||
for block in &pending_blocks {
|
||||
debug!(
|
||||
"Resubmitting pending block with id {}",
|
||||
block.header.block_id
|
||||
);
|
||||
// TODO: We could cache the inscribe tx for each pending block to avoid re-creating it
|
||||
// on every retry.
|
||||
let now = Instant::now();
|
||||
let (tx, _msg_id) = block_settlement_client
|
||||
.create_inscribe_tx(block)
|
||||
.context("Failed to create inscribe tx for pending block")?;
|
||||
|
||||
debug!(">>>> Create inscribe: {:?}", now.elapsed());
|
||||
|
||||
let now = Instant::now();
|
||||
if let Err(e) = block_settlement_client
|
||||
.submit_inscribe_tx_to_bedrock(tx)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to resubmit block with id {} with error {e:#}",
|
||||
block.header.block_id
|
||||
);
|
||||
}
|
||||
debug!(">>>> Post: {:?}", now.elapsed());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
async fn retry_pending_blocks_loop(
|
||||
seq_core: Arc<Mutex<SequencerCore>>,
|
||||
retry_pending_blocks_timeout: Duration,
|
||||
) -> Result<Never> {
|
||||
loop {
|
||||
tokio::time::sleep(retry_pending_blocks_timeout).await;
|
||||
retry_pending_blocks(&seq_core).await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
async fn listen_for_bedrock_blocks_loop(seq_core: Arc<Mutex<SequencerCore>>) -> Result<Never> {
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
|
||||
let indexer_client = seq_core.lock().await.indexer_client();
|
||||
|
||||
let retry_delay = Duration::from_secs(5);
|
||||
|
||||
loop {
|
||||
// TODO: Subscribe from the first pending block ID?
|
||||
let mut subscription = indexer_client
|
||||
.subscribe_to_finalized_blocks()
|
||||
.await
|
||||
.context("Failed to subscribe to finalized blocks")?;
|
||||
|
||||
while let Some(block_id) = subscription.next().await {
|
||||
let block_id = block_id.context("Failed to get next block from subscription")?;
|
||||
|
||||
info!("Received new L2 block with ID {block_id}");
|
||||
|
||||
seq_core
|
||||
.lock()
|
||||
.await
|
||||
.clean_finalized_blocks_from_db(block_id)
|
||||
.with_context(|| {
|
||||
format!("Failed to clean finalized blocks from DB for block ID {block_id}")
|
||||
})?;
|
||||
}
|
||||
|
||||
warn!(
|
||||
"Block subscription closed unexpectedly, reason: {:?}, retrying after {retry_delay:?}",
|
||||
subscription.close_reason()
|
||||
);
|
||||
tokio::time::sleep(retry_delay).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "standalone")]
|
||||
async fn listen_for_bedrock_blocks_loop(_seq_core: Arc<Mutex<SequencerCore>>) -> Result<Never> {
|
||||
std::future::pending::<Result<Never>>().await
|
||||
}
|
||||
|
||||
#[cfg(feature = "standalone")]
|
||||
async fn retry_pending_blocks_loop(
|
||||
_seq_core: Arc<Mutex<SequencerCore>>,
|
||||
_retry_pending_blocks_timeout: Duration,
|
||||
) -> Result<Never> {
|
||||
std::future::pending::<Result<Never>>().await
|
||||
}
|
||||
|
||||
pub async fn main_runner() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let args = Args::parse();
|
||||
let Args { home_dir } = args;
|
||||
|
||||
let app_config = SequencerConfig::from_path(&home_dir.join("sequencer_config.json"))?;
|
||||
|
||||
if let Some(rust_log) = &app_config.override_rust_log {
|
||||
info!("RUST_LOG env var set to {rust_log:?}");
|
||||
|
||||
// SAFETY: there is no other threads running at this point
|
||||
unsafe {
|
||||
std::env::set_var(RUST_LOG, rust_log);
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo: Add restart on failures
|
||||
let mut sequencer_handle = startup_sequencer(app_config).await?;
|
||||
|
||||
info!("Sequencer running. Monitoring concurrent tasks...");
|
||||
|
||||
let Err(err) = sequencer_handle.run_forever().await;
|
||||
error!("Sequencer failed: {err:#}");
|
||||
|
||||
info!("Shutting down sequencer...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use sequencer_service::main_runner;
|
||||
|
||||
pub const NUM_THREADS: usize = 4;
|
||||
|
||||
// TODO: Why it requires config as a directory and not as a file?
|
||||
fn main() -> Result<()> {
|
||||
actix::System::with_tokio_rt(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(NUM_THREADS)
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
})
|
||||
.block_on(main_runner())
|
||||
}
|
||||
Reference in New Issue
Block a user