v0.3.10-pre.006

This commit is contained in:
2026-09-07 18:58:48 +02:00
parent d4f4237723
commit f7c57f21c5
17 changed files with 1748 additions and 61 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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 = [

View File

@@ -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"));

View 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;
}

View File

@@ -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;

View File

@@ -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()),
}; };

View 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;

View File

@@ -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());
} }

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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());
}
}

View 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
View 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
```

View File

@@ -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
``` ```

View File

@@ -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 dingestion multi-source # Plan v0.3.10 — RAW Transaction commune + Worker dingestion 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

View File

@@ -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 dingestion # Validation v0.3.10 — RAW Transaction commune + Worker dingestion
@@ -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`.