feat: refactor sequencer RPC server-side

This commit is contained in:
Daniil Polyakov 2026-03-12 15:43:18 +03:00
parent bffc711470
commit be94e133fa
51 changed files with 677 additions and 3126 deletions

411
Cargo.lock generated
View File

@ -2,229 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "actix"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
dependencies = [
"actix-macros",
"actix-rt",
"actix_derive",
"bitflags 2.11.0",
"bytes",
"crossbeam-channel",
"futures-core",
"futures-sink",
"futures-task",
"futures-util",
"log",
"once_cell",
"parking_lot",
"pin-project-lite",
"smallvec",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-codec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [
"bitflags 2.11.0",
"bytes",
"futures-core",
"futures-sink",
"memchr",
"pin-project-lite",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "actix-cors"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
dependencies = [
"actix-utils",
"actix-web",
"derive_more",
"futures-util",
"log",
"once_cell",
"smallvec",
]
[[package]]
name = "actix-http"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e"
dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"actix-utils",
"bitflags 2.11.0",
"bytes",
"bytestring",
"derive_more",
"encoding_rs",
"foldhash",
"futures-core",
"http 0.2.12",
"httparse",
"httpdate",
"itoa",
"language-tags",
"mime",
"percent-encoding",
"pin-project-lite",
"smallvec",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "actix-macros"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [
"quote",
"syn 2.0.117",
]
[[package]]
name = "actix-router"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7"
dependencies = [
"bytestring",
"cfg-if",
"http 0.2.12",
"regex-lite",
"serde",
"tracing",
]
[[package]]
name = "actix-rt"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
dependencies = [
"futures-core",
"tokio",
]
[[package]]
name = "actix-server"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
dependencies = [
"actix-rt",
"actix-service",
"actix-utils",
"futures-core",
"futures-util",
"mio",
"socket2 0.5.10",
"tokio",
"tracing",
]
[[package]]
name = "actix-service"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "actix-utils"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
dependencies = [
"local-waker",
"pin-project-lite",
]
[[package]]
name = "actix-web"
version = "4.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf"
dependencies = [
"actix-codec",
"actix-http",
"actix-macros",
"actix-router",
"actix-rt",
"actix-server",
"actix-service",
"actix-utils",
"actix-web-codegen",
"bytes",
"bytestring",
"cfg-if",
"derive_more",
"encoding_rs",
"foldhash",
"futures-core",
"futures-util",
"impl-more",
"itoa",
"language-tags",
"log",
"mime",
"once_cell",
"pin-project-lite",
"regex-lite",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"socket2 0.6.3",
"time",
"tracing",
"url",
]
[[package]]
name = "actix-web-codegen"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
dependencies = [
"actix-router",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "actix_derive"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "addchain"
version = "0.2.1"
@ -1011,7 +788,7 @@ dependencies = [
"axum-core 0.4.5",
"bytes",
"futures-util",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"hyper",
@ -1045,7 +822,7 @@ dependencies = [
"bytes",
"form_urlencoded",
"futures-util",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"hyper",
@ -1080,7 +857,7 @@ dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"mime",
@ -1099,7 +876,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"mime",
@ -1313,7 +1090,7 @@ dependencies = [
"futures-util",
"hex",
"home",
"http 1.4.0",
"http",
"http-body-util",
"hyper",
"hyper-named-pipe",
@ -1466,15 +1243,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "bytestring"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
dependencies = [
"bytes",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
@ -1877,15 +1645,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.11.0"
@ -1992,15 +1751,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -2297,7 +2047,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case 0.10.0",
"proc-macro2",
"quote",
"rustc_version",
@ -3099,7 +2848,7 @@ dependencies = [
"futures-core",
"futures-sink",
"gloo-utils",
"http 1.4.0",
"http",
"js-sys",
"pin-project",
"serde",
@ -3163,7 +2912,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http 1.4.0",
"http",
"indexmap 2.13.0",
"slab",
"tokio",
@ -3318,17 +3067,6 @@ dependencies = [
"utf8-width",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.4.0"
@ -3346,7 +3084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.4.0",
"http",
]
[[package]]
@ -3357,7 +3095,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http",
"http-body",
"pin-project-lite",
]
@ -3432,7 +3170,7 @@ dependencies = [
"futures-channel",
"futures-core",
"h2",
"http 1.4.0",
"http",
"http-body",
"httparse",
"httpdate",
@ -3465,7 +3203,7 @@ version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http 1.4.0",
"http",
"hyper",
"hyper-util",
"log",
@ -3516,14 +3254,14 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.4.0",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.3",
"socket2",
"system-configuration",
"tokio",
"tower-service",
@ -3684,12 +3422,6 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "impl-more"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
[[package]]
name = "include_bytes_aligned"
version = "0.1.4"
@ -3725,7 +3457,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"arc-swap",
"async-trait",
"clap",
"env_logger",
"futures",
@ -4048,7 +3779,7 @@ dependencies = [
"futures-channel",
"futures-util",
"gloo-net",
"http 1.4.0",
"http",
"jsonrpsee-core",
"pin-project",
"rustls",
@ -4073,7 +3804,7 @@ dependencies = [
"bytes",
"futures-timer",
"futures-util",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"jsonrpsee-types",
@ -4134,7 +3865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f"
dependencies = [
"futures-util",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"hyper",
@ -4160,7 +3891,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5"
dependencies = [
"http 1.4.0",
"http",
"serde",
"serde_json",
"thiserror 2.0.18",
@ -4184,7 +3915,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79"
dependencies = [
"http 1.4.0",
"http",
"jsonrpsee-client-transport",
"jsonrpsee-core",
"jsonrpsee-types",
@ -4238,12 +3969,6 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "language-tags"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "lazy-regex"
version = "3.6.0"
@ -4620,12 +4345,6 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "local-waker"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
[[package]]
name = "lock_api"
version = "0.4.14"
@ -5384,7 +5103,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.61.2",
]
@ -5398,7 +5116,7 @@ dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http 1.4.0",
"http",
"httparse",
"memchr",
"mime",
@ -5533,6 +5251,7 @@ name = "nssa"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"borsh",
"env_logger",
"hex",
@ -5545,6 +5264,7 @@ dependencies = [
"risc0-zkvm",
"secp256k1",
"serde",
"serde_with",
"sha2",
"test-case",
"test_program_methods",
@ -6270,7 +5990,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.3",
"socket2",
"thiserror 2.0.18",
"tokio",
"tracing",
@ -6307,7 +6027,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.3",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
@ -6581,12 +6301,6 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-lite"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
[[package]]
name = "regex-syntax"
version = "0.8.10"
@ -6606,7 +6320,7 @@ dependencies = [
"futures-core",
"futures-util",
"h2",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"hyper",
@ -7452,48 +7166,39 @@ dependencies = [
"url",
]
[[package]]
name = "sequencer_rpc"
version = "0.1.0"
dependencies = [
"actix-cors",
"actix-web",
"anyhow",
"base58",
"base64 0.22.1",
"bedrock_client",
"borsh",
"bytesize",
"common",
"futures",
"hex",
"itertools 0.14.0",
"log",
"mempool",
"nssa",
"sequencer_core",
"serde",
"serde_json",
"tempfile",
"tokio",
]
[[package]]
name = "sequencer_service"
version = "0.1.0"
dependencies = [
"actix",
"actix-web",
"anyhow",
"borsh",
"bytesize",
"clap",
"common",
"env_logger",
"futures",
"indexer_service_rpc",
"jsonrpsee",
"log",
"mempool",
"nssa",
"nssa_core",
"sequencer_core",
"sequencer_rpc",
"sequencer_service_rpc",
"tokio",
"tokio-util",
]
[[package]]
name = "sequencer_service_rpc"
version = "0.1.0"
dependencies = [
"common",
"jsonrpsee",
"nssa",
"nssa_core",
"schemars 1.2.1",
"serde_json",
]
[[package]]
@ -7689,7 +7394,7 @@ dependencies = [
"const_format",
"futures",
"gloo-net",
"http 1.4.0",
"http",
"http-body-util",
"hyper",
"inventory",
@ -7826,16 +7531,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.3"
@ -7855,7 +7550,7 @@ dependencies = [
"base64 0.22.1",
"bytes",
"futures",
"http 1.4.0",
"http",
"httparse",
"log",
"rand 0.8.5",
@ -8161,7 +7856,7 @@ dependencies = [
"etcetera",
"ferroid",
"futures",
"http 1.4.0",
"http",
"itertools 0.14.0",
"log",
"memchr",
@ -8321,7 +8016,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.3",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
@ -8518,7 +8213,7 @@ dependencies = [
"base64 0.22.1",
"bytes",
"h2",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"hyper",
@ -8526,7 +8221,7 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
"socket2 0.6.3",
"socket2",
"sync_wrapper",
"tokio",
"tokio-stream",
@ -8576,7 +8271,7 @@ dependencies = [
"bytes",
"futures-core",
"futures-util",
"http 1.4.0",
"http",
"http-body",
"http-body-util",
"http-range-header",
@ -8678,7 +8373,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
dependencies = [
"bytes",
"data-encoding",
"http 1.4.0",
"http",
"httparse",
"log",
"rand 0.9.2",
@ -8860,7 +8555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
dependencies = [
"base64 0.22.1",
"http 1.4.0",
"http",
"httparse",
"log",
]

View File

@ -17,9 +17,9 @@ members = [
"programs/amm",
"programs/token/core",
"programs/token",
"sequencer_core",
"sequencer_rpc",
"sequencer_service",
"sequencer/core",
"sequencer/service",
"sequencer/service/rpc",
"indexer/core",
"indexer/service",
"indexer/service/protocol",
@ -42,8 +42,8 @@ common = { path = "common" }
mempool = { path = "mempool" }
storage = { path = "storage" }
key_protocol = { path = "key_protocol" }
sequencer_core = { path = "sequencer_core" }
sequencer_rpc = { path = "sequencer_rpc" }
sequencer_core = { path = "sequencer/core" }
sequencer_service_rpc = { path = "sequencer/service/rpc" }
sequencer_service = { path = "sequencer/service" }
indexer_core = { path = "indexer/core" }
indexer_service = { path = "indexer/service" }

View File

@ -10,7 +10,7 @@ pub type BlockHash = HashType;
pub type BlockId = u64;
pub type TimeStamp = u64;
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct BlockMeta {
pub id: BlockId,
pub hash: BlockHash,
@ -31,7 +31,7 @@ impl OwnHasher {
}
}
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct BlockHeader {
pub block_id: BlockId,
pub prev_block_hash: BlockHash,
@ -40,19 +40,19 @@ pub struct BlockHeader {
pub signature: nssa::Signature,
}
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct BlockBody {
pub transactions: Vec<NSSATransaction>,
}
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub enum BedrockStatus {
Pending,
Safe,
Finalized,
}
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Block {
pub header: BlockHeader,
pub body: BlockBody,
@ -60,7 +60,7 @@ pub struct Block {
pub bedrock_parent_id: MantleMsgId,
}
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct HashableBlockData {
pub block_id: BlockId,
pub prev_block_hash: BlockHash,

View File

@ -1,43 +0,0 @@
use nssa::AccountId;
use serde::Deserialize;
use crate::rpc_primitives::errors::RpcError;
#[derive(Debug, Clone, Deserialize)]
pub struct SequencerRpcError {
pub jsonrpc: String,
pub error: RpcError,
pub id: u64,
}
#[derive(thiserror::Error, Debug)]
pub enum SequencerClientError {
#[error("HTTP error")]
HTTPError(#[from] reqwest::Error),
#[error("Serde error")]
SerdeError(#[from] serde_json::Error),
#[error("Internal error: {0:?}")]
InternalError(SequencerRpcError),
}
impl From<SequencerRpcError> for SequencerClientError {
fn from(value: SequencerRpcError) -> Self {
Self::InternalError(value)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ExecutionFailureKind {
#[error("Failed to get data from sequencer")]
SequencerError(#[source] anyhow::Error),
#[error("Inputs amounts does not match outputs")]
AmountMismatchError,
#[error("Accounts key not found")]
KeyNotFoundError,
#[error("Sequencer client error: {0:?}")]
SequencerClientError(#[from] SequencerClientError),
#[error("Can not pay for operation")]
InsufficientFundsError,
#[error("Account {0} data is invalid")]
AccountDataError(AccountId),
}

View File

@ -5,9 +5,6 @@ use serde_with::{DeserializeFromStr, SerializeDisplay};
pub mod block;
pub mod config;
pub mod error;
pub mod rpc_primitives;
pub mod sequencer_client;
pub mod transaction;
// Module for tests utility functions

View File

@ -1,194 +0,0 @@
use std::fmt;
use serde_json::{Value, to_value};
#[derive(serde::Serialize)]
pub struct RpcParseError(pub String);
/// This struct may be returned from JSON RPC server in case of error.
///
/// It is expected that that this struct has impls From<_> all other RPC errors
/// like [`RpcBlockError`](crate::types::blocks::RpcBlockError).
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RpcError {
#[serde(flatten)]
pub error_struct: Option<RpcErrorKind>,
/// Deprecated please use the `error_struct` instead.
pub code: i64,
/// Deprecated please use the `error_struct` instead.
pub message: String,
/// Deprecated please use the `error_struct` instead.
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(tag = "name", content = "cause", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RpcErrorKind {
RequestValidationError(RpcRequestValidationErrorKind),
HandlerError(Value),
InternalError(Value),
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(tag = "name", content = "info", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RpcRequestValidationErrorKind {
MethodNotFound { method_name: String },
ParseError { error_message: String },
}
/// A general Server Error.
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum ServerError {
Timeout,
Closed,
}
impl RpcError {
/// A generic constructor.
///
/// Mostly for completeness, doesn't do anything but filling in the corresponding fields.
#[must_use]
pub const fn new(code: i64, message: String, data: Option<Value>) -> Self {
Self {
code,
message,
data,
error_struct: None,
}
}
/// Create an Invalid Param error.
pub fn invalid_params(data: impl serde::Serialize) -> Self {
let value = match to_value(data) {
Ok(value) => value,
Err(err) => {
return Self::server_error(Some(format!(
"Failed to serialize invalid parameters error: {:?}",
err.to_string()
)));
}
};
Self::new(-32_602, "Invalid params".to_owned(), Some(value))
}
/// Create a server error.
pub fn server_error<E: serde::Serialize>(e: Option<E>) -> Self {
Self::new(
-32_000,
"Server error".to_owned(),
e.map(|v| to_value(v).expect("Must be representable in JSON")),
)
}
/// Create a parse error.
#[must_use]
pub fn parse_error(e: String) -> Self {
Self {
code: -32_700,
message: "Parse error".to_owned(),
data: Some(Value::String(e.clone())),
error_struct: Some(RpcErrorKind::RequestValidationError(
RpcRequestValidationErrorKind::ParseError { error_message: e },
)),
}
}
#[must_use]
pub fn serialization_error(e: &str) -> Self {
Self::new_internal_error(Some(Value::String(e.to_owned())), e)
}
/// Helper method to define extract `INTERNAL_ERROR` in separate `RpcErrorKind`
/// Returns `HANDLER_ERROR` if the error is not internal one.
#[must_use]
pub fn new_internal_or_handler_error(error_data: Option<Value>, error_struct: Value) -> Self {
if error_struct["name"] == "INTERNAL_ERROR" {
let error_message = match error_struct["info"].get("error_message") {
Some(Value::String(error_message)) => error_message.as_str(),
_ => "InternalError happened during serializing InternalError",
};
Self::new_internal_error(error_data, error_message)
} else {
Self::new_handler_error(error_data, error_struct)
}
}
#[must_use]
pub fn new_internal_error(error_data: Option<Value>, info: &str) -> Self {
Self {
code: -32_000,
message: "Server error".to_owned(),
data: error_data,
error_struct: Some(RpcErrorKind::InternalError(serde_json::json!({
"name": "INTERNAL_ERROR",
"info": serde_json::json!({"error_message": info})
}))),
}
}
fn new_handler_error(error_data: Option<Value>, error_struct: Value) -> Self {
Self {
code: -32_000,
message: "Server error".to_owned(),
data: error_data,
error_struct: Some(RpcErrorKind::HandlerError(error_struct)),
}
}
/// Create a method not found error.
#[must_use]
pub fn method_not_found(method: String) -> Self {
Self {
code: -32_601,
message: "Method not found".to_owned(),
data: Some(Value::String(method.clone())),
error_struct: Some(RpcErrorKind::RequestValidationError(
RpcRequestValidationErrorKind::MethodNotFound {
method_name: method,
},
)),
}
}
}
impl fmt::Display for RpcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl From<RpcParseError> for RpcError {
fn from(parse_error: RpcParseError) -> Self {
Self::parse_error(parse_error.0)
}
}
impl From<std::convert::Infallible> for RpcError {
fn from(_: std::convert::Infallible) -> Self {
// SAFETY: Infallible error can never be constructed, so this code can never be reached.
unsafe { core::hint::unreachable_unchecked() }
}
}
impl fmt::Display for ServerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Timeout => write!(f, "ServerError: Timeout"),
Self::Closed => write!(f, "ServerError: Closed"),
}
}
}
impl From<ServerError> for RpcError {
fn from(e: ServerError) -> Self {
let error_data = match to_value(&e) {
Ok(value) => value,
Err(_err) => {
return Self::new_internal_error(None, "Failed to serialize ServerError");
}
};
Self::new_internal_error(Some(error_data), e.to_string().as_str())
}
}

View File

@ -1,588 +0,0 @@
// Copyright 2017 tokio-jsonrpc Developers
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//! JSON-RPC 2.0 messages.
//!
//! The main entrypoint here is the [Message](enum.Message.html). The others are just building
//! blocks and you should generally work with `Message` instead.
use std::fmt::{Formatter, Result as FmtResult};
use serde::{
de::{Deserializer, Error, Unexpected, Visitor},
ser::{SerializeStruct as _, Serializer},
};
use serde_json::{Result as JsonResult, Value};
use super::errors::RpcError;
pub type Parsed = Result<Message, Broken>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Version;
impl serde::Serialize for Version {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str("2.0")
}
}
impl<'de> serde::Deserialize<'de> for Version {
#[expect(
clippy::renamed_function_params,
reason = "More readable than original serde parameter names"
)]
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct VersionVisitor;
impl Visitor<'_> for VersionVisitor {
type Value = Version;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("a version string")
}
fn visit_str<E: Error>(self, value: &str) -> Result<Version, E> {
match value {
"2.0" => Ok(Version),
_ => Err(E::invalid_value(Unexpected::Str(value), &"value 2.0")),
}
}
}
deserializer.deserialize_str(VersionVisitor)
}
}
/// An RPC request.
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[expect(
clippy::partial_pub_fields,
reason = "We don't want to allow access to the version, but the others are public for ease of use"
)]
pub struct Request {
jsonrpc: Version,
pub method: String,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub params: Value,
pub id: Value,
}
impl Request {
#[must_use]
pub fn from_payload_version_2_0(method: String, payload: serde_json::Value) -> Self {
Self {
jsonrpc: Version,
method,
params: payload,
// ToDo: Correct checking of id
id: 1.into(),
}
}
/// Answer the request with a (positive) reply.
///
/// The ID is taken from the request.
#[must_use]
pub fn reply(&self, reply: Value) -> Message {
Message::Response(Response {
jsonrpc: Version,
result: Ok(reply),
id: self.id.clone(),
})
}
/// Answer the request with an error.
#[must_use]
pub fn error(&self, error: RpcError) -> Message {
Message::Response(Response {
jsonrpc: Version,
result: Err(error),
id: self.id.clone(),
})
}
}
/// A response to an RPC.
///
/// It is created by the methods on [Request](struct.Request.html).
#[expect(
clippy::partial_pub_fields,
reason = "We don't want to allow access to the version, but the others are public for ease of use"
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
jsonrpc: Version,
pub result: Result<Value, RpcError>,
pub id: Value,
}
impl serde::Serialize for Response {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut sub = serializer.serialize_struct("Response", 3)?;
sub.serialize_field("jsonrpc", &self.jsonrpc)?;
match &self.result {
Ok(value) => sub.serialize_field("result", value),
Err(err) => sub.serialize_field("error", err),
}?;
sub.serialize_field("id", &self.id)?;
sub.end()
}
}
/// A helper trick for deserialization.
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct WireResponse {
// It is actually used to eat and sanity check the deserialized text
#[serde(rename = "jsonrpc")]
_jsonrpc: Version,
// Make sure we accept null as Some(Value::Null), instead of going to None
#[serde(default, deserialize_with = "some_value")]
result: Option<Value>,
error: Option<RpcError>,
id: Value,
}
// Implementing deserialize is hard. We sidestep the difficulty by deserializing a similar
// structure that directly corresponds to whatever is on the wire and then convert it to our more
// convenient representation.
impl<'de> serde::Deserialize<'de> for Response {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wr: WireResponse = serde::Deserialize::deserialize(deserializer)?;
let result = match (wr.result, wr.error) {
(Some(res), None) => Ok(res),
(None, Some(err)) => Err(err),
_ => {
let err = D::Error::custom("Either 'error' or 'result' is expected, but not both");
return Err(err);
}
};
Ok(Self {
jsonrpc: Version,
result,
id: wr.id,
})
}
}
/// A notification (doesn't expect an answer).
#[expect(
clippy::partial_pub_fields,
reason = "We don't want to allow access to the version, but the others are public for ease of use"
)]
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Notification {
jsonrpc: Version,
pub method: String,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub params: Value,
}
/// One message of the JSON RPC protocol.
///
/// One message, directly mapped from the structures of the protocol. See the
/// [specification](http://www.jsonrpc.org/specification) for more details.
///
/// Since the protocol allows one endpoint to be both client and server at the same time, the
/// message can decode and encode both directions of the protocol.
///
/// The `Batch` variant is supposed to be created directly, without a constructor.
///
/// The `UnmatchedSub` variant is used when a request is an array and some of the subrequests
/// aren't recognized as valid json rpc 2.0 messages. This is never returned as a top-level
/// element, it is returned as `Err(Broken::Unmatched)`.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Message {
/// An RPC request.
Request(Request),
/// A response to a Request.
Response(Response),
/// A notification.
Notification(Notification),
/// A batch of more requests or responses.
///
/// The protocol allows bundling multiple requests, notifications or responses to a single
/// message.
///
/// This variant has no direct constructor and is expected to be constructed manually.
Batch(Vec<Self>),
/// An unmatched sub entry in a `Batch`.
///
/// When there's a `Batch` and an element doesn't comform to the JSONRPC 2.0 format, that one
/// is represented by this. This is never produced as a top-level value when parsing, the
/// `Err(Broken::Unmatched)` is used instead. It is not possible to serialize.
#[serde(skip_serializing)]
UnmatchedSub(Value),
}
impl Message {
/// A constructor for a request.
///
/// The ID is auto-set to dontcare.
#[must_use]
pub fn request(method: String, params: Value) -> Self {
let id = Value::from("dontcare");
Self::Request(Request {
jsonrpc: Version,
method,
params,
id,
})
}
/// Create a top-level error (without an ID).
#[must_use]
pub const fn error(error: RpcError) -> Self {
Self::Response(Response {
jsonrpc: Version,
result: Err(error),
id: Value::Null,
})
}
/// A constructor for a notification.
#[must_use]
pub const fn notification(method: String, params: Value) -> Self {
Self::Notification(Notification {
jsonrpc: Version,
method,
params,
})
}
/// A constructor for a response.
#[must_use]
pub const fn response(id: Value, result: Result<Value, RpcError>) -> Self {
Self::Response(Response {
jsonrpc: Version,
result,
id,
})
}
/// Returns id or Null if there is no id.
#[must_use]
pub fn id(&self) -> Value {
match self {
Self::Request(req) => req.id.clone(),
Self::Response(response) => response.id.clone(),
Self::Notification(_) | Self::Batch(_) | Self::UnmatchedSub(_) => Value::Null,
}
}
}
impl From<Message> for String {
fn from(val: Message) -> Self {
::serde_json::ser::to_string(&val).expect("message serialization to json should not fail")
}
}
impl From<Message> for Vec<u8> {
fn from(val: Message) -> Self {
::serde_json::ser::to_vec(&val)
.expect("message serialization to json bytes should not fail")
}
}
/// A broken message.
///
/// Protocol-level errors.
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(untagged)]
pub enum Broken {
/// It was valid JSON, but doesn't match the form of a JSONRPC 2.0 message.
Unmatched(Value),
/// Invalid JSON.
#[serde(skip_deserializing)]
SyntaxError(String),
}
impl Broken {
/// Generate an appropriate error message.
///
/// The error message for these things are specified in the RFC, so this just creates an error
/// with the right values.
#[must_use]
pub fn reply(&self) -> Message {
match self {
Self::Unmatched(_) => Message::error(RpcError::parse_error(
"JSON RPC Request format was expected".to_owned(),
)),
Self::SyntaxError(e) => Message::error(RpcError::parse_error(e.clone())),
}
}
}
/// A trick to easily deserialize and detect valid JSON, but invalid Message.
#[derive(serde::Deserialize)]
#[serde(untagged)]
pub enum WireMessage {
Message(Message),
Broken(Broken),
}
pub fn decoded_to_parsed(res: JsonResult<WireMessage>) -> Parsed {
match res {
Ok(WireMessage::Message(Message::UnmatchedSub(value))) => Err(Broken::Unmatched(value)),
Ok(WireMessage::Message(m)) => Ok(m),
Ok(WireMessage::Broken(b)) => Err(b),
Err(e) => Err(Broken::SyntaxError(e.to_string())),
}
}
/// Read a [Message](enum.Message.html) from a slice.
///
/// Invalid JSON or JSONRPC messages are reported as [Broken](enum.Broken.html).
pub fn from_slice(s: &[u8]) -> Parsed {
decoded_to_parsed(::serde_json::de::from_slice(s))
}
/// Read a [Message](enum.Message.html) from a string.
///
/// Invalid JSON or JSONRPC messages are reported as [Broken](enum.Broken.html).
pub fn from_str(s: &str) -> Parsed {
from_slice(s.as_bytes())
}
/// Deserializer for `Option<Value>` that produces `Some(Value::Null)`.
///
/// The usual one produces None in that case. But we need to know the difference between
/// `{x: null}` and `{}`.
fn some_value<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Value>, D::Error> {
serde::Deserialize::deserialize(deserializer).map(Some)
}
#[cfg(test)]
mod tests {
use serde_json::{Value, de::from_slice, json, ser::to_vec};
use super::*;
/// Test serialization and deserialization of the Message.
///
/// We first deserialize it from a string. That way we check deserialization works.
/// But since serialization doesn't have to produce the exact same result (order, spaces, …),
/// we then serialize and deserialize the thing again and check it matches.
#[test]
fn message_serde() {
// A helper for running one message test
fn one(input: &str, expected: &Message) {
let parsed: Message = from_str(input).unwrap();
assert_eq!(*expected, parsed);
let serialized = to_vec(&parsed).unwrap();
let deserialized: Message = from_slice(&serialized).unwrap();
assert_eq!(parsed, deserialized);
}
// A request without parameters
one(
r#"{"jsonrpc": "2.0", "method": "call", "id": 1}"#,
&Message::Request(Request {
jsonrpc: Version,
method: "call".to_owned(),
params: Value::Null,
id: json!(1),
}),
);
// A request with parameters
one(
r#"{"jsonrpc": "2.0", "method": "call", "params": [1, 2, 3], "id": 2}"#,
&Message::Request(Request {
jsonrpc: Version,
method: "call".to_owned(),
params: json!([1, 2, 3]),
id: json!(2),
}),
);
// A notification (with parameters)
one(
r#"{"jsonrpc": "2.0", "method": "notif", "params": {"x": "y"}}"#,
&Message::Notification(Notification {
jsonrpc: Version,
method: "notif".to_owned(),
params: json!({"x": "y"}),
}),
);
// A successful response
one(
r#"{"jsonrpc": "2.0", "result": 42, "id": 3}"#,
&Message::Response(Response {
jsonrpc: Version,
result: Ok(json!(42)),
id: json!(3),
}),
);
// A successful response
one(
r#"{"jsonrpc": "2.0", "result": null, "id": 3}"#,
&Message::Response(Response {
jsonrpc: Version,
result: Ok(Value::Null),
id: json!(3),
}),
);
// An error
one(
r#"{"jsonrpc": "2.0", "error": {"code": 42, "message": "Wrong!"}, "id": null}"#,
&Message::Response(Response {
jsonrpc: Version,
result: Err(RpcError::new(42, "Wrong!".to_owned(), None)),
id: Value::Null,
}),
);
// A batch
one(
r#"[
{"jsonrpc": "2.0", "method": "notif"},
{"jsonrpc": "2.0", "method": "call", "id": 42}
]"#,
&Message::Batch(vec![
Message::Notification(Notification {
jsonrpc: Version,
method: "notif".to_owned(),
params: Value::Null,
}),
Message::Request(Request {
jsonrpc: Version,
method: "call".to_owned(),
params: Value::Null,
id: json!(42),
}),
]),
);
// Some handling of broken messages inside a batch
let parsed = from_str(
r#"[
{"jsonrpc": "2.0", "method": "notif"},
{"jsonrpc": "2.0", "method": "call", "id": 42},
true
]"#,
)
.unwrap();
assert_eq!(
Message::Batch(vec![
Message::Notification(Notification {
jsonrpc: Version,
method: "notif".to_owned(),
params: Value::Null,
}),
Message::Request(Request {
jsonrpc: Version,
method: "call".to_owned(),
params: Value::Null,
id: json!(42),
}),
Message::UnmatchedSub(Value::Bool(true)),
]),
parsed
);
to_vec(&Message::UnmatchedSub(Value::Null)).unwrap_err();
}
/// A helper for the `broken` test.
///
/// Check that the given JSON string parses, but is not recognized as a valid RPC message.
///
/// Test things that are almost but not entirely JSONRPC are rejected.
///
/// The reject is done by returning it as Unmatched.
#[test]
fn broken() {
// A helper with one test
fn one(input: &str) {
let msg = from_str(input);
match msg {
Err(Broken::Unmatched(_)) => (),
_ => panic!("{input} recognized as an RPC message: {msg:?}!"),
}
}
// Missing the version
one(r#"{"method": "notif"}"#);
// Wrong version
one(r#"{"jsonrpc": 2.0, "method": "notif"}"#);
// A response with both result and error
one(r#"{"jsonrpc": "2.0", "result": 42, "error": {"code": 42, "message": "!"}, "id": 1}"#);
// A response without an id
one(r#"{"jsonrpc": "2.0", "result": 42}"#);
// An extra field
one(r#"{"jsonrpc": "2.0", "method": "weird", "params": 42, "others": 43, "id": 2}"#);
// Something completely different
one(r#"{"x": [1, 2, 3]}"#);
match from_str("{]") {
Err(Broken::SyntaxError(_)) => (),
other => panic!("Something unexpected: {other:?}"),
}
}
/// Test some non-trivial aspects of the constructors.
///
/// This doesn't have a full coverage, because there's not much to actually test there.
/// Most of it is related to the ids.
#[test]
#[ignore = "Not a full coverage test"]
fn constructors() {
let msg1 = Message::request("call".to_owned(), json!([1, 2, 3]));
let msg2 = Message::request("call".to_owned(), json!([1, 2, 3]));
// They differ, even when created with the same parameters
assert_ne!(msg1, msg2);
// And, specifically, they differ in the ID's
let (req1, req2) = if let (Message::Request(req1), Message::Request(req2)) = (msg1, msg2) {
assert_ne!(req1.id, req2.id);
assert!(req1.id.is_string());
assert!(req2.id.is_string());
(req1, req2)
} else {
panic!("Non-request received");
};
let id1 = req1.id.clone();
// When we answer a message, we get the same ID
if let Message::Response(resp) = req1.reply(json!([1, 2, 3])) {
assert_eq!(
resp,
Response {
jsonrpc: Version,
result: Ok(json!([1, 2, 3])),
id: id1
}
);
} else {
panic!("Not a response");
}
let id2 = req2.id.clone();
// The same with an error
if let Message::Response(resp) = req2.error(RpcError::new(42, "Wrong!".to_owned(), None)) {
assert_eq!(
resp,
Response {
jsonrpc: Version,
result: Err(RpcError::new(42, "Wrong!".to_owned(), None)),
id: id2,
}
);
} else {
panic!("Not a response");
}
// When we have unmatched, we generate a top-level error with Null id.
if let Message::Response(resp) =
Message::error(RpcError::new(43, "Also wrong!".to_owned(), None))
{
assert_eq!(
resp,
Response {
jsonrpc: Version,
result: Err(RpcError::new(43, "Also wrong!".to_owned(), None)),
id: Value::Null,
}
);
} else {
panic!("Not a response");
}
}
}

View File

@ -1,57 +0,0 @@
use bytesize::ByteSize;
use serde::{Deserialize, Serialize};
pub mod errors;
pub mod message;
pub mod parser;
pub mod requests;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RpcLimitsConfig {
/// Maximum byte size of the json payload.
pub json_payload_max_size: ByteSize,
}
impl Default for RpcLimitsConfig {
fn default() -> Self {
Self {
json_payload_max_size: ByteSize::mib(10),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RpcConfig {
pub addr: String,
pub cors_allowed_origins: Vec<String>,
#[serde(default)]
pub limits_config: RpcLimitsConfig,
}
impl Default for RpcConfig {
fn default() -> Self {
Self {
addr: "0.0.0.0:3040".to_owned(),
cors_allowed_origins: vec!["*".to_owned()],
limits_config: RpcLimitsConfig::default(),
}
}
}
impl RpcConfig {
#[must_use]
pub fn new(addr: &str) -> Self {
Self {
addr: addr.to_owned(),
..Default::default()
}
}
#[must_use]
pub fn with_port(port: u16) -> Self {
Self {
addr: format!("0.0.0.0:{port}"),
..Default::default()
}
}
}

View File

@ -1,29 +0,0 @@
use serde::de::DeserializeOwned;
use serde_json::Value;
use super::errors::RpcParseError;
#[macro_export]
macro_rules! parse_request {
($request_name:ty) => {
impl RpcRequest for $request_name {
fn parse(value: Option<Value>) -> Result<Self, RpcParseError> {
parse_params::<Self>(value)
}
}
};
}
pub trait RpcRequest: Sized {
fn parse(value: Option<Value>) -> Result<Self, RpcParseError>;
}
pub fn parse_params<T: DeserializeOwned>(value: Option<Value>) -> Result<T, RpcParseError> {
value.map_or_else(
|| Err(RpcParseError("Require at least one parameter".to_owned())),
|value| {
serde_json::from_value(value)
.map_err(|err| RpcParseError(format!("Failed parsing args: {err}")))
},
)
}

View File

@ -1,219 +0,0 @@
use std::collections::HashMap;
use nssa::AccountId;
use nssa_core::program::ProgramId;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{
errors::RpcParseError,
parser::{RpcRequest, parse_params},
};
use crate::{HashType, parse_request};
mod base64_deser {
use base64::{Engine as _, engine::general_purpose};
use serde::{self, Deserialize, Deserializer, Serializer, ser::SerializeSeq as _};
pub mod vec {
use super::*;
pub fn serialize<S>(bytes_vec: &[Vec<u8>], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(bytes_vec.len()))?;
for bytes in bytes_vec {
let s = general_purpose::STANDARD.encode(bytes);
seq.serialize_element(&s)?;
}
seq.end()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
let base64_strings: Vec<String> = Deserialize::deserialize(deserializer)?;
base64_strings
.into_iter()
.map(|s| {
general_purpose::STANDARD
.decode(&s)
.map_err(serde::de::Error::custom)
})
.collect()
}
}
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let base64_string = general_purpose::STANDARD.encode(bytes);
serializer.serialize_str(&base64_string)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let base64_string: String = Deserialize::deserialize(deserializer)?;
general_purpose::STANDARD
.decode(&base64_string)
.map_err(serde::de::Error::custom)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct HelloRequest;
#[derive(Serialize, Deserialize, Debug)]
pub struct RegisterAccountRequest {
pub account_id: [u8; 32],
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SendTxRequest {
#[serde(with = "base64_deser")]
pub transaction: Vec<u8>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetBlockDataRequest {
pub block_id: u64,
}
/// Get a range of blocks from `start_block_id` to `end_block_id` (inclusive).
#[derive(Serialize, Deserialize, Debug)]
pub struct GetBlockRangeDataRequest {
pub start_block_id: u64,
pub end_block_id: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetGenesisIdRequest;
#[derive(Serialize, Deserialize, Debug)]
pub struct GetLastBlockRequest;
#[derive(Serialize, Deserialize, Debug)]
pub struct GetInitialTestnetAccountsRequest;
#[derive(Serialize, Deserialize, Debug)]
pub struct GetAccountBalanceRequest {
pub account_id: AccountId,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetTransactionByHashRequest {
pub hash: HashType,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetAccountsNoncesRequest {
pub account_ids: Vec<AccountId>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetAccountRequest {
pub account_id: AccountId,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetProofForCommitmentRequest {
pub commitment: nssa_core::Commitment,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetProgramIdsRequest;
parse_request!(HelloRequest);
parse_request!(RegisterAccountRequest);
parse_request!(SendTxRequest);
parse_request!(GetBlockDataRequest);
parse_request!(GetBlockRangeDataRequest);
parse_request!(GetGenesisIdRequest);
parse_request!(GetLastBlockRequest);
parse_request!(GetInitialTestnetAccountsRequest);
parse_request!(GetAccountBalanceRequest);
parse_request!(GetTransactionByHashRequest);
parse_request!(GetAccountsNoncesRequest);
parse_request!(GetProofForCommitmentRequest);
parse_request!(GetAccountRequest);
parse_request!(GetProgramIdsRequest);
#[derive(Serialize, Deserialize, Debug)]
pub struct HelloResponse {
pub greeting: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct RegisterAccountResponse {
pub status: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SendTxResponse {
pub status: String,
pub tx_hash: HashType,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetBlockDataResponse {
#[serde(with = "base64_deser")]
pub block: Vec<u8>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetBlockRangeDataResponse {
#[serde(with = "base64_deser::vec")]
pub blocks: Vec<Vec<u8>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetGenesisIdResponse {
pub genesis_id: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetLastBlockResponse {
pub last_block: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetAccountBalanceResponse {
pub balance: u128,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetAccountsNoncesResponse {
pub nonces: Vec<u128>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetTransactionByHashResponse {
pub transaction: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetAccountResponse {
pub account: nssa::Account,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetProofForCommitmentResponse {
pub membership_proof: Option<nssa_core::MembershipProof>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetProgramIdsResponse {
pub program_ids: HashMap<String, ProgramId>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GetInitialTestnetAccountsResponse {
/// Hex encoded account id.
pub account_id: String,
pub balance: u64,
}

View File

@ -1,361 +0,0 @@
use std::{collections::HashMap, ops::RangeInclusive};
use anyhow::Result;
use nssa::AccountId;
use nssa_core::program::ProgramId;
use reqwest::Client;
use serde::Deserialize;
use serde_json::Value;
use url::Url;
use super::rpc_primitives::requests::{
GetAccountBalanceRequest, GetAccountBalanceResponse, GetBlockDataRequest, GetBlockDataResponse,
GetGenesisIdRequest, GetGenesisIdResponse, GetInitialTestnetAccountsRequest,
};
use crate::{
HashType,
config::BasicAuth,
error::{SequencerClientError, SequencerRpcError},
rpc_primitives::{
self,
requests::{
GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest,
GetAccountsNoncesResponse, GetBlockRangeDataRequest, GetBlockRangeDataResponse,
GetInitialTestnetAccountsResponse, GetLastBlockRequest, GetLastBlockResponse,
GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest,
GetProofForCommitmentResponse, GetTransactionByHashRequest,
GetTransactionByHashResponse, SendTxRequest, SendTxResponse,
},
},
transaction::NSSATransaction,
};
#[derive(Debug, Clone, Deserialize)]
struct SequencerRpcResponse {
#[serde(rename = "jsonrpc")]
_jsonrpc: String,
result: serde_json::Value,
#[serde(rename = "id")]
_id: u64,
}
#[derive(Clone)]
pub struct SequencerClient {
pub client: reqwest::Client,
pub sequencer_addr: Url,
pub basic_auth: Option<BasicAuth>,
}
impl SequencerClient {
pub fn new(sequencer_addr: Url) -> Result<Self> {
Self::new_with_auth(sequencer_addr, None)
}
pub fn new_with_auth(sequencer_addr: Url, basic_auth: Option<BasicAuth>) -> Result<Self> {
Ok(Self {
client: Client::builder()
// Add more fields if needed
.timeout(std::time::Duration::from_secs(60))
// Should be kept in sync with server keep-alive settings
.pool_idle_timeout(std::time::Duration::from_secs(5))
.build()?,
sequencer_addr,
basic_auth,
})
}
pub async fn call_method_with_payload(
&self,
method: &str,
payload: Value,
) -> Result<Value, SequencerClientError> {
let request =
rpc_primitives::message::Request::from_payload_version_2_0(method.to_owned(), payload);
log::debug!(
"Calling method {method} with payload {request:?} to sequencer at {}",
self.sequencer_addr
);
let strategy = tokio_retry::strategy::FixedInterval::from_millis(10000).take(60);
let response_vall = tokio_retry::Retry::spawn(strategy, || async {
let mut call_builder = self.client.post(self.sequencer_addr.clone());
if let Some(BasicAuth { username, password }) = &self.basic_auth {
call_builder = call_builder.basic_auth(username, password.as_deref());
}
let call_res_res = call_builder.json(&request).send().await;
match call_res_res {
Err(err) => Err(err),
Ok(call_res) => call_res.json::<Value>().await,
}
})
.await?;
if let Ok(response) = serde_json::from_value::<SequencerRpcResponse>(response_vall.clone())
{
Ok(response.result)
} else {
let err_resp = serde_json::from_value::<SequencerRpcError>(response_vall)?;
Err(err_resp.into())
}
}
/// Get block data at `block_id` from sequencer.
pub async fn get_block(
&self,
block_id: u64,
) -> Result<GetBlockDataResponse, SequencerClientError> {
let block_req = GetBlockDataRequest { block_id };
let req = serde_json::to_value(block_req)?;
let resp = self.call_method_with_payload("get_block", req).await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
pub async fn get_block_range(
&self,
range: RangeInclusive<u64>,
) -> Result<GetBlockRangeDataResponse, SequencerClientError> {
let block_req = GetBlockRangeDataRequest {
start_block_id: *range.start(),
end_block_id: *range.end(),
};
let req = serde_json::to_value(block_req)?;
let resp = self
.call_method_with_payload("get_block_range", req)
.await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Get last known `blokc_id` from sequencer.
pub async fn get_last_block(&self) -> Result<GetLastBlockResponse, SequencerClientError> {
let block_req = GetLastBlockRequest {};
let req = serde_json::to_value(block_req)?;
let resp = self.call_method_with_payload("get_last_block", req).await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Get account public balance for `account_id`. `account_id` must be a valid hex-string for 32
/// bytes.
pub async fn get_account_balance(
&self,
account_id: AccountId,
) -> Result<GetAccountBalanceResponse, SequencerClientError> {
let block_req = GetAccountBalanceRequest { account_id };
let req = serde_json::to_value(block_req)?;
let resp = self
.call_method_with_payload("get_account_balance", req)
.await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Get accounts nonces for `account_ids`. `account_ids` must be a list of valid hex-strings for
/// 32 bytes.
pub async fn get_accounts_nonces(
&self,
account_ids: Vec<AccountId>,
) -> Result<GetAccountsNoncesResponse, SequencerClientError> {
let block_req = GetAccountsNoncesRequest { account_ids };
let req = serde_json::to_value(block_req)?;
let resp = self
.call_method_with_payload("get_accounts_nonces", req)
.await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
pub async fn get_account(
&self,
account_id: AccountId,
) -> Result<GetAccountResponse, SequencerClientError> {
let block_req = GetAccountRequest { account_id };
let req = serde_json::to_value(block_req)?;
let resp = self.call_method_with_payload("get_account", req).await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Get transaction details for `hash`.
pub async fn get_transaction_by_hash(
&self,
hash: HashType,
) -> Result<GetTransactionByHashResponse, SequencerClientError> {
let block_req = GetTransactionByHashRequest { hash };
let req = serde_json::to_value(block_req)?;
let resp = self
.call_method_with_payload("get_transaction_by_hash", req)
.await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Send transaction to sequencer.
pub async fn send_tx_public(
&self,
transaction: nssa::PublicTransaction,
) -> Result<SendTxResponse, SequencerClientError> {
let transaction = NSSATransaction::Public(transaction);
let tx_req = SendTxRequest {
transaction: borsh::to_vec(&transaction).unwrap(),
};
let req = serde_json::to_value(tx_req)?;
let resp = self.call_method_with_payload("send_tx", req).await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Send transaction to sequencer.
pub async fn send_tx_private(
&self,
transaction: nssa::PrivacyPreservingTransaction,
) -> Result<SendTxResponse, SequencerClientError> {
let transaction = NSSATransaction::PrivacyPreserving(transaction);
let tx_req = SendTxRequest {
transaction: borsh::to_vec(&transaction).unwrap(),
};
let req = serde_json::to_value(tx_req)?;
let resp = self.call_method_with_payload("send_tx", req).await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Get genesis id from sequencer.
pub async fn get_genesis_id(&self) -> Result<GetGenesisIdResponse, SequencerClientError> {
let genesis_req = GetGenesisIdRequest {};
let req = serde_json::to_value(genesis_req).unwrap();
let resp = self
.call_method_with_payload("get_genesis", req)
.await
.unwrap();
let resp_deser = serde_json::from_value(resp).unwrap();
Ok(resp_deser)
}
/// Get initial testnet accounts from sequencer.
pub async fn get_initial_testnet_accounts(
&self,
) -> Result<Vec<GetInitialTestnetAccountsResponse>, SequencerClientError> {
let acc_req = GetInitialTestnetAccountsRequest {};
let req = serde_json::to_value(acc_req).unwrap();
let resp = self
.call_method_with_payload("get_initial_testnet_accounts", req)
.await
.unwrap();
let resp_deser = serde_json::from_value(resp).unwrap();
Ok(resp_deser)
}
/// Get proof for commitment.
pub async fn get_proof_for_commitment(
&self,
commitment: nssa_core::Commitment,
) -> Result<Option<nssa_core::MembershipProof>, SequencerClientError> {
let acc_req = GetProofForCommitmentRequest { commitment };
let req = serde_json::to_value(acc_req).unwrap();
let resp = self
.call_method_with_payload("get_proof_for_commitment", req)
.await
.unwrap();
let resp_deser = serde_json::from_value::<GetProofForCommitmentResponse>(resp)
.unwrap()
.membership_proof;
Ok(resp_deser)
}
pub async fn send_tx_program(
&self,
transaction: nssa::ProgramDeploymentTransaction,
) -> Result<SendTxResponse, SequencerClientError> {
let transaction = NSSATransaction::ProgramDeployment(transaction);
let tx_req = SendTxRequest {
transaction: borsh::to_vec(&transaction).unwrap(),
};
let req = serde_json::to_value(tx_req)?;
let resp = self.call_method_with_payload("send_tx", req).await?;
let resp_deser = serde_json::from_value(resp)?;
Ok(resp_deser)
}
/// Get Ids of the programs used by the node.
pub async fn get_program_ids(
&self,
) -> Result<HashMap<String, ProgramId>, SequencerClientError> {
let acc_req = GetProgramIdsRequest {};
let req = serde_json::to_value(acc_req).unwrap();
let resp = self
.call_method_with_payload("get_program_ids", req)
.await
.unwrap();
let resp_deser = serde_json::from_value::<GetProgramIdsResponse>(resp)
.unwrap()
.program_ids;
Ok(resp_deser)
}
}

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use crate::HashType;
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub enum NSSATransaction {
Public(nssa::PublicTransaction),
PrivacyPreserving(nssa::PrivacyPreservingTransaction),
@ -87,7 +87,7 @@ impl From<nssa::ProgramDeploymentTransaction> for NSSATransaction {
}
#[derive(
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize,
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
pub enum TxKind {
Public,

View File

@ -21,7 +21,6 @@ log.workspace = true
jsonrpsee.workspace = true
serde_json.workspace = true
futures.workspace = true
async-trait = "0.1.89"
arc-swap = "1.8.1"
[features]

View File

@ -359,12 +359,16 @@ impl From<ProgramDeploymentMessage> for nssa::program_deployment_transaction::Me
// WitnessSet conversions
// ============================================================================
impl TryFrom<nssa::public_transaction::WitnessSet> for WitnessSet {
type Error = ();
fn try_from(_value: nssa::public_transaction::WitnessSet) -> Result<Self, Self::Error> {
// Public transaction witness sets don't have proofs, so we can't convert them directly
Err(())
impl From<nssa::public_transaction::WitnessSet> for WitnessSet {
fn from(value: nssa::public_transaction::WitnessSet) -> Self {
Self {
signatures_and_public_keys: value
.signatures_and_public_keys()
.iter()
.map(|(sig, pk)| (sig.clone().into(), pk.clone().into()))
.collect(),
proof: None,
}
}
}
@ -376,7 +380,7 @@ impl From<nssa::privacy_preserving_transaction::witness_set::WitnessSet> for Wit
.into_iter()
.map(|(sig, pk)| (sig.into(), pk.into()))
.collect(),
proof: proof.into(),
proof: Some(proof.into()),
}
}
}
@ -396,7 +400,9 @@ impl TryFrom<WitnessSet> for nssa::privacy_preserving_transaction::witness_set::
Ok(Self::from_raw_parts(
signatures_and_public_keys,
proof.into(),
proof
.map(Into::into)
.ok_or_else(|| nssa::error::NssaError::InvalidInput("Missing proof".to_string()))?,
))
}
}
@ -416,14 +422,7 @@ impl From<nssa::PublicTransaction> for PublicTransaction {
Self {
hash,
message: message.into(),
witness_set: WitnessSet {
signatures_and_public_keys: witness_set
.signatures_and_public_keys()
.iter()
.map(|(sig, pk)| (sig.clone().into(), pk.clone().into()))
.collect(),
proof: Proof(vec![]), // Public transactions don't have proofs
},
witness_set: witness_set.into(),
}
}
}

View File

@ -240,7 +240,7 @@ pub struct PrivacyPreservingMessage {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct WitnessSet {
pub signatures_and_public_keys: Vec<(Signature, PublicKey)>,
pub proof: Proof,
pub proof: Option<Proof>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]

View File

@ -13,6 +13,7 @@ pub mod mock_service;
pub struct IndexerHandle {
addr: SocketAddr,
/// Option because of `Drop` which forbids to simply move out of `self` in `stopped()`.
server_handle: Option<jsonrpsee::server::ServerHandle>,
}
impl IndexerHandle {
@ -28,6 +29,7 @@ impl IndexerHandle {
self.addr
}
/// Wait for all Indexer tasks to stop.
pub async fn stopped(mut self) {
let handle = self
.server_handle
@ -37,15 +39,11 @@ impl IndexerHandle {
handle.stopped().await;
}
#[expect(
clippy::redundant_closure_for_method_calls,
reason = "Clippy suggested path jsonrpsee::jsonrpsee_server::ServerHandle is not accessible"
)]
#[must_use]
pub fn is_stopped(&self) -> bool {
self.server_handle
.as_ref()
.is_none_or(|handle| handle.is_stopped())
.is_none_or(ServerHandle::is_stopped)
}
}

View File

@ -15,7 +15,10 @@ use indexer_service_protocol::{
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
Transaction, WitnessSet,
};
use jsonrpsee::{core::SubscriptionResult, types::ErrorObjectOwned};
use jsonrpsee::{
core::{SubscriptionResult, async_trait},
types::ErrorObjectOwned,
};
/// A mock implementation of the `IndexerService` RPC for testing purposes.
pub struct MockIndexerService {
@ -92,7 +95,7 @@ impl MockIndexerService {
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: indexer_service_protocol::Proof(vec![0; 32]),
proof: None,
},
}),
// PrivacyPreserving transactions
@ -124,7 +127,7 @@ impl MockIndexerService {
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: indexer_service_protocol::Proof(vec![0; 32]),
proof: Some(indexer_service_protocol::Proof(vec![0; 32])),
},
}),
// ProgramDeployment transactions (rare)
@ -171,7 +174,7 @@ impl MockIndexerService {
}
}
#[async_trait::async_trait]
#[async_trait]
impl indexer_service_rpc::RpcServer for MockIndexerService {
async fn subscribe_to_finalized_blocks(
&self,

View File

@ -7,7 +7,7 @@ use indexer_core::{IndexerCore, config::IndexerConfig};
use indexer_service_protocol::{Account, AccountId, Block, BlockId, HashType, Transaction};
use jsonrpsee::{
SubscriptionSink,
core::{Serialize, SubscriptionResult},
core::{Serialize, SubscriptionResult, async_trait},
types::{ErrorCode, ErrorObject, ErrorObjectOwned},
};
use log::{debug, error, info, warn};
@ -30,7 +30,7 @@ impl IndexerService {
}
}
#[async_trait::async_trait]
#[async_trait]
impl indexer_service_rpc::RpcServer for IndexerService {
async fn subscribe_to_finalized_blocks(
&self,

View File

@ -14,10 +14,12 @@ anyhow.workspace = true
thiserror.workspace = true
risc0-zkvm.workspace = true
serde.workspace = true
serde_with.workspace = true
sha2.workspace = true
rand.workspace = true
borsh.workspace = true
hex.workspace = true
base64.workspace = true
secp256k1 = "0.31.1"
risc0-binfmt = "3.0.2"
log.workspace = true

14
nssa/src/base64.rs Normal file
View File

@ -0,0 +1,14 @@
use base64::prelude::{BASE64_STANDARD, Engine as _};
use serde::{Deserialize as _, Deserializer, Serialize as _, Serializer};
pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
let base64 = BASE64_STANDARD.encode(v);
String::serialize(&base64, s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64 = String::deserialize(d)?;
BASE64_STANDARD
.decode(base64.as_bytes())
.map_err(serde::de::Error::custom)
}

View File

@ -18,6 +18,7 @@ pub use public_transaction::PublicTransaction;
pub use signature::{PrivateKey, PublicKey, Signature};
pub use state::V02State;
mod base64;
pub mod encoding;
pub mod error;
mod merkle_tree;

View File

@ -8,6 +8,7 @@ use nssa_core::{
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
};
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
use serde::{Deserialize, Serialize};
use crate::{
error::NssaError,
@ -17,8 +18,8 @@ use crate::{
};
/// Proof of the privacy preserving execution circuit.
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct Proof(pub(crate) Vec<u8>);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Proof(#[serde(with = "crate::base64")] pub(crate) Vec<u8>);
impl Proof {
#[must_use]

View File

@ -4,13 +4,14 @@ use nssa_core::{
account::{Account, Nonce},
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
use crate::{AccountId, error::NssaError};
pub type ViewTag = u8;
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct EncryptedAccountData {
pub ciphertext: Ciphertext,
pub epk: EphemeralPublicKey,
@ -44,7 +45,7 @@ impl EncryptedAccountData {
}
}
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Message {
pub public_account_ids: Vec<AccountId>,
pub nonces: Vec<Nonce>,

View File

@ -8,6 +8,7 @@ use nssa_core::{
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
account::{Account, AccountWithMetadata},
};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, digest::FixedOutput as _};
use super::{message::Message, witness_set::WitnessSet};
@ -17,7 +18,7 @@ use crate::{
privacy_preserving_transaction::{circuit::Proof, message::EncryptedAccountData},
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct PrivacyPreservingTransaction {
pub message: Message,
pub witness_set: WitnessSet,

View File

@ -1,11 +1,12 @@
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use crate::{
PrivateKey, PublicKey, Signature,
privacy_preserving_transaction::{circuit::Proof, message::Message},
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct WitnessSet {
pub(crate) signatures_and_public_keys: Vec<(Signature, PublicKey)>,
pub(crate) proof: Proof,

View File

@ -1,7 +1,9 @@
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Message {
#[serde(with = "crate::base64")]
pub(crate) bytecode: Vec<u8>,
}

View File

@ -1,12 +1,13 @@
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::account::AccountId;
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, digest::FixedOutput as _};
use crate::{
V02State, error::NssaError, program::Program, program_deployment_transaction::message::Message,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct ProgramDeploymentTransaction {
pub message: Message,
}

View File

@ -3,11 +3,11 @@ use nssa_core::{
account::Nonce,
program::{InstructionData, ProgramId},
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::{AccountId, error::NssaError, program::Program};
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Message {
pub program_id: ProgramId,
pub account_ids: Vec<AccountId>,

View File

@ -6,6 +6,7 @@ use nssa_core::{
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, digest::FixedOutput as _};
use crate::{
@ -15,7 +16,7 @@ use crate::{
state::MAX_NUMBER_CHAINED_CALLS,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct PublicTransaction {
pub message: Message,
pub witness_set: WitnessSet,

View File

@ -1,8 +1,9 @@
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use crate::{PrivateKey, PublicKey, Signature, public_transaction::Message};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct WitnessSet {
pub(crate) signatures_and_public_keys: Vec<(Signature, PublicKey)>,
}

View File

@ -1,22 +1,43 @@
use std::str::FromStr;
use borsh::{BorshDeserialize, BorshSerialize};
pub use private_key::PrivateKey;
pub use public_key::PublicKey;
use rand::{RngCore as _, rngs::OsRng};
use serde_with::{DeserializeFromStr, SerializeDisplay};
mod private_key;
mod public_key;
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(
Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, BorshSerialize, BorshDeserialize,
)]
pub struct Signature {
pub value: [u8; 64],
}
impl std::fmt::Debug for Signature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::fmt::Display for Signature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.value))
}
}
impl FromStr for Signature {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0_u8; 64];
hex::decode_to_slice(s, &mut bytes)?;
Ok(Self { value: bytes })
}
}
impl Signature {
#[must_use]
pub fn new(key: &PrivateKey, message: &[u8]) -> Self {

View File

@ -1,13 +1,37 @@
use std::str::FromStr;
use rand::{Rng as _, rngs::OsRng};
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::error::NssaError;
// TODO: Remove Debug, Clone, Serialize, Deserialize, PartialEq and Eq for security reasons
// TODO: Implement Zeroize
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, SerializeDisplay, DeserializeFromStr, PartialEq, Eq)]
pub struct PrivateKey([u8; 32]);
impl std::fmt::Debug for PrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::fmt::Display for PrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl FromStr for PrivateKey {
type Err = NssaError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0_u8; 32];
hex::decode_to_slice(s, &mut bytes).map_err(|_err| NssaError::InvalidPrivateKey)?;
Self::try_new(bytes)
}
}
impl PrivateKey {
#[must_use]
pub fn new_os_random() -> Self {

View File

@ -1,19 +1,38 @@
use std::str::FromStr;
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::account::AccountId;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use sha2::{Digest as _, Sha256};
use crate::{PrivateKey, error::NssaError};
#[derive(Clone, PartialEq, Eq, BorshSerialize, Serialize, Deserialize)]
#[derive(Clone, PartialEq, Eq, BorshSerialize, SerializeDisplay, DeserializeFromStr)]
pub struct PublicKey([u8; 32]);
impl std::fmt::Debug for PublicKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::fmt::Display for PublicKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl FromStr for PublicKey {
type Err = NssaError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0_u8; 32];
hex::decode_to_slice(s, &mut bytes)
.map_err(|_err| NssaError::InvalidPublicKey(secp256k1::Error::InvalidPublicKey))?;
Self::try_new(bytes)
}
}
impl BorshDeserialize for PublicKey {
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
let mut buf = [0_u8; 32];

View File

@ -7,7 +7,7 @@ use common::{
transaction::NSSATransaction,
};
use nssa::V02State;
use storage::sequencer::RocksDBIO;
use storage::{error::DbError, sequencer::RocksDBIO};
pub struct SequencerStore {
dbio: RocksDBIO,
@ -42,8 +42,8 @@ impl SequencerStore {
})
}
pub fn get_block_at_id(&self, id: u64) -> Result<Block> {
Ok(self.dbio.get_block(id)?)
pub fn get_block_at_id(&self, id: u64) -> Result<Block, DbError> {
self.dbio.get_block(id)
}
pub fn delete_block_at_id(&mut self, block_id: u64) -> Result<()> {

View File

@ -22,8 +22,6 @@ use url::Url;
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.
@ -41,8 +39,6 @@ pub struct SequencerConfig {
/// 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.

View File

@ -27,6 +27,8 @@ pub mod block_store;
pub mod config;
pub mod indexer_client;
pub use storage::error::DbError;
#[cfg(feature = "mock")]
pub mod mock;
@ -394,7 +396,6 @@ mod tests {
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(),

View File

@ -9,20 +9,25 @@ workspace = true
[dependencies]
common.workspace = true
nssa.workspace = true
nssa_core.workspace = true
mempool.workspace = true
borsh.workspace = true
sequencer_core = { workspace = true, features = ["testnet"] }
sequencer_rpc.workspace = true
sequencer_service_rpc = { workspace = true, features = ["server"] }
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
tokio-util.workspace = true
jsonrpsee.workspace = true
futures.workspace = true
bytesize.workspace = true
[features]
default = []
# Runs the sequencer in standalone mode without depending on Bedrock and Indexer services.
standalone = ["sequencer_core/mock", "sequencer_rpc/standalone"]
standalone = ["sequencer_core/mock"]

View File

@ -1,5 +1,5 @@
[package]
name = "sequencer_rpc"
name = "sequencer_service_rpc"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
@ -8,32 +8,14 @@ license = { workspace = true }
workspace = true
[dependencies]
nssa.workspace = true
common.workspace = true
mempool.workspace = true
sequencer_core = { workspace = true }
bedrock_client.workspace = true
nssa.workspace = true
nssa_core.workspace = true
anyhow.workspace = true
jsonrpsee = { workspace = true, features = ["macros"] }
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"] }
schemars.workspace = true
[features]
default = []
# Includes types to run the sequencer in standalone mode
standalone = ["sequencer_core/mock"]
client = ["jsonrpsee/client"]
server = ["jsonrpsee/server"]

View File

@ -1,55 +1,74 @@
use std::sync::Arc;
use std::collections::BTreeMap;
use common::{
rpc_primitives::errors::{RpcError, RpcErrorKind},
HashType,
block::{Block, BlockId},
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 jsonrpsee::proc_macros::rpc;
#[cfg(feature = "server")]
use jsonrpsee::types::ErrorObjectOwned;
use nssa::{Account, AccountId, ProgramId};
use nssa_core::{Commitment, MembershipProof, account::Nonce};
use self::types::err_rpc::RpcErr;
#[cfg(all(not(feature = "server"), not(feature = "client")))]
compile_error!("At least one of `server` or `client` features must be enabled.");
pub mod net_utils;
pub mod process;
pub mod types;
#[cfg_attr(all(feature = "server", not(feature = "client")), rpc(server))]
#[cfg_attr(all(feature = "client", not(feature = "server")), rpc(client))]
#[cfg_attr(all(feature = "server", feature = "client"), rpc(server, client))]
pub trait Rpc {
#[method(name = "sendTransaction")]
async fn send_transaction(&self, tx: NSSATransaction) -> Result<HashType, ErrorObjectOwned>;
#[cfg(feature = "standalone")]
pub type JsonHandlerWithMockClients = JsonHandler<MockBlockSettlementClient, MockIndexerClient>;
// TODO: expand healthcheck response into some kind of report
#[method(name = "checkHealth")]
async fn check_health(&self) -> Result<(), ErrorObjectOwned>;
// 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,
}
// TODO: These functions should be removed after wallet starts using indexer
// for this type of queries.
//
// =============================================================================================
#[method(name = "getBlockData")]
async fn get_block_data(&self, block_id: BlockId) -> Result<Block, ErrorObjectOwned>;
#[method(name = "getBlockRangeData")]
async fn get_block_range_data(
&self,
start_block_id: BlockId,
end_block_id: BlockId,
) -> Result<Vec<Block>, ErrorObjectOwned>;
#[method(name = "getLastBlockId")]
async fn get_last_block_id(&self) -> Result<BlockId, ErrorObjectOwned>;
#[method(name = "getAccountBalance")]
async fn get_account_balance(&self, account_id: AccountId) -> Result<u128, ErrorObjectOwned>;
#[method(name = "getTransactionByHash")]
async fn get_transaction_by_hash(
&self,
hash: HashType,
) -> Result<NSSATransaction, ErrorObjectOwned>;
#[method(name = "getAccountsNonces")]
async fn get_accounts_nonces(
&self,
account_ids: Vec<AccountId>,
) -> Result<Vec<Nonce>, ErrorObjectOwned>;
#[method(name = "getProofForCommitment")]
async fn get_proof_for_commitment(
&self,
commitment: Commitment,
) -> Result<MembershipProof, ErrorObjectOwned>;
#[method(name = "getAccount")]
async fn get_account(&self, account_id: AccountId) -> Result<Account, ErrorObjectOwned>;
#[method(name = "getProgramIds")]
async fn get_program_ids(&self) -> Result<BTreeMap<String, ProgramId>, ErrorObjectOwned>;
// =============================================================================================
}

View File

@ -1,104 +0,0 @@
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))
}

View File

@ -1,786 +0,0 @@
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);
}
}

View File

@ -1,49 +0,0 @@
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()))
}
}

View File

@ -1 +0,0 @@
pub mod err_rpc;

View File

@ -1,59 +1,75 @@
use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use std::{net::SocketAddr, 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};
use anyhow::{Context as _, Result, anyhow};
use bytesize::ByteSize;
use common::transaction::NSSATransaction;
use futures::never::Never;
use jsonrpsee::server::ServerHandle;
#[cfg(not(feature = "standalone"))]
use log::warn;
use log::{error, info};
use mempool::MemPoolHandle;
#[cfg(feature = "standalone")]
use sequencer_core::SequencerCoreWithMockClients as SequencerCore;
use sequencer_core::config::SequencerConfig;
pub use sequencer_core::config::*;
#[cfg(not(feature = "standalone"))]
use sequencer_core::{SequencerCore, block_settlement_client::BlockSettlementClientTrait as _};
use sequencer_rpc::new_http_server;
use sequencer_service_rpc::RpcServer as _;
use tokio::{sync::Mutex, task::JoinHandle};
pub const RUST_LOG: &str = "RUST_LOG";
pub mod service;
#[derive(Parser, Debug)]
#[clap(version)]
struct Args {
/// Path to configs.
home_dir: PathBuf,
}
const REQUEST_BODY_MAX_SIZE: ByteSize = ByteSize::mib(10);
/// Handle to manage the sequencer and its tasks.
///
/// Implements `Drop` to ensure all tasks are aborted and the HTTP server is stopped when dropped.
/// Implements `Drop` to ensure all tasks are aborted and the RPC server is stopped when dropped.
pub struct SequencerHandle {
addr: SocketAddr,
http_server_handle: ServerHandle,
/// Option because of `Drop` which forbids to simply move out of `self` in `stopped()`.
server_handle: Option<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.
fn new(
addr: SocketAddr,
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>>,
) -> Self {
Self {
addr,
server_handle: Some(server_handle),
main_loop_handle,
retry_pending_blocks_loop_handle,
listen_for_bedrock_blocks_loop_handle,
}
}
/// Wait for all Sequencer tasks to stop.
#[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> {
pub async fn stopped(mut self) -> Result<Never> {
let Self {
addr: _,
http_server_handle: _,
server_handle,
main_loop_handle,
retry_pending_blocks_loop_handle,
listen_for_bedrock_blocks_loop_handle,
} = self;
} = &mut self;
let server_handle = server_handle.take().expect("Server handle is set");
tokio::select! {
_ = server_handle.stopped() => {
Err(anyhow!("RPC Server stopped"))
}
res = main_loop_handle => {
res
.context("Main loop task panicked")?
@ -89,7 +105,7 @@ impl Drop for SequencerHandle {
fn drop(&mut self) {
let Self {
addr: _,
http_server_handle,
server_handle,
main_loop_handle,
retry_pending_blocks_loop_handle,
listen_for_bedrock_blocks_loop_handle,
@ -99,31 +115,35 @@ impl Drop for SequencerHandle {
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();
let Some(handle) = server_handle else {
return;
};
if let Err(err) = handle.stop() {
error!("An error occurred while stopping Sequencer RPC server: {err}");
}
}
}
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;
pub async fn run(config: SequencerConfig, port: u16) -> Result<SequencerHandle> {
let block_timeout = config.block_create_timeout;
let retry_pending_blocks_timeout = config.retry_pending_blocks_timeout;
let max_block_size = config.max_block_size;
let (sequencer_core, mempool_handle) = SequencerCore::start_from_config(app_config).await;
let (sequencer_core, mempool_handle) = SequencerCore::start_from_config(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),
let (server_handle, addr) = run_server(
Arc::clone(&seq_core_wrapped),
mempool_handle,
port,
max_block_size.as_u64(),
)
.await?;
info!("HTTP server started");
let http_server_handle = http_server.handle();
tokio::spawn(http_server);
info!("RPC server started");
#[cfg(not(feature = "standalone"))]
{
@ -146,13 +166,42 @@ pub async fn startup_sequencer(app_config: SequencerConfig) -> Result<SequencerH
let listen_for_bedrock_blocks_loop_handle =
tokio::spawn(listen_for_bedrock_blocks_loop(seq_core_wrapped));
Ok(SequencerHandle {
Ok(SequencerHandle::new(
addr,
http_server_handle,
server_handle,
main_loop_handle,
retry_pending_blocks_loop_handle,
listen_for_bedrock_blocks_loop_handle,
})
))
}
async fn run_server(
sequencer: Arc<Mutex<SequencerCore>>,
mempool_handle: MemPoolHandle<NSSATransaction>,
port: u16,
max_block_size: u64,
) -> Result<(ServerHandle, SocketAddr)> {
let server = jsonrpsee::server::ServerBuilder::with_config(
jsonrpsee::server::ServerConfigBuilder::new()
.max_request_body_size(
u32::try_from(REQUEST_BODY_MAX_SIZE.as_u64())
.expect("REQUEST_BODY_MAX_SIZE should be less than u32::MAX"),
)
.build(),
)
.build(SocketAddr::from(([0, 0, 0, 0], port)))
.await
.context("Failed to build RPC server")?;
let addr = server
.local_addr()
.context("Failed to get local address of RPC server")?;
info!("Starting Sequencer Service RPC server on {addr}");
let service = service::SequencerService::new(sequencer, mempool_handle, max_block_size);
let handle = server.start(service.into_rpc());
Ok((handle, addr))
}
async fn main_loop(seq_core: Arc<Mutex<SequencerCore>>, block_timeout: Duration) -> Result<Never> {
@ -210,7 +259,7 @@ async fn retry_pending_blocks(seq_core: &Arc<Mutex<SequencerCore>>) -> Result<()
.create_inscribe_tx(block)
.context("Failed to create inscribe tx for pending block")?;
debug!(">>>> Create inscribe: {:?}", now.elapsed());
debug!("Create inscribe: {:?}", now.elapsed());
let now = Instant::now();
if let Err(e) = block_settlement_client
@ -222,7 +271,7 @@ async fn retry_pending_blocks(seq_core: &Arc<Mutex<SequencerCore>>) -> Result<()
block.header.block_id
);
}
debug!(">>>> Post: {:?}", now.elapsed());
debug!("Post: {:?}", now.elapsed());
}
Ok(())
}
@ -287,33 +336,3 @@ async fn retry_pending_blocks_loop(
) -> 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(())
}

View File

@ -1,16 +1,60 @@
use std::path::PathBuf;
use anyhow::Result;
use sequencer_service::main_runner;
use clap::Parser;
use log::{error, info};
use tokio_util::sync::CancellationToken;
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())
#[derive(Debug, Parser)]
#[clap(version)]
struct Args {
#[clap(name = "config")]
config_path: PathBuf,
#[clap(short, long, default_value = "3040")]
port: u16,
}
#[tokio::main]
#[expect(
clippy::integer_division_remainder_used,
reason = "Generated by select! macro, can't be easily rewritten to avoid this lint"
)]
async fn main() -> Result<()> {
env_logger::init();
let Args { config_path, port } = Args::parse();
let cancellation_token = listen_for_shutdown_signal();
let config = sequencer_service::SequencerConfig::from_path(&config_path)?;
let sequencer_handle = sequencer_service::run(config, port).await?;
tokio::select! {
() = cancellation_token.cancelled() => {
info!("Shutting down sequencer...");
}
Err(err) = sequencer_handle.stopped() => {
error!("Sequencer stopped unexpectedly: {err}");
}
}
info!("Sequencer shutdown complete");
Ok(())
}
fn listen_for_shutdown_signal() -> CancellationToken {
let cancellation_token = CancellationToken::new();
let cancellation_token_clone = cancellation_token.clone();
tokio::spawn(async move {
if let Err(err) = tokio::signal::ctrl_c().await {
error!("Failed to listen for Ctrl-C signal: {err}");
return;
}
info!("Received Ctrl-C signal");
cancellation_token_clone.cancel();
});
cancellation_token
}

View File

@ -0,0 +1,202 @@
use std::{collections::BTreeMap, sync::Arc};
use common::{
HashType,
block::{Block, BlockId},
transaction::NSSATransaction,
};
use jsonrpsee::{
core::async_trait,
types::{ErrorCode, ErrorObjectOwned},
};
use log::warn;
use mempool::MemPoolHandle;
use nssa::{self, program::Program};
use nssa_core::{
Commitment, MembershipProof,
account::{Account, AccountId, Nonce},
program::ProgramId,
};
use sequencer_core::{
DbError, SequencerCore, block_settlement_client::BlockSettlementClientTrait,
indexer_client::IndexerClientTrait,
};
use tokio::sync::Mutex;
pub struct SequencerService<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> {
sequencer: Arc<Mutex<SequencerCore<BC, IC>>>,
mempool_handle: MemPoolHandle<NSSATransaction>,
max_block_size: u64,
}
impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerService<BC, IC> {
pub fn new(
sequencer: Arc<Mutex<SequencerCore<BC, IC>>>,
mempool_handle: MemPoolHandle<NSSATransaction>,
max_block_size: u64,
) -> Self {
Self {
sequencer,
mempool_handle,
max_block_size,
}
}
}
#[async_trait]
impl<BC: BlockSettlementClientTrait + Send + 'static, IC: IndexerClientTrait + Send + 'static>
sequencer_service_rpc::RpcServer for SequencerService<BC, IC>
{
async fn send_transaction(&self, tx: NSSATransaction) -> Result<HashType, ErrorObjectOwned> {
// Reserve ~200 bytes for block header overhead
const BLOCK_HEADER_OVERHEAD: u64 = 200;
let tx_hash = tx.hash();
let tx_size = u64::try_from(
borsh::to_vec(&tx)
.expect("NSSATransaction BorshSerialize should never fail")
.len(),
)
.expect("Transaction size should fit in u64");
let max_tx_size = self.max_block_size.saturating_sub(BLOCK_HEADER_OVERHEAD);
if tx_size > max_tx_size {
return Err(ErrorObjectOwned::owned(
ErrorCode::InvalidParams.code(),
format!("Transaction too large: size {tx_size}, max {max_tx_size}"),
None::<()>,
));
}
let authenticated_tx = tx
.transaction_stateless_check()
.inspect_err(|err| warn!("Error at pre_check {err:#?}"))
.map_err(|err| {
ErrorObjectOwned::owned(
ErrorCode::InvalidParams.code(),
format!("{err:?}"),
None::<()>,
)
})?;
self.mempool_handle
.push(authenticated_tx)
.await
.expect("Mempool is closed, this is a bug");
Ok(tx_hash)
}
async fn check_health(&self) -> Result<(), ErrorObjectOwned> {
Ok(())
}
async fn get_block_data(&self, block_id: BlockId) -> Result<Block, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
sequencer
.block_store()
.get_block_at_id(block_id)
.map_err(|err| db_error_to_rpc_error(&err))
}
async fn get_block_range_data(
&self,
start_block_id: BlockId,
end_block_id: BlockId,
) -> Result<Vec<Block>, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
(start_block_id..=end_block_id)
.map(|block_id| sequencer.block_store().get_block_at_id(block_id))
.collect::<Result<Vec<_>, _>>()
.map_err(|err| db_error_to_rpc_error(&err))
}
async fn get_last_block_id(&self) -> Result<BlockId, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
Ok(sequencer.chain_height())
}
async fn get_account_balance(&self, account_id: AccountId) -> Result<u128, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
let account = sequencer.state().get_account_by_id(account_id);
Ok(account.balance)
}
async fn get_transaction_by_hash(
&self,
hash: HashType,
) -> Result<NSSATransaction, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
sequencer
.block_store()
.get_transaction_by_hash(hash)
.ok_or_else(|| {
ErrorObjectOwned::owned(NOT_FOUND_ERROR_CODE, "Transaction not found", None::<()>)
})
}
async fn get_accounts_nonces(
&self,
account_ids: Vec<AccountId>,
) -> Result<Vec<Nonce>, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
let nonces = account_ids
.into_iter()
.map(|account_id| sequencer.state().get_account_by_id(account_id).nonce)
.collect();
Ok(nonces)
}
async fn get_proof_for_commitment(
&self,
commitment: Commitment,
) -> Result<MembershipProof, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
sequencer
.state()
.get_proof_for_commitment(&commitment)
.ok_or_else(|| {
ErrorObjectOwned::owned(
NOT_FOUND_ERROR_CODE,
"Proof for commitment not found",
None::<()>,
)
})
}
async fn get_account(&self, account_id: AccountId) -> Result<Account, ErrorObjectOwned> {
let sequencer = self.sequencer.lock().await;
Ok(sequencer.state().get_account_by_id(account_id))
}
async fn get_program_ids(&self) -> Result<BTreeMap<String, ProgramId>, ErrorObjectOwned> {
let mut program_ids = BTreeMap::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,
);
Ok(program_ids)
}
}
const NOT_FOUND_ERROR_CODE: i32 = -31999;
fn db_error_to_rpc_error(err: &DbError) -> ErrorObjectOwned {
match err {
DbError::NotFound { entity } => ErrorObjectOwned::owned(
NOT_FOUND_ERROR_CODE,
format!("{entity} not found"),
None::<()>,
),
_ => ErrorObjectOwned::owned(ErrorCode::InternalError.code(), err.to_string(), None::<()>),
}
}

View File

@ -14,6 +14,8 @@ pub enum DbError {
},
#[error("Logic Error: {additional_info}")]
DbInteractionError { additional_info: String },
#[error("{entity} not found")]
NotFound { entity: String },
}
impl DbError {
@ -39,4 +41,9 @@ impl DbError {
additional_info: message,
}
}
#[must_use]
pub const fn not_found(entity: String) -> Self {
Self::NotFound { entity }
}
}

View File

@ -180,9 +180,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"First block not found".to_owned(),
))
Err(DbError::not_found("First block".to_owned()))
}
}
@ -209,9 +207,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"Last block not found".to_owned(),
))
Err(DbError::not_found("Last block".to_owned()))
}
}
@ -286,9 +282,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"Last breakpoint id not found".to_owned(),
))
Err(DbError::not_found("Last breakpoint id".to_owned()))
}
}
@ -537,9 +531,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"Block on this id not found".to_owned(),
))
Err(DbError::not_found(format!("Block with id {block_id}")))
}
}
@ -618,7 +610,7 @@ impl RocksDBIO {
.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))
}
pub fn get_breakpoint(&self, br_id: u64) -> DbResult<V02State> {
fn get_breakpoint(&self, br_id: u64) -> DbResult<V02State> {
let cf_br = self.breakpoint_column();
let res = self
.db
@ -641,6 +633,8 @@ impl RocksDBIO {
)
})?)
} else {
// Note: this is not a `DbError::NotFound` case, because we expect that all searched
// breakpoints will be present in db as this is an internal method.
Err(DbError::db_interaction_error(
"Breakpoint on this id not found".to_owned(),
))
@ -686,9 +680,7 @@ impl RocksDBIO {
Ok(breakpoint)
} else {
Err(DbError::db_interaction_error(
"Block on this id not found".to_owned(),
))
Err(DbError::not_found(format!("Block with id {block_id}")))
}
}
@ -740,9 +732,7 @@ impl RocksDBIO {
DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned()))
})?)
} else {
Err(DbError::db_interaction_error(
"Block on this hash not found".to_owned(),
))
Err(DbError::not_found("Block with given hash".to_owned()))
}
}
@ -766,9 +756,7 @@ impl RocksDBIO {
DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned()))
})?)
} else {
Err(DbError::db_interaction_error(
"Block for this tx hash not found".to_owned(),
))
Err(DbError::not_found("Block for given tx hash".to_owned()))
}
}

View File

@ -142,9 +142,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"First block not found".to_owned(),
))
Err(DbError::not_found("First block".to_owned()))
}
}
@ -171,9 +169,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"Last block not found".to_owned(),
))
Err(DbError::not_found("Last block".to_owned()))
}
}
@ -399,9 +395,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"Latest block meta not found".to_owned(),
))
Err(DbError::not_found("Latest block meta".to_owned()))
}
}
@ -465,9 +459,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"Block on this id not found".to_owned(),
))
Err(DbError::not_found(format!("Block with id {block_id}")))
}
}
@ -494,9 +486,7 @@ impl RocksDBIO {
)
})?)
} else {
Err(DbError::db_interaction_error(
"Block on this id not found".to_owned(),
))
Err(DbError::not_found("NSSA state".to_owned()))
}
}
@ -512,9 +502,7 @@ impl RocksDBIO {
.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?
.is_none()
{
return Err(DbError::db_interaction_error(
"Block on this id not found".to_owned(),
));
return Err(DbError::not_found(format!("Block with id {block_id}")));
}
self.db

View File

@ -9,79 +9,13 @@
{
"Public": {
"account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r",
"pub_sign_key": [
127,
39,
48,
152,
242,
91,
113,
230,
192,
5,
169,
81,
159,
38,
120,
218,
141,
28,
127,
1,
246,
162,
119,
120,
226,
217,
148,
138,
189,
249,
1,
251
]
"pub_sign_key": "7f273098f25b71e6c005a9519f2678da8d1c7f01f6a27778e2d9948abdf901fb"
}
},
{
"Public": {
"account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2",
"pub_sign_key": [
244,
52,
248,
116,
23,
32,
1,
69,
134,
174,
67,
53,
109,
42,
236,
98,
87,
218,
8,
98,
34,
246,
4,
221,
183,
93,
105,
115,
59,
134,
252,
76
]
"pub_sign_key": "f434f8741720014586ae43356d2aec6257da086222f604ddb75d69733b86fc4c"
}
},
{

View File

@ -51,6 +51,22 @@ pub enum AccDecodeData {
Decode(nssa_core::SharedSecretKey, AccountId),
}
#[derive(Debug, thiserror::Error)]
pub enum ExecutionFailureKind {
#[error("Failed to get data from sequencer")]
SequencerError(#[source] anyhow::Error),
#[error("Inputs amounts does not match outputs")]
AmountMismatchError,
#[error("Accounts key not found")]
KeyNotFoundError,
#[error("Sequencer client error: {0:?}")]
SequencerClientError(#[from] SequencerClientError),
#[error("Can not pay for operation")]
InsufficientFundsError,
#[error("Account {0} data is invalid")]
AccountDataError(AccountId),
}
#[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")]
pub struct WalletCore {
config_path: PathBuf,