v0.3.10-pre.006
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 498
|
# version: 499
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"]
|
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.10-pre.5.fix.2"
|
version = "0.3.10-pre.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-job-backfill-lib/Cargo.toml
|
# file: crates/ksp-job-backfill-lib/Cargo.toml
|
||||||
# version: 6
|
# version: 7
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-job-backfill-lib"
|
name = "ksp-job-backfill-lib"
|
||||||
@@ -22,6 +22,8 @@ tokio = { workspace = true, features = ["macros", "sync"] }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
tokio-tungstenite = { workspace = true, features = ["handshake"] }
|
tokio-tungstenite = { workspace = true, features = ["handshake"] }
|
||||||
|
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||||
|
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Dependency firewall canaries through the concrete cancellation and latest-value runtime tranche.
|
//! Dependency firewall canaries through the concrete cancellation and latest-value runtime tranche.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_boundaries() {
|
fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_boundaries() {
|
||||||
let manifest = include_str!("../Cargo.toml");
|
let manifest = include_str!("../Cargo.toml");
|
||||||
|
let production_manifest = match manifest.split_once("[dev-dependencies]") {
|
||||||
|
std::option::Option::Some((production, _)) => production,
|
||||||
|
std::option::Option::None => manifest,
|
||||||
|
};
|
||||||
for required in [
|
for required in [
|
||||||
"futures-util = { workspace = true, features = [\"std\"] }",
|
"futures-util = { workspace = true, features = [\"std\"] }",
|
||||||
"ksp-core-lib = { path = \"../ksp-core-lib\" }",
|
"ksp-core-lib = { path = \"../ksp-core-lib\" }",
|
||||||
@@ -18,7 +22,7 @@ fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_bound
|
|||||||
"sha2.workspace = true",
|
"sha2.workspace = true",
|
||||||
"tokio = { workspace = true, features = [\"macros\", \"sync\"] }",
|
"tokio = { workspace = true, features = [\"macros\", \"sync\"] }",
|
||||||
] {
|
] {
|
||||||
assert!(manifest.contains(required), "required Backfill dependency missing: {required}");
|
assert!(production_manifest.contains(required), "required Backfill dependency missing: {required}");
|
||||||
}
|
}
|
||||||
for forbidden in [
|
for forbidden in [
|
||||||
"ksp-config-lib",
|
"ksp-config-lib",
|
||||||
@@ -33,7 +37,7 @@ fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_bound
|
|||||||
"serde = {",
|
"serde = {",
|
||||||
"tonic",
|
"tonic",
|
||||||
] {
|
] {
|
||||||
assert!(!manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
assert!(!production_manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
||||||
}
|
}
|
||||||
let root = include_str!("../src/lib.rs");
|
let root = include_str!("../src/lib.rs");
|
||||||
assert!(!root.contains("tokio::"), "Tokio implementation types must not leak through the public crate root");
|
assert!(!root.contains("tokio::"), "Tokio implementation types must not leak through the public crate root");
|
||||||
@@ -59,6 +63,30 @@ fn pre_005_websocket_parity_dependency_stays_test_only() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_yellowstone_parity_dependencies_stay_test_only() {
|
||||||
|
let manifest = include_str!("../Cargo.toml");
|
||||||
|
let parts = manifest.splitn(2, "[dev-dependencies]").collect::<std::vec::Vec<_>>();
|
||||||
|
let (dependencies, dev_dependencies) = match parts.as_slice() {
|
||||||
|
[dependencies, dev_dependencies] => (*dependencies, *dev_dependencies),
|
||||||
|
_ => {
|
||||||
|
assert_eq!(parts.len(), 2, "Backfill manifest must retain exactly one dev-dependencies section");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert!(!dependencies.contains("tonic"), "Yellowstone parity server must not become a production Backfill dependency");
|
||||||
|
assert!(!dependencies.contains("yellowstone-grpc-proto"), "Yellowstone protobuf must not become a production Backfill dependency");
|
||||||
|
assert!(
|
||||||
|
dev_dependencies.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"),
|
||||||
|
"Yellowstone parity server must remain an explicit test-only dependency",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dev_dependencies.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"),
|
||||||
|
"Yellowstone protobuf must remain an explicit test-only dependency",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separate() {
|
fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separate() {
|
||||||
let neutral_sources = [
|
let neutral_sources = [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
|
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
|
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ fn pre_010_manifest_dependency_surface_is_exact_and_backend_neutral() {
|
|||||||
"tokio",
|
"tokio",
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio", "tokio-tungstenite"]));
|
assert_eq!(dev, std::collections::BTreeSet::from(["tokio", "tokio-tungstenite", "tonic", "yellowstone-grpc-proto"]));
|
||||||
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
|
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
|
||||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
|
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
|
||||||
assert!(!manifest.contains("ksp-store-postgres-lib"));
|
assert!(!manifest.contains("ksp-store-postgres-lib"));
|
||||||
|
|||||||
465
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
Normal file
465
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
// file: crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
struct FixtureStream<T> {
|
||||||
|
receiver: tokio::sync::mpsc::Receiver<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FixtureStream<T> {
|
||||||
|
fn new(receiver: tokio::sync::mpsc::Receiver<T>) -> Self {
|
||||||
|
return Self { receiver };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> futures_util::Stream for FixtureStream<T> {
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn poll_next(self: std::pin::Pin<&mut Self>, context: &mut std::task::Context<'_>) -> std::task::Poll<std::option::Option<Self::Item>> {
|
||||||
|
return self.get_mut().receiver.poll_recv(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FixtureGeyser;
|
||||||
|
|
||||||
|
#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies.
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
|
||||||
|
type SubscribeStream = FixtureStream<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn subscribe(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, tonic::Status> {
|
||||||
|
let mut inbound = request.into_inner();
|
||||||
|
let (sender, receiver) = tokio::sync::mpsc::channel(4);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let initial = inbound.message().await;
|
||||||
|
if !matches!(initial, std::result::Result::Ok(std::option::Option::Some(_))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if sender.send(std::result::Result::Ok(transaction_update())).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = sender.send(std::result::Result::Ok(block_update())).await;
|
||||||
|
});
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(FixtureStream::new(receiver)));
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribeDeshredStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn subscribe_deshred(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeDeshredRequest>>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeDeshredStream>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("deshred is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribeGossipStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateGossip, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn subscribe_gossip(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeGossipRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeGossipStream>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("gossip is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn subscribe_replay_info(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("replay info is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ping(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::PingRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_latest_blockhash(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_block_height(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetBlockHeightRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetBlockHeightResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_slot(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetSlotRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetSlotResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_blockhash_valid(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::IsBlockhashValidRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::IsBlockhashValidResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_version(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetVersionRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetVersionResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FixtureServer {
|
||||||
|
endpoint_url: std::string::String,
|
||||||
|
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
|
||||||
|
task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixtureServer {
|
||||||
|
async fn start() -> std::result::Result<Self, &'static str> {
|
||||||
|
let bind_address = match "127.0.0.1:0".parse::<std::net::SocketAddr>() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err("fixture bind address must parse"),
|
||||||
|
};
|
||||||
|
let incoming = match tonic::transport::server::TcpIncoming::bind(bind_address) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err("fixture gRPC listener must bind"),
|
||||||
|
};
|
||||||
|
let local_address = match incoming.local_addr() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err("fixture gRPC listener must expose local address"),
|
||||||
|
};
|
||||||
|
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser);
|
||||||
|
let result = tonic::transport::Server::builder()
|
||||||
|
.serve_with_incoming_shutdown(service, incoming, async move {
|
||||||
|
let _ = shutdown_receiver.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
});
|
||||||
|
return std::result::Result::Ok(Self { endpoint_url: format!("http://{local_address}"), shutdown: std::option::Option::Some(shutdown), task });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop(mut self) {
|
||||||
|
if let std::option::Option::Some(shutdown) = self.shutdown.take() {
|
||||||
|
let _ = shutdown.send(());
|
||||||
|
}
|
||||||
|
let result = self.task.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_info() -> yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
|
||||||
|
signature: vec![9_u8; 64],
|
||||||
|
is_vote: false,
|
||||||
|
transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction {
|
||||||
|
signatures: vec![vec![9_u8; 64]],
|
||||||
|
message: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Message {
|
||||||
|
header: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::MessageHeader {
|
||||||
|
num_required_signatures: 1,
|
||||||
|
num_readonly_signed_accounts: 0,
|
||||||
|
num_readonly_unsigned_accounts: 1,
|
||||||
|
}),
|
||||||
|
account_keys: vec![vec![1_u8; 32], vec![2_u8; 32]],
|
||||||
|
recent_blockhash: vec![3_u8; 32],
|
||||||
|
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::CompiledInstruction {
|
||||||
|
program_id_index: 1,
|
||||||
|
accounts: vec![0_u8],
|
||||||
|
data: vec![4_u8, 5, 6],
|
||||||
|
}],
|
||||||
|
versioned: true,
|
||||||
|
address_table_lookups: vec![],
|
||||||
|
config: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionConfig {
|
||||||
|
priority_fee: std::option::Option::Some(7),
|
||||||
|
compute_unit_limit: std::option::Option::Some(8),
|
||||||
|
loaded_accounts_data_size_limit: std::option::Option::Some(9),
|
||||||
|
heap_size: std::option::Option::Some(32_768),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
meta: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta {
|
||||||
|
fee: 5_000,
|
||||||
|
pre_balances: vec![100, 200],
|
||||||
|
post_balances: vec![90, 210],
|
||||||
|
compute_units_consumed: std::option::Option::Some(123),
|
||||||
|
cost_units: std::option::Option::Some(456),
|
||||||
|
..std::default::Default::default()
|
||||||
|
}),
|
||||||
|
index: 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["transactions-pre-006".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Transaction(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction { transaction: std::option::Option::Some(transaction_info()), slot: 42 },
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["blocks-pre-006".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateBlock {
|
||||||
|
slot: 42,
|
||||||
|
blockhash: ksp_core_lib::Pubkey::new_from_array([21_u8; 32]).to_string(),
|
||||||
|
rewards: std::option::Option::None,
|
||||||
|
block_time: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UnixTimestamp { timestamp: 1_787_104_000 }),
|
||||||
|
block_height: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::BlockHeight { block_height: 41 }),
|
||||||
|
transactions: vec![transaction_info()],
|
||||||
|
parent_slot: 41,
|
||||||
|
parent_blockhash: ksp_core_lib::Pubkey::new_from_array([22_u8; 32]).to_string(),
|
||||||
|
executed_transaction_count: 1,
|
||||||
|
updated_account_count: 0,
|
||||||
|
accounts: vec![],
|
||||||
|
entries_count: 0,
|
||||||
|
entries: vec![],
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_settings(url: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||||
|
let endpoint_url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(url) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"pre-006-fixture",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
endpoint_url,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_wire(
|
||||||
|
transaction: &ksp_onchain_transport_lib::YellowstoneStoredTransaction,
|
||||||
|
) -> ksp_core_lib::Result<ksp_raw_transaction_lib::RawSolanaTransactionWire> {
|
||||||
|
let message = transaction.message();
|
||||||
|
let header = message.header();
|
||||||
|
let required = match u8::try_from(header.num_required_signatures()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_required_signatures")),
|
||||||
|
};
|
||||||
|
let readonly_signed = match u8::try_from(header.num_readonly_signed_accounts()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_readonly_signed_accounts")),
|
||||||
|
};
|
||||||
|
let readonly_unsigned = match u8::try_from(header.num_readonly_unsigned_accounts()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_readonly_unsigned_accounts")),
|
||||||
|
};
|
||||||
|
let version = if message.config().is_some() {
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1
|
||||||
|
} else if message.versioned() {
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V0
|
||||||
|
} else {
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::Legacy
|
||||||
|
};
|
||||||
|
let signatures = transaction.signatures().iter().map(|value| return *value.as_bytes()).collect();
|
||||||
|
let account_keys = message.account_keys().iter().map(solana_pubkey_bytes).collect();
|
||||||
|
let mut instructions = std::vec::Vec::with_capacity(message.instructions().len());
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
let program_id_index = match u8::try_from(instruction.program_id_index()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("program_id_index")),
|
||||||
|
};
|
||||||
|
instructions.push(ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(
|
||||||
|
program_id_index,
|
||||||
|
instruction.accounts().to_vec(),
|
||||||
|
instruction.data().to_vec(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let address_table_lookups = message
|
||||||
|
.address_table_lookups()
|
||||||
|
.iter()
|
||||||
|
.map(|lookup| {
|
||||||
|
return ksp_raw_transaction_lib::RawSolanaAddressTableLookup::new(
|
||||||
|
lookup.account_key().to_bytes(),
|
||||||
|
lookup.writable_indexes().to_vec(),
|
||||||
|
lookup.readonly_indexes().to_vec(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let config = message.config().map(|value| {
|
||||||
|
return ksp_raw_transaction_lib::RawSolanaTransactionConfig::new(
|
||||||
|
value.priority_fee(),
|
||||||
|
value.compute_unit_limit(),
|
||||||
|
value.loaded_accounts_data_size_limit(),
|
||||||
|
value.heap_size(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let raw_message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||||
|
version,
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(required, readonly_signed, readonly_unsigned),
|
||||||
|
account_keys,
|
||||||
|
*message.recent_blockhash().as_bytes(),
|
||||||
|
instructions,
|
||||||
|
address_table_lookups,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(ksp_raw_transaction_lib::RawSolanaTransactionWire::new(signatures, raw_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solana_pubkey_bytes(value: &ksp_core_lib::Pubkey) -> [u8; 32] {
|
||||||
|
return value.to_bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn projection_error(field: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new("KSP-RAW-TRANSACTION-PRE-006-PROJECTION", "Yellowstone fixture field does not fit common wire")
|
||||||
|
.with_context("field", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_v1_wire() -> ksp_raw_transaction_lib::RawSolanaTransactionWire {
|
||||||
|
let message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 1),
|
||||||
|
vec![[1_u8; 32], [2_u8; 32]],
|
||||||
|
[3_u8; 32],
|
||||||
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(1, vec![0_u8], vec![4_u8, 5, 6])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::new(
|
||||||
|
std::option::Option::Some(7),
|
||||||
|
std::option::Option::Some(8),
|
||||||
|
std::option::Option::Some(9),
|
||||||
|
std::option::Option::Some(32_768),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
return ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[9_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail_test(message: std::string::String) {
|
||||||
|
assert!(message.is_empty(), "{message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pre_006_yellowstone_transaction_and_block_preserve_exact_v1_wire_and_block_time() {
|
||||||
|
let server = match FixtureServer::start().await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
assert!(error.is_empty(), "fixture server start failed: {error}");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let settings = match fixture_settings(server.endpoint_url.as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("fixture settings failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&settings).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("fixture channel connect failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
||||||
|
let mut session = match channel.open_standard_subscribe(request).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("fixture subscribe failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let expected = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&expected_v1_wire()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("expected V1 wire serialization failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let transaction = match session.next_update().await {
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||||
|
std::result::Result::Ok(std::option::Option::None) => {
|
||||||
|
fail_test("transaction update must be present".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("transaction update decode failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let transaction = match transaction {
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Transaction(value) => value,
|
||||||
|
_ => {
|
||||||
|
fail_test("first Yellowstone fixture update must be a transaction".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(transaction.slot(), 42);
|
||||||
|
assert_eq!(transaction.transaction().index(), 3);
|
||||||
|
assert_eq!(transaction.transaction().meta().fee(), 5_000);
|
||||||
|
let transaction_wire = match project_wire(transaction.transaction().transaction()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("transaction projection failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let transaction_bytes = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&transaction_wire) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("transaction V1 wire serialization failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(transaction_bytes, expected);
|
||||||
|
let block = match session.next_update().await {
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||||
|
std::result::Result::Ok(std::option::Option::None) => {
|
||||||
|
fail_test("block update must be present".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("block update decode failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let block = match block {
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Block(value) => value,
|
||||||
|
_ => {
|
||||||
|
fail_test("second Yellowstone fixture update must be a block".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(block.slot(), 42);
|
||||||
|
assert_eq!(block.block_time(), std::option::Option::Some(1_787_104_000));
|
||||||
|
assert_eq!(block.transactions().len(), 1);
|
||||||
|
assert_eq!(block.transactions()[0].index(), 3);
|
||||||
|
let block_wire = match project_wire(block.transactions()[0].transaction()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("block transaction projection failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let block_bytes = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&block_wire) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("block V1 wire serialization failed: {}", error.code()));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(block_bytes, expected);
|
||||||
|
assert_eq!(block_bytes, transaction_bytes);
|
||||||
|
assert!(transaction.transaction().meta().error().is_none());
|
||||||
|
assert_eq!(block.transactions()[0].meta().fee(), transaction.transaction().meta().fee());
|
||||||
|
assert!(session.close().await.is_ok());
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-raw-transaction-lib/src/lib.rs
|
// file: crates/ksp-raw-transaction-lib/src/lib.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -16,6 +16,7 @@ mod acquisition;
|
|||||||
mod canonical;
|
mod canonical;
|
||||||
mod error;
|
mod error;
|
||||||
mod signature;
|
mod signature;
|
||||||
|
mod wire;
|
||||||
|
|
||||||
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
|
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
|
||||||
pub use self::acquisition::RawTransactionAcquisition;
|
pub use self::acquisition::RawTransactionAcquisition;
|
||||||
@@ -47,6 +48,24 @@ pub use self::signature::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
|||||||
pub use self::signature::extract_raw_transaction_signature_from_binary_base64;
|
pub use self::signature::extract_raw_transaction_signature_from_binary_base64;
|
||||||
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
|
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
|
||||||
pub use self::signature::parse_raw_transaction_signature;
|
pub use self::signature::parse_raw_transaction_signature;
|
||||||
|
/// One source-neutral v0 address-table lookup.
|
||||||
|
pub use self::wire::RawSolanaAddressTableLookup;
|
||||||
|
/// One source-neutral compiled Solana instruction.
|
||||||
|
pub use self::wire::RawSolanaCompiledInstruction;
|
||||||
|
/// Source-neutral Solana message header.
|
||||||
|
pub use self::wire::RawSolanaMessageHeader;
|
||||||
|
/// Source-neutral Solana transaction message version.
|
||||||
|
pub use self::wire::RawSolanaMessageVersion;
|
||||||
|
/// Optional source-neutral Transaction V1 inline configuration.
|
||||||
|
pub use self::wire::RawSolanaTransactionConfig;
|
||||||
|
/// Complete source-neutral Solana transaction message.
|
||||||
|
pub use self::wire::RawSolanaTransactionMessage;
|
||||||
|
/// Complete source-neutral Solana transaction wire material.
|
||||||
|
pub use self::wire::RawSolanaTransactionWire;
|
||||||
|
/// Serializes one source-neutral Solana transaction to exact canonical wire bytes.
|
||||||
|
pub use self::wire::serialize_solana_transaction_wire;
|
||||||
|
/// Serializes one source-neutral Solana transaction to canonical padded standard Base64.
|
||||||
|
pub use self::wire::serialize_solana_transaction_wire_base64;
|
||||||
|
|
||||||
/// Creates a safe canonicalization error without copying source payload material.
|
/// Creates a safe canonicalization error without copying source payload material.
|
||||||
pub(crate) use self::error::canonicalization_error;
|
pub(crate) use self::error::canonicalization_error;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-raw-transaction-lib/src/signature.rs
|
// file: crates/ksp-raw-transaction-lib/src/signature.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
use base64::Engine; // rust-rules: trait-import
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
|
||||||
@@ -68,7 +68,14 @@ pub fn extract_raw_transaction_signature_from_binary_base64(value: &str) -> ksp_
|
|||||||
if base64::engine::general_purpose::STANDARD.encode(decoded.as_slice()) != value {
|
if base64::engine::general_purpose::STANDARD.encode(decoded.as_slice()) != value {
|
||||||
return std::result::Result::Err(crate::signature_error());
|
return std::result::Result::Err(crate::signature_error());
|
||||||
}
|
}
|
||||||
let prefix = decode_signature_count(decoded.as_slice());
|
if decoded.first() == std::option::Option::Some(&0x81) {
|
||||||
|
return extract_v1_signature(decoded.as_slice());
|
||||||
|
}
|
||||||
|
return extract_legacy_or_v0_signature(decoded.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_legacy_or_v0_signature(decoded: &[u8]) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
let prefix = decode_signature_count(decoded);
|
||||||
let (signature_count, prefix_len) = match prefix {
|
let (signature_count, prefix_len) = match prefix {
|
||||||
std::result::Result::Ok(prefix) => prefix,
|
std::result::Result::Ok(prefix) => prefix,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
@@ -87,11 +94,151 @@ pub fn extract_raw_transaction_signature_from_binary_base64(value: &str) -> ksp_
|
|||||||
if message_offset >= decoded.len() {
|
if message_offset >= decoded.len() {
|
||||||
return std::result::Result::Err(crate::signature_error());
|
return std::result::Result::Err(crate::signature_error());
|
||||||
}
|
}
|
||||||
let first_end = match prefix_len.checked_add(64) {
|
return copy_signature(decoded, prefix_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_v1_signature(decoded: &[u8]) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
if decoded.len() > 4_096 || decoded.len() < 42 {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let required_signatures = match decoded.get(1) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let readonly_signed = match decoded.get(2) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let readonly_unsigned = match decoded.get(3) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if required_signatures == 0 || required_signatures > 12 || readonly_signed >= required_signatures {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let mask_bytes = match decoded.get(4..8) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mask = u32::from_le_bytes([mask_bytes[0], mask_bytes[1], mask_bytes[2], mask_bytes[3]]);
|
||||||
|
if mask & !0x1f != 0 || matches!(mask & 0b11, 1 | 2) {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let instruction_count = match decoded.get(40) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let address_count = match decoded.get(41) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if instruction_count > 64 || address_count > 64 || address_count < required_signatures + readonly_unsigned {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let addresses_len = match address_count.checked_mul(32) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mut offset = match 42_usize.checked_add(addresses_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let config_len = config_value_length(mask);
|
||||||
|
offset = match offset.checked_add(config_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let headers_len = match instruction_count.checked_mul(4) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let headers_end = match offset.checked_add(headers_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if headers_end > decoded.len() {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let mut payload_len = 0_usize;
|
||||||
|
for index in 0..instruction_count {
|
||||||
|
let header_offset = offset + (index * 4);
|
||||||
|
let program_index = usize::from(decoded[header_offset]);
|
||||||
|
let account_index_count = usize::from(decoded[header_offset + 1]);
|
||||||
|
let data_len = u16::from_le_bytes([decoded[header_offset + 2], decoded[header_offset + 3]]);
|
||||||
|
if program_index >= address_count {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
payload_len = match payload_len.checked_add(account_index_count).and_then(|value| return value.checked_add(usize::from(data_len))) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let payload_end = match headers_end.checked_add(payload_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let payload = match decoded.get(headers_end..payload_end) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mut payload_offset = 0_usize;
|
||||||
|
for index in 0..instruction_count {
|
||||||
|
let header_offset = offset + (index * 4);
|
||||||
|
let account_index_count = usize::from(decoded[header_offset + 1]);
|
||||||
|
let data_len = usize::from(u16::from_le_bytes([decoded[header_offset + 2], decoded[header_offset + 3]]));
|
||||||
|
let account_end = match payload_offset.checked_add(account_index_count) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let account_indexes = match payload.get(payload_offset..account_end) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if account_indexes.iter().any(|value| return usize::from(*value) >= address_count) {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
payload_offset = match account_end.checked_add(data_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let signatures_len = match required_signatures.checked_mul(64) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let expected_end = match payload_end.checked_add(signatures_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if expected_end != decoded.len() {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
return copy_signature(decoded, payload_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_value_length(mask: u32) -> usize {
|
||||||
|
let mut length = 0_usize;
|
||||||
|
if mask & 0b11 == 0b11 {
|
||||||
|
length += 8;
|
||||||
|
}
|
||||||
|
if mask & (1_u32 << 2) != 0 {
|
||||||
|
length += 4;
|
||||||
|
}
|
||||||
|
if mask & (1_u32 << 3) != 0 {
|
||||||
|
length += 4;
|
||||||
|
}
|
||||||
|
if mask & (1_u32 << 4) != 0 {
|
||||||
|
length += 4;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_signature(decoded: &[u8], offset: usize) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
let first_end = match offset.checked_add(64) {
|
||||||
std::option::Option::Some(end) => end,
|
std::option::Option::Some(end) => end,
|
||||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
};
|
};
|
||||||
let first = match decoded.get(prefix_len..first_end) {
|
let first = match decoded.get(offset..first_end) {
|
||||||
std::option::Option::Some(first) => first,
|
std::option::Option::Some(first) => first,
|
||||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
};
|
};
|
||||||
|
|||||||
545
crates/ksp-raw-transaction-lib/src/wire.rs
Normal file
545
crates/ksp-raw-transaction-lib/src/wire.rs
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/src/wire.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const MAX_LEGACY_SHORT_VECTOR_VALUE: usize = u16::MAX as usize;
|
||||||
|
const MAX_V1_ADDRESS_COUNT: usize = 64;
|
||||||
|
const MAX_V1_INSTRUCTION_COUNT: usize = 64;
|
||||||
|
const MAX_V1_SIGNATURE_COUNT: usize = 12;
|
||||||
|
const MAX_V1_TRANSACTION_BYTES: usize = 4_096;
|
||||||
|
|
||||||
|
/// Source-neutral Solana transaction message version used by the exact wire serializer.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RawSolanaMessageVersion {
|
||||||
|
/// Legacy Solana message without a version prefix.
|
||||||
|
Legacy,
|
||||||
|
/// Versioned transaction message v0, prefixed with `0x80`.
|
||||||
|
V0,
|
||||||
|
/// Transaction V1 / SIMD-0385 message, prefixed with `0x81`.
|
||||||
|
V1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exact three-byte Solana message header.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaMessageHeader {
|
||||||
|
num_required_signatures: u8,
|
||||||
|
num_readonly_signed_accounts: u8,
|
||||||
|
num_readonly_unsigned_accounts: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaMessageHeader {
|
||||||
|
/// Creates an exact Solana message header.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(num_required_signatures: u8, num_readonly_signed_accounts: u8, num_readonly_unsigned_accounts: u8) -> Self {
|
||||||
|
return Self { num_required_signatures, num_readonly_signed_accounts, num_readonly_unsigned_accounts };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the required signature count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn num_required_signatures(self) -> u8 {
|
||||||
|
return self.num_required_signatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the readonly signed-account count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn num_readonly_signed_accounts(self) -> u8 {
|
||||||
|
return self.num_readonly_signed_accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the readonly unsigned-account count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn num_readonly_unsigned_accounts(self) -> u8 {
|
||||||
|
return self.num_readonly_unsigned_accounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One source-neutral compiled Solana instruction.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaCompiledInstruction {
|
||||||
|
program_id_index: u8,
|
||||||
|
accounts: std::vec::Vec<u8>,
|
||||||
|
data: std::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaCompiledInstruction {
|
||||||
|
/// Creates one compiled instruction while preserving exact ordered account indexes and data bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(program_id_index: u8, accounts: std::vec::Vec<u8>, data: std::vec::Vec<u8>) -> Self {
|
||||||
|
return Self { program_id_index, accounts, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the program account index.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn program_id_index(&self) -> u8 {
|
||||||
|
return self.program_id_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered account indexes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accounts(&self) -> &[u8] {
|
||||||
|
return self.accounts.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns exact instruction data bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn data(&self) -> &[u8] {
|
||||||
|
return self.data.as_slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaCompiledInstruction {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("RawSolanaCompiledInstruction")
|
||||||
|
.field("program_id_index", &self.program_id_index)
|
||||||
|
.field("account_index_count", &self.accounts.len())
|
||||||
|
.field("data_length", &self.data.len())
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One source-neutral v0 address-table lookup.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaAddressTableLookup {
|
||||||
|
account_key: [u8; 32],
|
||||||
|
writable_indexes: std::vec::Vec<u8>,
|
||||||
|
readonly_indexes: std::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaAddressTableLookup {
|
||||||
|
/// Creates one exact v0 address-table lookup.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(account_key: [u8; 32], writable_indexes: std::vec::Vec<u8>, readonly_indexes: std::vec::Vec<u8>) -> Self {
|
||||||
|
return Self { account_key, writable_indexes, readonly_indexes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the lookup-table account key bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn account_key(&self) -> &[u8; 32] {
|
||||||
|
return &self.account_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered writable lookup indexes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn writable_indexes(&self) -> &[u8] {
|
||||||
|
return self.writable_indexes.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered readonly lookup indexes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn readonly_indexes(&self) -> &[u8] {
|
||||||
|
return self.readonly_indexes.as_slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaAddressTableLookup {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("RawSolanaAddressTableLookup")
|
||||||
|
.field("writable_index_count", &self.writable_indexes.len())
|
||||||
|
.field("readonly_index_count", &self.readonly_indexes.len())
|
||||||
|
.finish_non_exhaustive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional inline budget configuration carried by Solana Transaction V1.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaTransactionConfig {
|
||||||
|
priority_fee: std::option::Option<u64>,
|
||||||
|
compute_unit_limit: std::option::Option<u32>,
|
||||||
|
loaded_accounts_data_size_limit: std::option::Option<u32>,
|
||||||
|
heap_size: std::option::Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaTransactionConfig {
|
||||||
|
/// Creates an explicit Transaction V1 configuration, including the meaningful all-`None` case.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
|
priority_fee: std::option::Option<u64>,
|
||||||
|
compute_unit_limit: std::option::Option<u32>,
|
||||||
|
loaded_accounts_data_size_limit: std::option::Option<u32>,
|
||||||
|
heap_size: std::option::Option<u32>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { priority_fee, compute_unit_limit, loaded_accounts_data_size_limit, heap_size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional priority fee.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn priority_fee(self) -> std::option::Option<u64> {
|
||||||
|
return self.priority_fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional compute-unit limit.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn compute_unit_limit(self) -> std::option::Option<u32> {
|
||||||
|
return self.compute_unit_limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional loaded-account-data-size limit.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn loaded_accounts_data_size_limit(self) -> std::option::Option<u32> {
|
||||||
|
return self.loaded_accounts_data_size_limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional heap-size override.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn heap_size(self) -> std::option::Option<u32> {
|
||||||
|
return self.heap_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete source-neutral Solana transaction message required for exact wire serialization.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaTransactionMessage {
|
||||||
|
version: crate::RawSolanaMessageVersion,
|
||||||
|
header: crate::RawSolanaMessageHeader,
|
||||||
|
account_keys: std::vec::Vec<[u8; 32]>,
|
||||||
|
recent_blockhash: [u8; 32],
|
||||||
|
instructions: std::vec::Vec<crate::RawSolanaCompiledInstruction>,
|
||||||
|
address_table_lookups: std::vec::Vec<crate::RawSolanaAddressTableLookup>,
|
||||||
|
config: std::option::Option<crate::RawSolanaTransactionConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaTransactionMessage {
|
||||||
|
/// Creates one exact source-neutral message.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
version: crate::RawSolanaMessageVersion,
|
||||||
|
header: crate::RawSolanaMessageHeader,
|
||||||
|
account_keys: std::vec::Vec<[u8; 32]>,
|
||||||
|
recent_blockhash: [u8; 32],
|
||||||
|
instructions: std::vec::Vec<crate::RawSolanaCompiledInstruction>,
|
||||||
|
address_table_lookups: std::vec::Vec<crate::RawSolanaAddressTableLookup>,
|
||||||
|
config: std::option::Option<crate::RawSolanaTransactionConfig>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { version, header, account_keys, recent_blockhash, instructions, address_table_lookups, config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the message version.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn version(&self) -> crate::RawSolanaMessageVersion {
|
||||||
|
return self.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the message header.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn header(&self) -> crate::RawSolanaMessageHeader {
|
||||||
|
return self.header;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered static account key bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn account_keys(&self) -> &[[u8; 32]] {
|
||||||
|
return self.account_keys.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the recent blockhash bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn recent_blockhash(&self) -> &[u8; 32] {
|
||||||
|
return &self.recent_blockhash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered compiled instructions.
|
||||||
|
#[must_use]
|
||||||
|
pub fn instructions(&self) -> &[crate::RawSolanaCompiledInstruction] {
|
||||||
|
return self.instructions.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered v0 address-table lookups.
|
||||||
|
#[must_use]
|
||||||
|
pub fn address_table_lookups(&self) -> &[crate::RawSolanaAddressTableLookup] {
|
||||||
|
return self.address_table_lookups.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns optional V1 inline configuration.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn config(&self) -> std::option::Option<crate::RawSolanaTransactionConfig> {
|
||||||
|
return self.config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaTransactionMessage {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("RawSolanaTransactionMessage")
|
||||||
|
.field("version", &self.version)
|
||||||
|
.field("header", &self.header)
|
||||||
|
.field("account_key_count", &self.account_keys.len())
|
||||||
|
.field("instruction_count", &self.instructions.len())
|
||||||
|
.field("address_table_lookup_count", &self.address_table_lookups.len())
|
||||||
|
.field("has_config", &self.config.is_some())
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete source-neutral Solana transaction wire material.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaTransactionWire {
|
||||||
|
signatures: std::vec::Vec<[u8; 64]>,
|
||||||
|
message: crate::RawSolanaTransactionMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaTransactionWire {
|
||||||
|
/// Creates one complete transaction wire.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(signatures: std::vec::Vec<[u8; 64]>, message: crate::RawSolanaTransactionMessage) -> Self {
|
||||||
|
return Self { signatures, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered signature bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn signatures(&self) -> &[[u8; 64]] {
|
||||||
|
return self.signatures.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the exact source-neutral message.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn message(&self) -> &crate::RawSolanaTransactionMessage {
|
||||||
|
return &self.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaTransactionWire {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.debug_struct("RawSolanaTransactionWire").field("signature_count", &self.signatures.len()).field("message", &self.message).finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes one source-neutral Solana transaction to exact canonical wire bytes.
|
||||||
|
pub fn serialize_solana_transaction_wire(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||||
|
if let std::result::Result::Err(error) = validate_transaction(transaction) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let mut output = std::vec::Vec::new();
|
||||||
|
match transaction.message().version() {
|
||||||
|
crate::RawSolanaMessageVersion::Legacy | crate::RawSolanaMessageVersion::V0 => {
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(transaction.signatures().len(), &mut output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for signature in transaction.signatures() {
|
||||||
|
output.extend_from_slice(signature);
|
||||||
|
}
|
||||||
|
if transaction.message().version() == crate::RawSolanaMessageVersion::V0 {
|
||||||
|
output.push(0x80);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = encode_legacy_or_v0_message(transaction.message(), &mut output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crate::RawSolanaMessageVersion::V1 => {
|
||||||
|
if let std::result::Result::Err(error) = encode_v1_message(transaction, &mut output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if output.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire.bytes").with_context("actual_len", output.len().to_string()));
|
||||||
|
}
|
||||||
|
if transaction.message().version() == crate::RawSolanaMessageVersion::V1 && output.len() > MAX_V1_TRANSACTION_BYTES {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire.bytes").with_context("actual_len", output.len().to_string()));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes one source-neutral Solana transaction and returns canonical padded standard Base64.
|
||||||
|
pub fn serialize_solana_transaction_wire_base64(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<std::string::String> {
|
||||||
|
let bytes = match crate::serialize_solana_transaction_wire(transaction) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(base64::engine::general_purpose::STANDARD.encode(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_transaction(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<()> {
|
||||||
|
let message = transaction.message();
|
||||||
|
let required_signatures = usize::from(message.header().num_required_signatures());
|
||||||
|
if required_signatures == 0 || transaction.signatures().len() != required_signatures {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
if usize::from(message.header().num_readonly_signed_accounts()) >= required_signatures {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
if message.account_keys().len() < required_signatures + usize::from(message.header().num_readonly_unsigned_accounts()) {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
let loaded_count = message.address_table_lookups().iter().fold(0_usize, |count, lookup| {
|
||||||
|
return count.saturating_add(lookup.writable_indexes().len()).saturating_add(lookup.readonly_indexes().len());
|
||||||
|
});
|
||||||
|
let account_count = message.account_keys().len().saturating_add(loaded_count);
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
if usize::from(instruction.program_id_index()) >= account_count {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
for account_index in instruction.accounts() {
|
||||||
|
if usize::from(*account_index) >= account_count {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match message.version() {
|
||||||
|
crate::RawSolanaMessageVersion::Legacy => {
|
||||||
|
if !message.address_table_lookups().is_empty() || message.config().is_some() {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crate::RawSolanaMessageVersion::V0 => {
|
||||||
|
if message.config().is_some() {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crate::RawSolanaMessageVersion::V1 => {
|
||||||
|
if message.config().is_none() || !message.address_table_lookups().is_empty() {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
if transaction.signatures().len() > MAX_V1_SIGNATURE_COUNT
|
||||||
|
|| message.account_keys().len() > MAX_V1_ADDRESS_COUNT
|
||||||
|
|| message.instructions().len() > MAX_V1_INSTRUCTION_COUNT
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
for (index, account) in message.account_keys().iter().enumerate() {
|
||||||
|
if message.account_keys()[..index].contains(account) {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
if instruction.accounts().len() > usize::from(u8::MAX) || instruction.data().len() > usize::from(u16::MAX) {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(heap_size) = message.config().and_then(crate::RawSolanaTransactionConfig::heap_size) {
|
||||||
|
if !(32_768..=262_144).contains(&heap_size) || heap_size % 1_024 != 0 {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_legacy_or_v0_message(message: &crate::RawSolanaTransactionMessage, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||||
|
encode_header(message.header(), output);
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(message.account_keys().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for account_key in message.account_keys() {
|
||||||
|
output.extend_from_slice(account_key);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(message.recent_blockhash());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(message.instructions().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
output.push(instruction.program_id_index());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(instruction.accounts().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(instruction.accounts());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(instruction.data().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(instruction.data());
|
||||||
|
}
|
||||||
|
if message.version() == crate::RawSolanaMessageVersion::V0 {
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(message.address_table_lookups().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for lookup in message.address_table_lookups() {
|
||||||
|
output.extend_from_slice(lookup.account_key());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(lookup.writable_indexes().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(lookup.writable_indexes());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(lookup.readonly_indexes().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(lookup.readonly_indexes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_v1_message(transaction: &crate::RawSolanaTransactionWire, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||||
|
let message = transaction.message();
|
||||||
|
let config = match message.config() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::material_error("wire")),
|
||||||
|
};
|
||||||
|
output.push(0x81);
|
||||||
|
encode_header(message.header(), output);
|
||||||
|
let mut mask = 0_u32;
|
||||||
|
if config.priority_fee().is_some() {
|
||||||
|
mask |= 0b11;
|
||||||
|
}
|
||||||
|
if config.compute_unit_limit().is_some() {
|
||||||
|
mask |= 1_u32 << 2;
|
||||||
|
}
|
||||||
|
if config.loaded_accounts_data_size_limit().is_some() {
|
||||||
|
mask |= 1_u32 << 3;
|
||||||
|
}
|
||||||
|
if config.heap_size().is_some() {
|
||||||
|
mask |= 1_u32 << 4;
|
||||||
|
}
|
||||||
|
output.extend_from_slice(&mask.to_le_bytes());
|
||||||
|
output.extend_from_slice(message.recent_blockhash());
|
||||||
|
output.push(message.instructions().len() as u8);
|
||||||
|
output.push(message.account_keys().len() as u8);
|
||||||
|
for account_key in message.account_keys() {
|
||||||
|
output.extend_from_slice(account_key);
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.priority_fee() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.compute_unit_limit() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.loaded_accounts_data_size_limit() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.heap_size() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
output.push(instruction.program_id_index());
|
||||||
|
output.push(instruction.accounts().len() as u8);
|
||||||
|
output.extend_from_slice(&(instruction.data().len() as u16).to_le_bytes());
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
output.extend_from_slice(instruction.accounts());
|
||||||
|
output.extend_from_slice(instruction.data());
|
||||||
|
}
|
||||||
|
for signature in transaction.signatures() {
|
||||||
|
output.extend_from_slice(signature);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_header(header: crate::RawSolanaMessageHeader, output: &mut std::vec::Vec<u8>) {
|
||||||
|
output.push(header.num_required_signatures());
|
||||||
|
output.push(header.num_readonly_signed_accounts());
|
||||||
|
output.push(header.num_readonly_unsigned_accounts());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_short_vec(value: usize, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||||
|
if value > MAX_LEGACY_SHORT_VECTOR_VALUE {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
let mut remaining = value;
|
||||||
|
loop {
|
||||||
|
let mut byte = (remaining & 0x7f) as u8;
|
||||||
|
remaining >>= 7;
|
||||||
|
if remaining != 0 {
|
||||||
|
byte |= 0x80;
|
||||||
|
}
|
||||||
|
output.push(byte);
|
||||||
|
if remaining == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/wire.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Dependency-boundary canaries for the common RAW Transaction foundation.
|
//! Dependency-boundary canaries for the common RAW Transaction foundation.
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ fn manifest() -> std::string::String {
|
|||||||
fn production_sources() -> std::string::String {
|
fn production_sources() -> std::string::String {
|
||||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||||
let mut combined = std::string::String::new();
|
let mut combined = std::string::String::new();
|
||||||
for name in ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs"] {
|
for name in ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs", "wire.rs"] {
|
||||||
let source = std::fs::read_to_string(root.join(name)).unwrap_or_default();
|
let source = std::fs::read_to_string(root.join(name)).unwrap_or_default();
|
||||||
combined.push_str(source.as_str());
|
combined.push_str(source.as_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-raw-transaction-lib/tests/public_api.rs
|
// file: crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Integration canaries for the public common RAW Transaction crate-root surface.
|
//! Integration canaries for the public common RAW Transaction crate-root surface.
|
||||||
|
|
||||||
@@ -99,3 +99,24 @@ fn pre_004_embedded_signature_and_block_material_contract_are_available_from_cra
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_source_neutral_wire_contract_is_available_from_crate_root() {
|
||||||
|
let message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[1_u8; 32]],
|
||||||
|
[2_u8; 32],
|
||||||
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(0, vec![0], vec![3])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
let wire = ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[4_u8; 64]], message);
|
||||||
|
let encoded = ksp_raw_transaction_lib::serialize_solana_transaction_wire_base64(&wire);
|
||||||
|
assert!(encoded.is_ok());
|
||||||
|
if let std::result::Result::Ok(encoded) = encoded {
|
||||||
|
assert!(encoded.starts_with("gQ"));
|
||||||
|
assert!(ksp_raw_transaction_lib::extract_raw_transaction_signature_from_binary_base64(encoded.as_str()).is_ok());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
// file: crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
//! Release-completeness canaries for the common RAW Transaction foundation.
|
//! Release-completeness canaries for the common RAW Transaction foundation.
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ fn pre_002_production_module_inventory_is_exact() {
|
|||||||
names.push(name.to_owned());
|
names.push(name.to_owned());
|
||||||
}
|
}
|
||||||
names.sort();
|
names.sort();
|
||||||
let expected = ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs"];
|
let expected = ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs", "wire.rs"];
|
||||||
assert_eq!(names.len(), expected.len());
|
assert_eq!(names.len(), expected.len());
|
||||||
for (actual, expected_name) in names.iter().zip(expected.iter()) {
|
for (actual, expected_name) in names.iter().zip(expected.iter()) {
|
||||||
assert_eq!(actual.as_str(), *expected_name);
|
assert_eq!(actual.as_str(), *expected_name);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// file: crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
// file: crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_signature_parser_accepts_exact_sixty_four_zero_bytes() {
|
fn pre_002_signature_parser_accepts_exact_sixty_four_zero_bytes() {
|
||||||
@@ -60,3 +62,20 @@ fn pre_004_embedded_signature_rejects_invalid_base64_noncanonical_short_vec_and_
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_embedded_signature_extracts_terminal_v1_signature_and_rejects_trailing_bytes() {
|
||||||
|
let encoded = "gQEAAR8AAABRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUQECQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQgcAAAAAAAAACAAAAAkAAAAAgAAAAQEDAAABAgOqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
|
||||||
|
let signature = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||||
|
assert!(signature.is_ok());
|
||||||
|
if let std::result::Result::Ok(signature) = signature {
|
||||||
|
assert_eq!(signature.as_bytes(), &[0xaa_u8; 64]);
|
||||||
|
}
|
||||||
|
let decoded = base64::engine::general_purpose::STANDARD.decode(encoded);
|
||||||
|
assert!(decoded.is_ok());
|
||||||
|
if let std::result::Result::Ok(mut decoded) = decoded {
|
||||||
|
decoded.push(0);
|
||||||
|
let trailing = base64::engine::general_purpose::STANDARD.encode(decoded);
|
||||||
|
assert!(crate::extract_raw_transaction_signature_from_binary_base64(trailing.as_str()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
122
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
Normal file
122
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/unit_tests/wire.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
const LEGACY_GOLDEN_BASE64: &str = "AREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREBAAECISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIjExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExAQEBAAKquw==";
|
||||||
|
const V0_GOLDEN_BASE64: &str = "ARISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhKAAQAAASMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIBAAEBAcwBJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQBAAA=";
|
||||||
|
const V1_EMPTY_CONFIG_GOLDEN_BASE64: &str = "gQEAAAAAAABSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUgABQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0Orq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur";
|
||||||
|
const V1_GOLDEN_BASE64: &str = "gQEAAR8AAABRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUQECQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQgcAAAAAAAAACAAAAAkAAAAAgAAAAQEDAAABAgOqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
|
||||||
|
|
||||||
|
fn legacy_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::Legacy,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 1),
|
||||||
|
vec![[0x21_u8; 32], [0x22_u8; 32]],
|
||||||
|
[0x31_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(1, vec![0], vec![0xaa, 0xbb])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0x11_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v0_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V0,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[0x23_u8; 32]],
|
||||||
|
[0x32_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(0, vec![1], vec![0xcc])],
|
||||||
|
vec![crate::RawSolanaAddressTableLookup::new([0x24_u8; 32], vec![0], vec![])],
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0x12_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 1),
|
||||||
|
vec![[0x41_u8; 32], [0x42_u8; 32]],
|
||||||
|
[0x51_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(1, vec![0], vec![1, 2, 3])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::new(
|
||||||
|
std::option::Option::Some(7),
|
||||||
|
std::option::Option::Some(8),
|
||||||
|
std::option::Option::Some(9),
|
||||||
|
std::option::Option::Some(32_768),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0xaa_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1_empty_config_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[0x43_u8; 32]],
|
||||||
|
[0x52_u8; 32],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0xab_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_legacy_v0_and_v1_wire_goldens_are_exact() {
|
||||||
|
let legacy = crate::serialize_solana_transaction_wire_base64(&legacy_wire());
|
||||||
|
let v0 = crate::serialize_solana_transaction_wire_base64(&v0_wire());
|
||||||
|
let v1 = crate::serialize_solana_transaction_wire_base64(&v1_wire());
|
||||||
|
let v1_empty = crate::serialize_solana_transaction_wire_base64(&v1_empty_config_wire());
|
||||||
|
assert_eq!(legacy.as_deref(), std::result::Result::Ok(LEGACY_GOLDEN_BASE64));
|
||||||
|
assert_eq!(v0.as_deref(), std::result::Result::Ok(V0_GOLDEN_BASE64));
|
||||||
|
assert_eq!(v1.as_deref(), std::result::Result::Ok(V1_GOLDEN_BASE64));
|
||||||
|
assert_eq!(v1_empty.as_deref(), std::result::Result::Ok(V1_EMPTY_CONFIG_GOLDEN_BASE64));
|
||||||
|
let v1_bytes = crate::serialize_solana_transaction_wire(&v1_wire());
|
||||||
|
assert!(v1_bytes.is_ok());
|
||||||
|
if let std::result::Result::Ok(v1_bytes) = v1_bytes {
|
||||||
|
assert_eq!(v1_bytes.len(), 198);
|
||||||
|
assert_eq!(v1_bytes.first(), std::option::Option::Some(&0x81));
|
||||||
|
assert_eq!(v1_bytes.get(v1_bytes.len() - 64..), std::option::Option::Some([0xaa_u8; 64].as_slice()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_v1_structural_guards_reject_alt_duplicates_and_missing_config() {
|
||||||
|
let mut missing_config = v1_wire();
|
||||||
|
missing_config.message.config = std::option::Option::None;
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&missing_config).is_err());
|
||||||
|
let mut with_lookup = v1_wire();
|
||||||
|
with_lookup.message.address_table_lookups.push(crate::RawSolanaAddressTableLookup::new([9_u8; 32], vec![], vec![]));
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&with_lookup).is_err());
|
||||||
|
let mut duplicate = v1_wire();
|
||||||
|
duplicate.message.account_keys[1] = duplicate.message.account_keys[0];
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&duplicate).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_v1_program_index_zero_is_valid_and_bounds_remain_enforced() {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[1_u8; 32]],
|
||||||
|
[2_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(0, vec![0], vec![])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
let wire = crate::RawSolanaTransactionWire::new(vec![[3_u8; 64]], message);
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&wire).is_ok());
|
||||||
|
let too_many_addresses = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
(0_u8..65_u8).map(|value| return [value; 32]).collect(),
|
||||||
|
[2_u8; 32],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
let wire = crate::RawSolanaTransactionWire::new(vec![[3_u8; 64]], too_many_addresses);
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&wire).is_err());
|
||||||
|
}
|
||||||
206
deltas/0.3.10/pre.006.md
Normal file
206
deltas/0.3.10/pre.006.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.006` — Transaction V1 + parité Yellowstone transaction/block
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.3.10-pre.005-fix.002
|
||||||
|
workspace.package.version = 0.3.10-pre.5.fix.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur communiqué le 2026-09-06 ferme cette baseline : audits statiques propres, `cargo check --workspace` PASS, Clippy strict PASS, common RAW PASS, 387 tests Transport avec suites publiques/release/doctests PASS, et suite Backfill complète PASS, y compris hardening et les deux canaris WS/Helius.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Fermer avant le Worker concret :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transaction V1 / SIMD-0385 dans la common source-neutral
|
||||||
|
Yellowstone Transaction -> transaction wire exact
|
||||||
|
Yellowstone Block -> même transaction wire + block_time
|
||||||
|
qualification RAW-direct vs hydration
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun adapter productif `Transport DTO -> common` n'est ajouté. `TR-C2` reste Worker-owned et `pre.007` reste le premier point où la crate Worker concrète peut apparaître.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.6
|
||||||
|
Cargo.toml header = 499
|
||||||
|
workspace members = 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common wire Legacy/V0/V1
|
||||||
|
|
||||||
|
`ksp-raw-transaction-lib` ajoute un modèle source-neutral :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RawSolanaMessageVersion
|
||||||
|
RawSolanaMessageHeader
|
||||||
|
RawSolanaCompiledInstruction
|
||||||
|
RawSolanaAddressTableLookup
|
||||||
|
RawSolanaTransactionConfig
|
||||||
|
RawSolanaTransactionMessage
|
||||||
|
RawSolanaTransactionWire
|
||||||
|
```
|
||||||
|
|
||||||
|
et deux serializers :
|
||||||
|
|
||||||
|
```text
|
||||||
|
serialize_solana_transaction_wire
|
||||||
|
serialize_solana_transaction_wire_base64
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy et V0 conservent le layout signatures-first existant. V1 suit SIMD-0385 : `0x81`, header, config mask, lifetime, compteurs, adresses, valeurs config, headers/payloads d'instructions et signatures terminales.
|
||||||
|
|
||||||
|
Les gardes V1 verrouillent notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction <= 4096 bytes
|
||||||
|
1..=12 signatures
|
||||||
|
<= 64 adresses
|
||||||
|
<= 64 instructions
|
||||||
|
num_readonly_signed < num_required_signatures
|
||||||
|
indexes programme/comptes bornés
|
||||||
|
aucune Address Lookup Table
|
||||||
|
aucune adresse dupliquée
|
||||||
|
config présente, y compris vide
|
||||||
|
aucun trailing byte après les signatures
|
||||||
|
```
|
||||||
|
|
||||||
|
Les goldens couvrent Legacy, V0, V1 configuration complète et V1 configuration vide.
|
||||||
|
|
||||||
|
## Extraction de signature V1
|
||||||
|
|
||||||
|
`extract_raw_transaction_signature_from_binary_base64` distingue désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
premier octet == 0x81 -> parser V1 et signatures terminales
|
||||||
|
sinon -> parser short-vector signatures-first Legacy/V0
|
||||||
|
```
|
||||||
|
|
||||||
|
Le parser V1 parcourt structurellement mask/config, adresses, headers, payloads et signatures et exige la fin exacte du wire.
|
||||||
|
|
||||||
|
## Parité Yellowstone
|
||||||
|
|
||||||
|
`crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs` monte un serveur Geyser local déterministe, ouvre le vrai `YellowstoneGrpcChannel::open_standard_subscribe` puis reçoit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SubscribeUpdateTransaction V1
|
||||||
|
SubscribeUpdateBlock V1 portant la même transaction
|
||||||
|
```
|
||||||
|
|
||||||
|
La projection vers `RawSolanaTransactionWire` est strictement test-only. Le canari exige le même wire V1 exact pour les deux updates et vérifie slot, transaction index et `block_time` côté Block.
|
||||||
|
|
||||||
|
## Qualification RAW
|
||||||
|
|
||||||
|
Le gate ferme volontairement une qualification partielle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Yellowstone transaction wire = direct/fiable
|
||||||
|
Yellowstone Transaction RAW = hydration HTTP obligatoire : block_time absent
|
||||||
|
Yellowstone Block RAW complet= hydration HTTP obligatoire : meta JSON byte-identical non prouvée
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun champ manquant n'est inventé et aucune projection protobuf -> JSON ad hoc n'est introduite pour forcer un hash RAW identique.
|
||||||
|
|
||||||
|
## Dependency firewall
|
||||||
|
|
||||||
|
Le Backfill ajoute uniquement en dev :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||||
|
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`dependency_boundary.rs` verrouille leur absence des dépendances production et `hardening.rs` recalibre le set dev exact. Les dépendances normales restent inchangées.
|
||||||
|
|
||||||
|
Il n'existe toujours aucun edge productif :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-onchain-transport-lib -> ksp-raw-transaction-lib
|
||||||
|
ksp-raw-transaction-lib -> ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fraîcheur externe revalidée
|
||||||
|
|
||||||
|
Revalidation le 2026-09-07 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SIMD-0385 : Transaction V1, status Review, version byte 129, signatures terminales, sans ALT
|
||||||
|
Solana Transaction V1 examples : capacité Yellowstone avec Message.config ; activation cluster encore feature-gated/pre-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Sources :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md
|
||||||
|
https://github.com/solana-foundation/transaction-v1-examples
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/solana-storage.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP implémente la capacité de lecture/canonicalisation V1 sans déclarer son activation sur Mainnet ou un autre cluster.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/wire.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
|
||||||
|
deltas/0.3.10/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/lib.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/signature.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||||
|
docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges dev
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Les arbres Cargo sont requis car `pre.006` ajoute deux dev-dependencies test-only.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
common sérialise Legacy/V0/V1 sans dépendre de Transport
|
||||||
|
V1 est reconnu par 0x81 et possède des signatures terminales
|
||||||
|
Yellowstone Transaction/Block conservent un transaction wire V1 exact sur canari
|
||||||
|
Yellowstone RAW complet reste hydraté par HTTP tant que TR-C4 meta/block_time complet n'est pas prouvé
|
||||||
|
aucun RAW v2 automatique
|
||||||
|
aucun Worker concret avant pre.007
|
||||||
|
TR-C2 reste Worker-owned
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md -->
|
<!-- file: docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Acquisition et alimentation `RawTransaction`
|
# Acquisition et alimentation `RawTransaction`
|
||||||
|
|
||||||
@@ -665,6 +665,54 @@ aucun edge Transport <-> ksp-raw-transaction-lib
|
|||||||
|
|
||||||
La migration doit préserver exactement les golden bytes/hash RAW v1 déjà prouvés. Aucun RAW v2 n'est justifié.
|
La migration doit préserver exactement les golden bytes/hash RAW v1 déjà prouvés. Aucun RAW v2 n'est justifié.
|
||||||
|
|
||||||
|
### 13.1 Transaction wire Legacy/V0/V1
|
||||||
|
|
||||||
|
`pre.006` matérialise dans `ksp-raw-transaction-lib` un modèle wire Solana source-neutral, indépendant de Transport et des crates runtime Solana. Il sérialise exactement les formes Legacy, V0 et V1 puis expose aussi la représentation Base64.
|
||||||
|
|
||||||
|
Pour V1, le contrat suit SIMD-0385 tel qu'audité le 7 septembre 2026 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0x81
|
||||||
|
LegacyHeader
|
||||||
|
TransactionConfigMask u32 LE
|
||||||
|
LifetimeSpecifier [32]
|
||||||
|
NumInstructions u8
|
||||||
|
NumAddresses u8
|
||||||
|
Addresses
|
||||||
|
ConfigValues
|
||||||
|
InstructionHeaders
|
||||||
|
InstructionPayloads
|
||||||
|
Signatures terminales
|
||||||
|
```
|
||||||
|
|
||||||
|
Les limites common verrouillées sont : transaction `<= 4096` octets, `<= 12` signatures, `<= 64` adresses, `<= 64` instructions, indexes bornés, aucune ALT en V1 et aucune donnée après les signatures. La présence de `Message.config` Yellowstone distingue V1 ; `versioned=true` sans config reste V0, et `versioned=false` sans config reste Legacy.
|
||||||
|
|
||||||
|
L'extracteur de signature Base64 common reconnaît désormais V1 au premier octet `0x81` et parcourt structurellement le message jusqu'aux signatures terminales au lieu d'appliquer le short-vector signatures-first de Legacy/V0.
|
||||||
|
|
||||||
|
Le format V1 est encore documenté upstream comme pre-release/feature-gated ; KSP implémente donc la capacité de lecture/canonicalisation sans déclarer son activation sur un cluster particulier.
|
||||||
|
|
||||||
|
### 13.2 Qualification Yellowstone `pre.006`
|
||||||
|
|
||||||
|
Un canari cross-layer test-only dans Backfill monte un serveur Geyser local, ouvre le vrai `YellowstoneGrpcChannel::open_standard_subscribe`, reçoit successivement une update `Transaction` V1 et une update `Block` contenant la même transaction, puis projette les DTO Transport vers le modèle wire common uniquement dans le test.
|
||||||
|
|
||||||
|
Le gate prouve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transaction update -> slot/index + transaction wire V1 exact
|
||||||
|
Block update -> slot/index + même transaction wire V1 exact + block_time
|
||||||
|
Transport -> aucun edge production vers ksp-raw-transaction-lib
|
||||||
|
Backfill -> tonic/yellowstone-grpc-proto strictement dev-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Il ne qualifie pas un RAW-direct complet Yellowstone : une update `Transaction` ne porte pas `block_time`, et la représentation protobuf Yellowstone de la meta n'est pas démontrée byte-identical avec la projection JSON HTTP servant au RAW v1 actuel. La décision `TR-C4` reste donc conservative :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Yellowstone Transaction/Block -> signal structuré + transaction wire fidèle
|
||||||
|
RAW v1 complet -> hydration HTTP avant persistence
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun adapter productif n'est ajouté en `pre.006`. Conformément à `TR-C2`, l'adapter `Transport DTO -> common` demeure réservé au Worker de `pre.007`.
|
||||||
|
|
||||||
## 14. Handoff `0.3.10` : Worker live
|
## 14. Handoff `0.3.10` : Worker live
|
||||||
|
|
||||||
### 14.1 Contrat fonctionnel
|
### 14.1 Contrat fonctionnel
|
||||||
@@ -805,7 +853,7 @@ Aucun code, DTO, Config, endpoint, secret, dépendance ou convention de version
|
|||||||
|
|
||||||
## 18. Sources externes auditées
|
## 18. Sources externes auditées
|
||||||
|
|
||||||
Sources consultées le **4 septembre 2026**. Cette liste consolide les anciens audits A/B dans un seul registre documentaire.
|
Sources consultées le **4 septembre 2026** et revalidées pour Transaction V1 le **7 septembre 2026**. Cette liste consolide les anciens audits A/B dans un seul registre documentaire.
|
||||||
|
|
||||||
### 18.1 Solana standard
|
### 18.1 Solana standard
|
||||||
|
|
||||||
@@ -828,43 +876,46 @@ Sources consultées le **4 septembre 2026**. Cette liste consolide les anciens a
|
|||||||
|
|
||||||
### 18.2 Yellowstone et providers
|
### 18.2 Yellowstone et providers
|
||||||
|
|
||||||
| Source | URL |
|
| Source | URL |
|
||||||
|----------------------------------|---------------------------------------------------------------------------------------------------|
|
|----------------------------------|--------------------------------------------------------------------------------------------------------------|
|
||||||
| Yellowstone proto | https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto |
|
| Yellowstone proto | https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto |
|
||||||
| Yellowstone README | https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md |
|
| Yellowstone storage proto | https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/solana-storage.proto |
|
||||||
| Yellowstone changelog | https://github.com/rpcpool/yellowstone-grpc/blob/master/CHANGELOG.md |
|
| SIMD-0385 Transaction V1 | https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md |
|
||||||
| Helius data streaming | https://www.helius.dev/docs/data-streaming |
|
| Transaction V1 examples | https://github.com/solana-foundation/transaction-v1-examples |
|
||||||
| Helius LaserStream | https://www.helius.dev/blog/introducing-laserstream |
|
| Yellowstone README | https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md |
|
||||||
| Helius WebSockets | https://www.helius.dev/blog/laserstream-websockets |
|
| Yellowstone changelog | https://github.com/rpcpool/yellowstone-grpc/blob/master/CHANGELOG.md |
|
||||||
| Helius enhanced WebSockets | https://www.helius.dev/blog/introducing-next-generation-enhanced-websockets |
|
| Helius data streaming | https://www.helius.dev/docs/data-streaming |
|
||||||
| Helius getTransactionsForAddress | https://www.helius.dev/blog/introducing-gettransactionsforaddress |
|
| Helius LaserStream | https://www.helius.dev/blog/introducing-laserstream |
|
||||||
| Helius preprocessed transactions | https://www.helius.dev/docs/shred-delivery/preprocessed-transactions |
|
| Helius WebSockets | https://www.helius.dev/blog/laserstream-websockets |
|
||||||
| PublicNode Solana | https://solana.publicnode.com/ |
|
| Helius enhanced WebSockets | https://www.helius.dev/blog/introducing-next-generation-enhanced-websockets |
|
||||||
| OrbitFlare Yellowstone | https://docs.orbitflare.com/data-streaming/yellowstone |
|
| Helius getTransactionsForAddress | https://www.helius.dev/blog/introducing-gettransactionsforaddress |
|
||||||
| OrbitFlare RPC | https://orbitflare.com/products/rpc-nodes |
|
| Helius preprocessed transactions | https://www.helius.dev/docs/shred-delivery/preprocessed-transactions |
|
||||||
| OrbitFlare gRPC | https://orbitflare.com/products/solana-grpc |
|
| PublicNode Solana | https://solana.publicnode.com/ |
|
||||||
| QuickNode Yellowstone | https://www.quicknode.com/guides/solana-development/tooling/solana-grpc/solana-grpc |
|
| OrbitFlare Yellowstone | https://docs.orbitflare.com/data-streaming/yellowstone |
|
||||||
| QuickNode gRPC plans | https://www.quicknode.com/blog/solana-grpc-is-now-included-with-scale-and-business-plans |
|
| OrbitFlare RPC | https://orbitflare.com/products/rpc-nodes |
|
||||||
| Alchemy Solana gRPC | https://www.alchemy.com/solana-grpc |
|
| OrbitFlare gRPC | https://orbitflare.com/products/solana-grpc |
|
||||||
| Alchemy Yellowstone quickstart | https://www.alchemy.com/docs/reference/yellowstone-grpc-quickstart |
|
| QuickNode Yellowstone | https://www.quicknode.com/guides/solana-development/tooling/solana-grpc/solana-grpc |
|
||||||
| Alchemy historical replay | https://www.alchemy.com/docs/reference/yellowstone-grpc-historical-replay |
|
| QuickNode gRPC plans | https://www.quicknode.com/blog/solana-grpc-is-now-included-with-scale-and-business-plans |
|
||||||
| Alchemy compute/bandwidth | https://www.alchemy.com/docs/reference/compute-unit-costs |
|
| Alchemy Solana gRPC | https://www.alchemy.com/solana-grpc |
|
||||||
| Chainstack Yellowstone | https://chainstack.com/yellowstone-grpc-more-streams-same-price/ |
|
| Alchemy Yellowstone quickstart | https://www.alchemy.com/docs/reference/yellowstone-grpc-quickstart |
|
||||||
| Shyft Yellowstone | https://shyft.to/solana-yellowstone-grpc |
|
| Alchemy historical replay | https://www.alchemy.com/docs/reference/yellowstone-grpc-historical-replay |
|
||||||
| Shyft pricing | https://shyft.to/solana-rpc-grpc-pricing |
|
| Alchemy compute/bandwidth | https://www.alchemy.com/docs/reference/compute-unit-costs |
|
||||||
| Shyft RabbitStream | https://shyft.to/solana-shreds-rabbitstream |
|
| Chainstack Yellowstone | https://chainstack.com/yellowstone-grpc-more-streams-same-price/ |
|
||||||
| Triton pricing | https://triton.one/pricing |
|
| Shyft Yellowstone | https://shyft.to/solana-yellowstone-grpc |
|
||||||
| Triton Yellowstone | https://blog.triton.one/complete-guide-to-solana-streaming-and-yellowstone-grpc/ |
|
| Shyft pricing | https://shyft.to/solana-rpc-grpc-pricing |
|
||||||
| Triton Fumarole | https://blog.triton.one/introducing-yellowstone-fumarole/ |
|
| Shyft RabbitStream | https://shyft.to/solana-shreds-rabbitstream |
|
||||||
| Triton Deshred | https://blog.triton.one/deshred-transactions-the-fastest-path-to-solana-data/ |
|
| Triton pricing | https://triton.one/pricing |
|
||||||
| dRPC Yellowstone | https://drpc.org/docs/solana-yellowstone-geyser-grpc |
|
| Triton Yellowstone | https://blog.triton.one/complete-guide-to-solana-streaming-and-yellowstone-grpc/ |
|
||||||
| Ankr Solana | https://www.ankr.com/docs/rpc-service/chains/chains-list/s-t/ |
|
| Triton Fumarole | https://blog.triton.one/introducing-yellowstone-fumarole/ |
|
||||||
| Ankr pricing | https://www.ankr.com/docs/rpc-service/pricing/ |
|
| Triton Deshred | https://blog.triton.one/deshred-transactions-the-fastest-path-to-solana-data/ |
|
||||||
| GetBlock pricing | https://getblock.io/pricing-new/ |
|
| dRPC Yellowstone | https://drpc.org/docs/solana-yellowstone-geyser-grpc |
|
||||||
| bloXroute pricing | https://bloxroute.com/pricing/ |
|
| Ankr Solana | https://www.ankr.com/docs/rpc-service/chains/chains-list/s-t/ |
|
||||||
| Jito ShredStream | https://docs.jito.wtf/lowlatencytxnfeed/ |
|
| Ankr pricing | https://www.ankr.com/docs/rpc-service/pricing/ |
|
||||||
| DoubleZero Edge | https://docs.malbeclabs.com/Edge%20Subscriber%20Connection/ |
|
| GetBlock pricing | https://getblock.io/pricing-new/ |
|
||||||
| Old Faithful | https://github.com/rpcpool/yellowstone-faithful |
|
| bloXroute pricing | https://bloxroute.com/pricing/ |
|
||||||
|
| Jito ShredStream | https://docs.jito.wtf/lowlatencytxnfeed/ |
|
||||||
|
| DoubleZero Edge | https://docs.malbeclabs.com/Edge%20Subscriber%20Connection/ |
|
||||||
|
| Old Faithful | https://github.com/rpcpool/yellowstone-faithful |
|
||||||
|
|
||||||
Les URLs sont documentaires. Aucun endpoint runtime, token ou secret de compte n'est ajouté au dépôt par cet audit.
|
Les URLs sont documentaires. Aucun endpoint runtime, token ou secret de compte n'est ajouté au dépôt par cet audit.
|
||||||
|
|
||||||
@@ -883,6 +934,9 @@ support vs preuve = axes séparés ; NON PROUVÉ n'implique pas REJET
|
|||||||
provider model = capabilities/configuration, jamais enum provider fermé
|
provider model = capabilities/configuration, jamais enum provider fermé
|
||||||
network identity = mainnet canonique ; mainnet-beta alias legacy/externe
|
network identity = mainnet canonique ; mainnet-beta alias legacy/externe
|
||||||
RAW canonicalisation = lower-layer commune, sans edge Job <-> Worker
|
RAW canonicalisation = lower-layer commune, sans edge Job <-> Worker
|
||||||
|
Transaction V1 = wire source-neutral Legacy/V0/V1 dans common ; activation cluster non supposée
|
||||||
|
Yellowstone pre.006 = transaction wire V1 qualifié ; RAW complet via hydration HTTP
|
||||||
|
TR-C2 = adapter productif Transport DTO -> common réservé au Worker pre.007
|
||||||
0.3.10 = Worker live multi-source + adaptations communes nécessaires
|
0.3.10 = Worker live multi-source + adaptations communes nécessaires
|
||||||
0.3.12 = Job Backfill multi-stratégie historique
|
0.3.12 = Job Backfill multi-stratégie historique
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md -->
|
<!-- file: docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md -->
|
||||||
<!-- version: 9 -->
|
<!-- version: 10 -->
|
||||||
|
|
||||||
# Plan v0.3.10 — RAW Transaction commune + Worker d’ingestion multi-source
|
# Plan v0.3.10 — RAW Transaction commune + Worker d’ingestion multi-source
|
||||||
|
|
||||||
@@ -1080,7 +1080,19 @@ Le second rejeu opérateur de `pre.005-fix.001` révèle ensuite deux gardes de
|
|||||||
|
|
||||||
### `pre.006` — parité Yellowstone transactions/blocks
|
### `pre.006` — parité Yellowstone transactions/blocks
|
||||||
|
|
||||||
Fermer les adapters structurés Yellowstone transaction/block, Transaction V1, meta et cross-source parity. Aucune persistance directe si le gate byte-identical échoue.
|
**Statut : réalisé, en attente du gate opérateur Rust.**
|
||||||
|
|
||||||
|
La common RAW gagne un modèle transaction wire source-neutral Legacy/V0/V1 et des serializers exacts bytes/Base64. Les goldens couvrent Legacy, V0, V1 avec configuration complète et V1 avec configuration présente mais vide. L'extracteur de première signature Base64 traite désormais le layout V1 `0x81` à signatures terminales et conserve le chemin signatures-first Legacy/V0.
|
||||||
|
|
||||||
|
Le modèle V1 verrouille les contraintes SIMD-0385 utiles à l'ingestion : 4096 octets maximum, 12 signatures, 64 adresses, 64 instructions, indexes bornés, aucune ALT, configuration inline et absence de trailing bytes après les signatures. La common reste Transport/Config/Job/Worker/runtime-free.
|
||||||
|
|
||||||
|
Un canari `yellowstone_raw_parity.rs` monte un Geyser Tonic local et utilise le vrai chemin public Transport `YellowstoneGrpcChannel -> open_standard_subscribe -> YellowstoneSubscribeUpdate`. Une `Transaction` V1 puis un `Block` V1 portant la même transaction sont projetés test-only vers la common et doivent sérialiser exactement le même wire V1. Le canari vérifie aussi slot, transaction index et `block_time` côté Block.
|
||||||
|
|
||||||
|
La qualification RAW-direct complète est refusée : l'update Transaction n'a pas `block_time`, et la meta Yellowstone protobuf n'est pas prouvée byte-identical avec la meta JSON HTTP du RAW v1 actuel. Le futur Worker doit donc utiliser Yellowstone comme signal live/transaction-wire riche puis hydrater par HTTP avant persistence RAW, tant qu'un gate ultérieur ne prouve pas `TR-C4`.
|
||||||
|
|
||||||
|
`tonic` et `yellowstone-grpc-proto` sont ajoutés uniquement en `dev-dependencies` du Backfill pour le serveur déterministe ; les canaris dependency/hardening verrouillent leur absence du graphe productif. Aucun adapter productif n'est placé en Backfill ou Transport : `TR-C2` reste réservé au Worker `pre.007`.
|
||||||
|
|
||||||
|
La fraicheur upstream a été revalidée le 7 septembre 2026 : SIMD-0385 décrit encore V1 comme `Review`/pre-release et les exemples Solana signalent une activation cluster feature-gated. KSP implémente donc le décodage/canonicalisation sans supposer l'activation Mainnet.
|
||||||
|
|
||||||
### `pre.007` — Worker foundation/runtime
|
### `pre.007` — Worker foundation/runtime
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md -->
|
<!-- file: docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md -->
|
||||||
<!-- version: 9 -->
|
<!-- version: 10 -->
|
||||||
|
|
||||||
# Validation v0.3.10 — RAW Transaction commune + Worker d’ingestion
|
# Validation v0.3.10 — RAW Transaction commune + Worker d’ingestion
|
||||||
|
|
||||||
@@ -942,3 +942,50 @@ KSP workspace Rust rule audit: clean
|
|||||||
Markdown table audit: clean (339 table(s), 761 file(s))
|
Markdown table audit: clean (339 table(s), 761 file(s))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 13.10 Gate d'assemblage `pre.006` — Transaction V1 et Yellowstone
|
||||||
|
|
||||||
|
Le rejeu opérateur de `pre.005-fix.002` communiqué le 6 septembre 2026 est intégralement vert : audits statiques, `cargo check --workspace`, Clippy strict, common RAW, 387 tests Transport avec suites publiques/release/doctests, et suite Backfill complète incluant hardening et parité WS/Helius. `pre.006` part donc de cette baseline fermée.
|
||||||
|
|
||||||
|
La tranche ajoute dans `ksp-raw-transaction-lib` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RawSolanaMessageVersion Legacy/V0/V1
|
||||||
|
RawSolanaMessageHeader
|
||||||
|
RawSolanaCompiledInstruction
|
||||||
|
RawSolanaAddressTableLookup
|
||||||
|
RawSolanaTransactionConfig
|
||||||
|
RawSolanaTransactionMessage
|
||||||
|
RawSolanaTransactionWire
|
||||||
|
serialize_solana_transaction_wire
|
||||||
|
serialize_solana_transaction_wire_base64
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests common imposent des goldens Base64 exacts Legacy/V0/V1, la configuration V1 vide significative, les limites structurelles V1, l'interdiction des ALT et l'extraction de signature depuis les signatures terminales.
|
||||||
|
|
||||||
|
Le canari Backfill `tests/yellowstone_raw_parity.rs` utilise un serveur Geyser local et le vrai canal public Yellowstone Transport. Il exige :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transaction V1 : slot = 42, index = 3, wire = golden common
|
||||||
|
Block V1 : slot = 42, index = 3, block_time présent, wire identique
|
||||||
|
meta : conservée comme DTO Yellowstone, sans prétendre à l'identité JSON HTTP
|
||||||
|
```
|
||||||
|
|
||||||
|
Décision de qualification :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Yellowstone transaction wire = QUALIFIÉ
|
||||||
|
Yellowstone Transaction RAW = HYDRATION HTTP (block_time absent)
|
||||||
|
Yellowstone Block RAW complet = HYDRATION HTTP (meta JSON byte-identical non prouvée)
|
||||||
|
TR-C2 adapter productif = différé au Worker pre.007
|
||||||
|
```
|
||||||
|
|
||||||
|
Le manifest Backfill ajoute uniquement en dev :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||||
|
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`tests/dependency_boundary.rs` vérifie qu'elles ne figurent pas avant `[dev-dependencies]`, et le canari hardening attend exactement `{tokio, tokio-tungstenite, tonic, yellowstone-grpc-proto}` en dev tout en gardant l'inventaire normal inchangé.
|
||||||
|
|
||||||
|
L'environnement d'assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Le gate Cargo/Clippy/tests reste donc opérateur. Les audits Python KSP et Markdown sont exécutés après fermeture du delta et leurs résultats sont enregistrés dans `deltas/0.3.10/pre.006.md`.
|
||||||
|
|||||||
Reference in New Issue
Block a user