Merge branch 'arjentix/fix-sequencer-msg-id' into Pravdyvy/indexer-state-management

This commit is contained in:
Pravdyvy 2026-02-16 10:16:25 +02:00
commit 344430a8f2
34 changed files with 181 additions and 146 deletions

10
Cargo.lock generated
View File

@ -2469,7 +2469,6 @@ dependencies = [
"console_error_panic_hook",
"console_log",
"env_logger",
"hex",
"indexer_service_protocol",
"indexer_service_rpc",
"jsonrpsee",
@ -3403,12 +3402,16 @@ dependencies = [
name = "indexer_service_protocol"
version = "0.1.0"
dependencies = [
"anyhow",
"base58",
"base64 0.22.1",
"common",
"hex",
"nssa",
"nssa_core",
"schemars 1.2.1",
"serde",
"serde_with",
]
[[package]]
@ -3833,9 +3836,9 @@ dependencies = [
[[package]]
name = "keccak"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
]
@ -8190,7 +8193,6 @@ dependencies = [
"amm_core",
"anyhow",
"async-stream",
"base58",
"base64 0.22.1",
"borsh",
"bytemuck",

View File

@ -11,7 +11,7 @@ services:
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657
ports:
# Map 0 port so that multiple instances can run on the same host
- "8080:18080/tcp"
- "0:18080/tcp"
volumes:
- ./scripts:/etc/logos-blockchain/scripts
- ./kzgrs_test_params:/kzgrs_test_params:z

View File

@ -26,9 +26,6 @@ console_log = "1.0"
# Date/Time
chrono.workspace = true
# Hex encoding/decoding
hex.workspace = true
# URL encoding
urlencoding = "2.1"

View File

@ -25,13 +25,6 @@ pub async fn get_account(account_id: AccountId) -> Result<Account, ServerFnError
.map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e)))
}
/// Parse hex string to bytes
#[cfg(feature = "ssr")]
fn parse_hex(s: &str) -> Option<Vec<u8>> {
let s = s.trim().trim_start_matches("0x");
hex::decode(s).ok()
}
/// Search for a block, transaction, or account by query string
#[server]
pub async fn search(query: String) -> Result<SearchResults, ServerFnError> {
@ -42,12 +35,8 @@ pub async fn search(query: String) -> Result<SearchResults, ServerFnError> {
let mut transactions = Vec::new();
let mut accounts = Vec::new();
// Try to parse as hash (32 bytes)
if let Some(bytes) = parse_hex(&query)
&& let Ok(hash_array) = <[u8; 32]>::try_from(bytes)
{
let hash = HashType(hash_array);
// Try as hash
if let Ok(hash) = HashType::from_str(&query) {
// Try as block hash
if let Ok(block) = client.get_block_by_hash(hash).await {
blocks.push(block);
@ -57,12 +46,13 @@ pub async fn search(query: String) -> Result<SearchResults, ServerFnError> {
if let Ok(tx) = client.get_transaction(hash).await {
transactions.push(tx);
}
}
// Try as account ID
let account_id = AccountId { value: hash_array };
if let Ok(account) = client.get_account(account_id).await {
accounts.push((account_id, account));
}
// Try as account ID
if let Ok(account_id) = AccountId::from_str(&query)
&& let Ok(account) = client.get_account(account_id).await
{
accounts.push((account_id, account));
}
// Try as block ID

View File

@ -2,12 +2,10 @@ use indexer_service_protocol::{Account, AccountId};
use leptos::prelude::*;
use leptos_router::components::A;
use crate::format_utils;
/// Account preview component
#[component]
pub fn AccountPreview(account_id: AccountId, account: Account) -> impl IntoView {
let account_id_str = format_utils::format_account_id(&account_id);
let account_id_str = account_id.to_string();
view! {
<div class="account-preview">
@ -20,7 +18,7 @@ pub fn AccountPreview(account_id: AccountId, account: Account) -> impl IntoView
</div>
{move || {
let Account { program_owner, balance, data, nonce } = &account;
let program_id = format_utils::format_program_id(program_owner);
let program_id = program_owner.to_string();
view! {
<div class="account-preview-body">
<div class="account-field">

View File

@ -32,8 +32,8 @@ pub fn BlockPreview(block: Block) -> impl IntoView {
let tx_count = transactions.len();
let hash_str = hex::encode(hash.0);
let prev_hash_str = hex::encode(prev_block_hash.0);
let hash_str = hash.to_string();
let prev_hash_str = prev_block_hash.to_string();
let time_str = format_utils::format_timestamp(timestamp);
let status_str = match &bedrock_status {
BedrockStatus::Pending => "Pending",

View File

@ -15,7 +15,7 @@ fn transaction_type_info(tx: &Transaction) -> (&'static str, &'static str) {
#[component]
pub fn TransactionPreview(transaction: Transaction) -> impl IntoView {
let hash = transaction.hash();
let hash_str = hex::encode(hash.0);
let hash_str = hash.to_string();
let (type_name, type_class) = transaction_type_info(&transaction);
// Get additional metadata based on transaction type

View File

@ -1,7 +1,5 @@
//! Formatting utilities for the explorer
use indexer_service_protocol::{AccountId, ProgramId};
/// Format timestamp to human-readable string
pub fn format_timestamp(timestamp: u64) -> String {
let seconds = timestamp / 1000;
@ -9,25 +7,3 @@ pub fn format_timestamp(timestamp: u64) -> String {
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap());
datetime.format("%Y-%m-%d %H:%M:%S UTC").to_string()
}
/// Format hash (32 bytes) to hex string
pub fn format_hash(hash: &[u8; 32]) -> String {
hex::encode(hash)
}
/// Format account ID to hex string
pub fn format_account_id(account_id: &AccountId) -> String {
hex::encode(account_id.value)
}
/// Format program ID to hex string
pub fn format_program_id(program_id: &ProgramId) -> String {
let bytes: Vec<u8> = program_id.iter().flat_map(|n| n.to_be_bytes()).collect();
hex::encode(bytes)
}
/// Parse hex string to bytes
pub fn parse_hex(s: &str) -> Option<Vec<u8>> {
let s = s.trim().trim_start_matches("0x");
hex::decode(s).ok()
}

View File

@ -1,8 +1,10 @@
use std::str::FromStr as _;
use indexer_service_protocol::{Account, AccountId};
use leptos::prelude::*;
use leptos_router::hooks::use_params_map;
use crate::{api, components::TransactionPreview, format_utils};
use crate::{api, components::TransactionPreview};
/// Account page component
#[component]
@ -17,16 +19,7 @@ pub fn AccountPage() -> impl IntoView {
// Parse account ID from URL params
let account_id = move || {
let account_id_str = params.read().get("id").unwrap_or_default();
format_utils::parse_hex(&account_id_str).and_then(|bytes| {
if bytes.len() == 32 {
let account_id_array: [u8; 32] = bytes.try_into().ok()?;
Some(AccountId {
value: account_id_array,
})
} else {
None
}
})
AccountId::from_str(&account_id_str).ok()
};
// Load account data
@ -98,8 +91,8 @@ pub fn AccountPage() -> impl IntoView {
} = acc;
let acc_id = account_id().expect("Account ID should be set");
let account_id_str = format_utils::format_account_id(&acc_id);
let program_id = format_utils::format_program_id(&program_owner);
let account_id_str = acc_id.to_string();
let program_id = program_owner.to_string();
let balance_str = balance.to_string();
let nonce_str = nonce.to_string();
let data_len = data.0.len();

View File

@ -1,3 +1,5 @@
use std::str::FromStr as _;
use indexer_service_protocol::{BedrockStatus, Block, BlockBody, BlockHeader, BlockId, HashType};
use leptos::prelude::*;
use leptos_router::{components::A, hooks::use_params_map};
@ -25,11 +27,8 @@ pub fn BlockPage() -> impl IntoView {
}
// Try to parse as block hash (hex string)
let id_str = id_str.trim().trim_start_matches("0x");
if let Some(bytes) = format_utils::parse_hex(id_str)
&& let Ok(hash_array) = <[u8; 32]>::try_from(bytes)
{
return Some(BlockIdOrHash::Hash(HashType(hash_array)));
if let Ok(hash) = HashType::from_str(&id_str) {
return Some(BlockIdOrHash::Hash(hash));
}
None
@ -68,10 +67,10 @@ pub fn BlockPage() -> impl IntoView {
bedrock_parent_id: _,
} = blk;
let hash_str = format_utils::format_hash(&hash.0);
let prev_hash = format_utils::format_hash(&prev_block_hash.0);
let hash_str = hash.to_string();
let prev_hash = prev_block_hash.to_string();
let timestamp_str = format_utils::format_timestamp(timestamp);
let signature_str = hex::encode(signature.0);
let signature_str = signature.to_string();
let status = match &bedrock_status {
BedrockStatus::Pending => "Pending",
BedrockStatus::Safe => "Safe",

View File

@ -1,3 +1,5 @@
use std::str::FromStr as _;
use indexer_service_protocol::{
HashType, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, PublicMessage, PublicTransaction, Transaction, WitnessSet,
@ -5,7 +7,7 @@ use indexer_service_protocol::{
use leptos::prelude::*;
use leptos_router::{components::A, hooks::use_params_map};
use crate::{api, format_utils};
use crate::api;
/// Transaction page component
#[component]
@ -14,15 +16,10 @@ pub fn TransactionPage() -> impl IntoView {
let transaction_resource = Resource::new(
move || {
let tx_hash_str = params.read().get("hash").unwrap_or_default();
format_utils::parse_hex(&tx_hash_str).and_then(|bytes| {
if bytes.len() == 32 {
let hash_array: [u8; 32] = bytes.try_into().ok()?;
Some(HashType(hash_array))
} else {
None
}
})
params
.read()
.get("hash")
.and_then(|s| HashType::from_str(&s).ok())
},
|hash_opt| async move {
match hash_opt {
@ -42,7 +39,7 @@ pub fn TransactionPage() -> impl IntoView {
.get()
.map(|result| match result {
Ok(tx) => {
let tx_hash = format_utils::format_hash(&tx.hash().0);
let tx_hash = tx.hash().to_string();
let tx_type = match &tx {
Transaction::Public(_) => "Public Transaction",
Transaction::PrivacyPreserving(_) => "Privacy-Preserving Transaction",
@ -86,10 +83,7 @@ pub fn TransactionPage() -> impl IntoView {
proof,
} = witness_set;
let program_id_str = program_id
.iter()
.map(|n| format!("{:08x}", n))
.collect::<String>();
let program_id_str = program_id.to_string();
let proof_len = proof.0.len();
let signatures_count = signatures_and_public_keys.len();
@ -123,7 +117,7 @@ pub fn TransactionPage() -> impl IntoView {
.into_iter()
.zip(nonces.into_iter())
.map(|(account_id, nonce)| {
let account_id_str = format_utils::format_account_id(&account_id);
let account_id_str = account_id.to_string();
view! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>
@ -197,7 +191,7 @@ pub fn TransactionPage() -> impl IntoView {
.into_iter()
.zip(nonces.into_iter())
.map(|(account_id, nonce)| {
let account_id_str = format_utils::format_account_id(&account_id);
let account_id_str = account_id.to_string();
view! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>

View File

@ -10,8 +10,12 @@ nssa = { workspace = true, optional = true }
common = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
serde_with.workspace = true
schemars.workspace = true
base64.workspace = true
base58.workspace = true
hex.workspace = true
anyhow.workspace = true
[features]
# Enable conversion to/from NSSA core types

View File

@ -6,6 +6,18 @@ use crate::*;
// Account-related conversions
// ============================================================================
impl From<[u32; 8]> for ProgramId {
fn from(value: [u32; 8]) -> Self {
Self(value)
}
}
impl From<ProgramId> for [u32; 8] {
fn from(value: ProgramId) -> Self {
value.0
}
}
impl From<nssa_core::account::AccountId> for AccountId {
fn from(value: nssa_core::account::AccountId) -> Self {
Self {
@ -31,7 +43,7 @@ impl From<nssa_core::account::Account> for Account {
} = value;
Self {
program_owner,
program_owner: program_owner.into(),
balance,
data: data.into(),
nonce,
@ -51,7 +63,7 @@ impl TryFrom<Account> for nssa_core::account::Account {
} = value;
Ok(nssa_core::account::Account {
program_owner,
program_owner: program_owner.into(),
balance,
data: data.try_into()?,
nonce,
@ -230,7 +242,7 @@ impl From<nssa::public_transaction::Message> for PublicMessage {
instruction_data,
} = value;
Self {
program_id,
program_id: program_id.into(),
account_ids: account_ids.into_iter().map(Into::into).collect(),
nonces,
instruction_data,
@ -247,7 +259,7 @@ impl From<PublicMessage> for nssa::public_transaction::Message {
instruction_data,
} = value;
Self::new_preserialized(
program_id,
program_id.into(),
account_ids.into_iter().map(Into::into).collect(),
nonces,
instruction_data,

View File

@ -3,23 +3,81 @@
//! Currently it mostly mimics types from `nssa_core`, but it's important to have a separate crate
//! to define a stable interface for the indexer service RPCs which evolves in its own way.
use std::{fmt::Display, str::FromStr};
use anyhow::anyhow;
use base58::{FromBase58 as _, ToBase58 as _};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
#[cfg(feature = "convert")]
mod convert;
pub type Nonce = u128;
pub type ProgramId = [u32; 8];
#[derive(
Debug, Copy, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr, JsonSchema,
)]
pub struct ProgramId(pub [u32; 8]);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
impl Display for ProgramId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let bytes: Vec<u8> = self.0.iter().flat_map(|n| n.to_be_bytes()).collect();
write!(f, "{}", bytes.to_base58())
}
}
impl FromStr for ProgramId {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s
.from_base58()
.map_err(|_| hex::FromHexError::InvalidStringLength)?;
if bytes.len() != 32 {
return Err(hex::FromHexError::InvalidStringLength);
}
let mut arr = [0u32; 8];
for (i, chunk) in bytes.chunks_exact(4).enumerate() {
arr[i] = u32::from_be_bytes(chunk.try_into().unwrap());
}
Ok(ProgramId(arr))
}
}
#[derive(
Debug, Copy, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr, JsonSchema,
)]
pub struct AccountId {
#[serde(with = "base64::arr")]
#[schemars(with = "String", description = "base64-encoded account ID")]
pub value: [u8; 32],
}
impl Display for AccountId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value.to_base58())
}
}
impl FromStr for AccountId {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s
.from_base58()
.map_err(|err| anyhow!("invalid base58: {err:?}"))?;
if bytes.len() != 32 {
return Err(anyhow!(
"invalid length: expected 32 bytes, got {}",
bytes.len()
));
}
let mut value = [0u8; 32];
value.copy_from_slice(&bytes);
Ok(AccountId { value })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct Account {
pub program_owner: ProgramId,
@ -48,13 +106,27 @@ pub struct BlockHeader {
pub signature: Signature,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr, JsonSchema)]
pub struct Signature(
#[serde(with = "base64::arr")]
#[schemars(with = "String", description = "base64-encoded signature")]
pub [u8; 64],
#[schemars(with = "String", description = "hex-encoded signature")] pub [u8; 64],
);
impl Display for Signature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl FromStr for Signature {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0u8; 64];
hex::decode_to_slice(s, &mut bytes)?;
Ok(Signature(bytes))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct BlockBody {
pub transactions: Vec<Transaction>,
@ -196,12 +268,26 @@ pub struct Data(
pub Vec<u8>,
);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct HashType(
#[serde(with = "base64::arr")]
#[schemars(with = "String", description = "base64-encoded hash")]
pub [u8; 32],
);
#[derive(
Debug, Copy, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr, JsonSchema,
)]
pub struct HashType(pub [u8; 32]);
impl Display for HashType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl FromStr for HashType {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0u8; 32];
hex::decode_to_slice(s, &mut bytes)?;
Ok(HashType(bytes))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct MantleMsgId(

View File

@ -4,8 +4,8 @@ use indexer_service_protocol::{
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment,
CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId,
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, PublicMessage, PublicTransaction, Signature, Transaction,
WitnessSet,
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
Transaction, WitnessSet,
};
use jsonrpsee::{core::SubscriptionResult, types::ErrorObjectOwned};
@ -35,7 +35,7 @@ impl MockIndexerService {
accounts.insert(
*account_id,
Account {
program_owner: [i as u32; 8],
program_owner: ProgramId([i as u32; 8]),
balance: 1000 * (i as u128 + 1),
data: Data(vec![0xaa, 0xbb, 0xcc]),
nonce: i as u128,
@ -73,7 +73,7 @@ impl MockIndexerService {
0 | 1 => Transaction::Public(PublicTransaction {
hash: tx_hash,
message: PublicMessage {
program_id: [1u32; 8],
program_id: ProgramId([1u32; 8]),
account_ids: vec![
account_ids[tx_idx as usize % account_ids.len()],
account_ids[(tx_idx as usize + 1) % account_ids.len()],
@ -95,7 +95,7 @@ impl MockIndexerService {
],
nonces: vec![block_id as u128],
public_post_states: vec![Account {
program_owner: [1u32; 8],
program_owner: ProgramId([1u32; 8]),
balance: 500,
data: Data(vec![0xdd, 0xee]),
nonce: block_id as u128,

View File

@ -173,24 +173,10 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
Ok(tx)
}
pub async fn produce_new_block_and_post_to_settlement_layer(&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(()) => {
info!("Posted block data to Bedrock, msg_id: {msg_id:?}");
}
Err(err) => {
error!("Failed to post block data to Bedrock with error: {err:#}");
}
}
}
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")?;
Ok(self.chain_height)
}

View File

@ -5,8 +5,8 @@
"is_genesis_random": true,
"max_num_tx_in_block": 20,
"mempool_max_size": 1000,
"block_create_timeout_millis": 5000,
"retry_pending_blocks_timeout_millis": 7000,
"block_create_timeout_millis": 12000,
"retry_pending_blocks_timeout_millis": 6000,
"port": 3040,
"bedrock_config": {
"backoff": {

View File

@ -147,9 +147,7 @@ async fn main_loop(seq_core: Arc<Mutex<SequencerCore>>, block_timeout: Duration)
let id = {
let mut state = seq_core.lock().await;
state
.produce_new_block_and_post_to_settlement_layer()
.await?
state.produce_new_block().await?
};
info!("Block with id {id} created");
@ -174,7 +172,10 @@ async fn retry_pending_blocks_loop(
(pending_blocks, client)
};
if let Some(block) = pending_blocks.first() {
if let Some(block) = pending_blocks
.iter()
.min_by_key(|block| block.header.block_id)
{
info!(
"Resubmitting pending block with id {}",
block.header.block_id

View File

@ -22,7 +22,6 @@ clap.workspace = true
base64.workspace = true
bytemuck.workspace = true
borsh.workspace = true
base58.workspace = true
hex.workspace = true
rand.workspace = true
itertools.workspace = true

View File

@ -1,5 +1,4 @@
use anyhow::Result;
use base58::ToBase58;
use clap::Subcommand;
use itertools::Itertools as _;
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
@ -104,8 +103,7 @@ impl WalletSubcommand for NewSubcommand {
.unwrap();
println!(
"Generated new account with account_id Private/{} at path {chain_index}",
account_id.to_bytes().to_base58()
"Generated new account with account_id Private/{account_id} at path {chain_index}",
);
println!("With npk {}", hex::encode(key.nullifer_public_key.0));
println!(