v0.3.10-pre.004

This commit is contained in:
2026-09-06 17:15:51 +02:00
parent 8309d829cc
commit e19a202565
20 changed files with 949 additions and 56 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 493
# version: 494
[workspace]
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"]
[workspace.package]
version = "0.3.10-pre.3"
version = "0.3.10-pre.4"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -0,0 +1,141 @@
// file: crates/ksp-job-backfill-lib/tests/http_block_material.rs
// version: 1
//! Cross-layer canary for the future Worker-owned HTTP block adapter without changing production ownership.
fn pool_for_url(url: &str) -> ksp_onchain_transport_lib::HttpTransportPool {
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
ksp_onchain_transport_lib::HttpRoleName::new("default"),
true,
std::vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
10,
ksp_onchain_transport_lib::HttpRoleLimits::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
),
);
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
"http-block-fixture",
true,
ksp_onchain_transport_lib::HttpProviderName::new("fixture-provider"),
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
ksp_onchain_transport_lib::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
std::option::Option::Some(1),
std::vec![role],
);
return ksp_onchain_transport_lib::HttpTransportPool::new(ksp_onchain_transport_lib::HttpTransportSettings::new(
std::vec![endpoint],
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
))
.expect("fixture pool must build");
}
fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle<()>) {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
let address = listener.local_addr().expect("fixture listener address must resolve");
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("fixture server must accept one request");
let mut bytes = std::vec::Vec::new();
let mut buffer = [0_u8; 1024];
loop {
let count = std::io::Read::read(&mut stream, &mut buffer).expect("fixture request must read");
if count == 0 {
break;
}
bytes.extend_from_slice(&buffer[..count]);
if bytes.windows(4).any(|window| return window == b"\r\n\r\n") {
break;
}
}
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
return;
});
return (format!("http://{address}"), handle);
}
fn map_meta(field: &ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>) -> ksp_raw_transaction_lib::RawTransactionWireField<serde_json::Value> {
return match field {
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
};
}
fn map_version(
field: &ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
) -> ksp_raw_transaction_lib::RawTransactionWireField<ksp_raw_transaction_lib::RawTransactionVersion> {
return match field {
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy) => {
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy)
},
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number)) => {
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Number(*number))
},
};
}
#[tokio::test(flavor = "current_thread")]
async fn pre_004_http_observed_block_projects_each_base64_transaction_to_common_material_without_new_production_edge() {
const BODY: &str = concat!(
"{\"jsonrpc\":\"2.0\",\"result\":{\"previousBlockhash\":\"previous\",\"blockhash\":\"block\",\"parentSlot\":430000122,",
"\"rewards\":[],\"numRewardPartitions\":0,\"blockTime\":1787072400,\"blockHeight\":410000000,",
"\"transactions\":[{\"transaction\":[\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"AAAAAA\",\"base64\"],\"meta\":{\"err\":null,\"fee\":5000},\"version\":\"legacy\"},",
"{\"transaction\":[\"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEA\",",
"\"base64\"],\"meta\":{\"err\":null,\"fee\":6000},\"version\":0}]},\"id\":1}",
);
let (url, handle) = serve_once(BODY);
let pool = pool_for_url(url.as_str());
let config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
std::option::Option::Some(0),
std::option::Option::Some(false),
);
let observed = pool
.get_block_observed(&ksp_onchain_transport_lib::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config))
.await
.expect("observed block fixture must succeed");
assert_eq!(observed.endpoint_name(), "http-block-fixture");
assert_eq!(observed.provider().as_str(), "fixture-provider");
let block = observed.value().as_ref().expect("fixture block must be present");
let transactions = block.transactions().value().expect("fixture transactions must be present");
assert_eq!(transactions.len(), 2);
let network = ksp_store_lib::RawNetworkId::new("devnet").expect("fixture network must be valid");
let expected_signatures = [[0_u8; 64], [1_u8; 64]];
for (index, transaction) in transactions.iter().enumerate() {
let (data, meta, version) = match transaction.transaction() {
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary {
data,
encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64,
} => (data.as_str(), map_meta(transaction.meta()), map_version(transaction.version())),
_ => panic!("fixture must remain full Base64 transaction material"),
};
let transaction_index = u32::try_from(index).expect("fixture transaction index must fit u32");
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
network.clone(),
430_000_123,
block.block_time(),
data,
meta,
version,
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
)
.expect("HTTP block material must map to common RAW material");
let raw = ksp_raw_transaction_lib::canonicalize_raw_transaction(material).expect("mapped HTTP block material must canonicalize");
assert_eq!(raw.reference().signature().as_bytes(), &expected_signatures[index]);
assert_eq!(raw.slot(), 430_000_123);
assert_eq!(raw.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_072_400_000));
assert!(raw.payload().bytes().ends_with(format!("\"transactionIndex\":{transaction_index}}}").as_bytes()));
}
handle.join().expect("fixture server must join");
return;
}

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
<!-- version: 24 -->
<!-- version: 25 -->
# Utilisation de `ksp-onchain-transport-lib`
@@ -449,7 +449,9 @@ let inflation_rate = pool.get_inflation_rate(&role).await;
let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await;
```
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Pour les consumers qui doivent conserver la provenance exacte dun pool multi-endpoint, `get_block_observed(...)` exécute la même forme moderne et retourne la valeur typée avec le nom sûr de lendpoint et le provider réellement gagnants après sélection/retry/reroute. Cette projection nexpose ni URL, ni headers, ni body HTTP brut. Une réponse RPC `null` reste `None` dans la valeur observée.
Lorsque `transactionDetails = full` et `encoding = base64`, chaque `SolanaBlockTransaction` conserve le transaction wire Base64, `meta` et `version`. Le consumer peut alors projeter ces DTOs vers sa couche métier sans faire dépendre Transport dune canonicalisation RAW particulière. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
## 6. Exécution JSON-RPC standard générique

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[],"numRewardPartitions":0,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","base64"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
// version: 7
// version: 8
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
@@ -591,25 +591,42 @@ impl crate::HttpTransportPool {
slot: u64,
config: std::option::Option<&crate::SolanaGetBlockConfig>,
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
if let std::option::Option::Some(config) = config
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
{
return std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
"getBlock commitment must be confirmed or finalized when explicitly provided",
)
.with_context("rpc_method", "getBlock")
.with_context("commitment", "processed"),
);
}
let mut params = std::vec![serde_json::json!(slot)];
if let std::option::Option::Some(config) = config {
params.push((*config).to_json_value());
}
let params = get_block_params(slot, config);
let params = match params {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return self.execute_get_block(role, params).await;
}
/// Executes the current object-form `getBlock` request and reports the safe identity of the endpoint that produced the successful response.
///
/// Routing, admission, timeout and retry behavior are identical to [`Self::get_block`]. The returned observation never contains an endpoint URL,
/// HTTP headers or a raw HTTP body.
pub async fn get_block_observed(
&self,
role: &crate::HttpRoleName,
slot: u64,
config: std::option::Option<&crate::SolanaGetBlockConfig>,
) -> ksp_core_lib::Result<crate::HttpObservedValue<std::option::Option<crate::SolanaConfirmedBlock>>> {
let params = get_block_params(slot, config);
let params = match params {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let observed = self.execute_blocks_rpc_observed("getBlock", role, params).await;
let observed = match observed {
std::result::Result::Ok(observed) => observed,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (value, endpoint_name, provider) = observed.into_parts();
let block = decode_get_block(value);
return match block {
std::result::Result::Ok(block) => std::result::Result::Ok(crate::HttpObservedValue::new(block, endpoint_name, provider)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Executes the deprecated bare-encoding `getBlock` request form retained by Solana RPC for backwards compatibility.
#[deprecated(note = "use HttpTransportPool::get_block with SolanaGetBlockConfig; the bare encoding request form is deprecated")]
pub async fn get_block_legacy(
@@ -635,16 +652,8 @@ impl crate::HttpTransportPool {
params: std::vec::Vec<serde_json::Value>,
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
let value = self.execute_blocks_rpc("getBlock", role, params).await;
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if value.is_null() {
return std::result::Result::Ok(std::option::Option::None);
}
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value);
return match block {
std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)),
return match value {
std::result::Result::Ok(value) => decode_get_block(value),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
@@ -829,6 +838,48 @@ impl crate::HttpTransportPool {
};
return self.execute_standard_rpc(role, method, params).await;
}
async fn execute_blocks_rpc_observed(
&self,
method_name: &'static str,
role: &crate::HttpRoleName,
params: std::vec::Vec<serde_json::Value>,
) -> ksp_core_lib::Result<crate::HttpObservedValue<serde_json::Value>> {
let method = blocks_descriptor(method_name);
let method = match method {
std::result::Result::Ok(method) => method,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return self.execute_standard_rpc_observed(role, method, params).await;
}
}
fn decode_get_block(value: serde_json::Value) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
if value.is_null() {
return std::result::Result::Ok(std::option::Option::None);
}
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value);
return match block {
std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn get_block_params(slot: u64, config: std::option::Option<&crate::SolanaGetBlockConfig>) -> ksp_core_lib::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = config
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
{
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlock commitment must be confirmed or finalized when explicitly provided")
.with_context("rpc_method", "getBlock")
.with_context("commitment", "processed"),
);
}
let mut params = std::vec![serde_json::json!(slot)];
if let std::option::Option::Some(config) = config {
params.push((*config).to_json_value());
}
return std::result::Result::Ok(params);
}
#[derive(serde::Deserialize)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 49
// version: 50
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -1021,3 +1021,10 @@ fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
}
#[test]
fn public_v0_3_10_pre_004_observed_get_block_surface_is_available_from_crate_root() {
let method = ksp_onchain_transport_lib::HttpTransportPool::get_block_observed;
let _ = method;
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
// version: 6
// version: 7
#[test]
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
@@ -188,6 +188,35 @@ fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHand
return (format!("http://{address}"), handle);
}
fn serve_status_and_count(status_line: &'static str) -> (std::string::String, std::thread::JoinHandle<(usize, std::string::String)>) {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
let address = listener.local_addr().expect("fixture listener address must resolve");
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("fixture server must accept first request");
let first_request = read_request(&mut stream);
let response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
let mut count = 1_usize;
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(120);
while std::time::Instant::now() < deadline {
match listener.accept() {
std::result::Result::Ok((mut retry_stream, _)) => {
let _ = read_request(&mut retry_stream);
std::io::Write::write_all(&mut retry_stream, response.as_bytes()).expect("fixture retry response must write");
count = count.saturating_add(1);
},
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(5));
},
std::result::Result::Err(error) => panic!("fixture listener failed while counting retries: {error}"),
}
}
return (count, first_request);
});
return (format!("http://{address}"), handle);
}
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
let mut bytes = std::vec::Vec::new();
let mut buffer = [0_u8; 1024];
@@ -784,3 +813,92 @@ async fn typed_get_block_rejects_processed_commitment_before_io() {
assert_eq!(error.context()[1].key(), "commitment");
assert_eq!(error.context()[1].value(), "processed");
}
#[tokio::test(flavor = "current_thread")]
async fn pre_004_get_block_observed_reports_actual_winner_after_retry_reroute() {
let (first_url, first_handle) = serve_status_and_count("429 Too Many Requests");
let (winner_url, winner_handle) = serve_once(include_str!("../fixtures/http/get_block.observed_material.success.json"));
let urls = [(first_url.as_str(), "first-endpoint", "first-provider"), (winner_url.as_str(), "winner-endpoint", "winner-provider")];
let mut endpoints = std::vec::Vec::with_capacity(urls.len());
for (url, endpoint_name, provider) in urls {
let role = crate::HttpEndpointRoleSettings::new(
crate::HttpRoleName::new("default"),
true,
std::vec![crate::HttpRequestKind::wildcard()],
10,
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
);
endpoints.push(crate::HttpEndpointSettings::new(
endpoint_name,
true,
crate::HttpProviderName::new(provider),
crate::HttpClusterName::new("local"),
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
std::time::Duration::from_millis(100),
std::time::Duration::from_secs(1),
std::option::Option::Some(1),
std::vec![role],
));
}
let pool = crate::HttpTransportPool::new(crate::HttpTransportSettings::new(
endpoints,
crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
))
.expect("observed fixture pool must build");
let config = crate::SolanaGetBlockConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
std::option::Option::Some(0),
std::option::Option::Some(false),
);
let observed = pool
.get_block_observed(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config))
.await
.expect("retry-safe observed getBlock must succeed on the second endpoint");
assert_eq!(observed.endpoint_name(), "winner-endpoint");
assert_eq!(observed.provider().as_str(), "winner-provider");
let block = observed.value().as_ref().expect("winning response must contain a block");
let transactions = block.transactions().value().expect("winning block must contain transactions");
assert_eq!(transactions.len(), 1);
assert!(matches!(
transactions[0].transaction(),
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. }
));
let rendered = format!("{observed:?}");
assert!(rendered.contains("winner-endpoint"));
assert!(rendered.contains("winner-provider"));
assert!(rendered.contains("<available>"));
assert!(!rendered.contains("AQAAAAAAAA"));
let (first_count, first_request) = first_handle.join().expect("first fixture server must join");
assert_eq!(first_count, 1);
assert_eq!(request_body(first_request.as_str())["method"], serde_json::json!("getBlock"));
let winner_request = winner_handle.join().expect("winner fixture server must join");
assert_eq!(request_body(winner_request.as_str())["method"], serde_json::json!("getBlock"));
return;
}
#[tokio::test(flavor = "current_thread")]
async fn pre_004_get_block_observed_preserves_null_and_reuses_get_block_validation() {
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.null.json"));
let pool = pool_for_url(url.as_str());
let observed = pool
.get_block_observed(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::None)
.await
.expect("observed null getBlock must succeed");
assert!(observed.value().is_none());
assert_eq!(observed.endpoint_name(), "fixture");
assert_eq!(observed.provider().as_str(), "fixture");
handle.join().expect("fixture server must join");
let processed = crate::SolanaGetBlockConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Processed),
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
std::option::Option::Some(0),
std::option::Option::Some(false),
);
let result = pool.get_block_observed(&crate::HttpRoleName::new("default"), 1, std::option::Option::Some(&processed)).await;
let error = result.expect_err("processed observed getBlock commitment must reject before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
return;
}

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-raw-transaction-lib/Cargo.toml
# version: 1
# version: 2
[package]
name = "ksp-raw-transaction-lib"
@@ -8,6 +8,7 @@ edition.workspace = true
repository.workspace = true
[dependencies]
base64.workspace = true
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-store-api = { path = "../ksp-store-api" }
serde_json = { workspace = true }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-raw-transaction-lib/src/canonical.rs
// version: 1
// version: 2
use sha2::Digest; // rust-rules: trait-import
@@ -75,6 +75,26 @@ impl crate::RawTransactionMaterial {
version,
};
}
/// Creates complete source-neutral RAW v1 material from a Base64 transaction wire that embeds its own canonical signature array.
pub fn binary_base64_with_embedded_signature(
network: ksp_store_api::RawNetworkId,
slot: u64,
block_time: std::option::Option<i64>,
transaction_data: impl std::convert::Into<std::string::String>,
meta: crate::RawTransactionWireField<serde_json::Value>,
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
transaction_index: crate::RawTransactionWireField<u32>,
) -> ksp_core_lib::Result<Self> {
let transaction_data = transaction_data.into();
let signature = crate::extract_raw_transaction_signature_from_binary_base64(transaction_data.as_str());
return match signature {
std::result::Result::Ok(signature) => {
std::result::Result::Ok(Self { block_time, meta, network, signature, slot, transaction_data, transaction_index, version })
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
impl std::fmt::Debug for crate::RawTransactionMaterial {

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-raw-transaction-lib/src/error.rs
// version: 1
// version: 2
/// Error code used when RAW v1 canonical payload construction cannot preserve the frozen format contract.
pub const ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction", "canonicalization_invalid");
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
pub const ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "material_invalid");
/// Error code used when textual Solana transaction signature material cannot decode to exactly 64 canonical bytes.
/// Error code used when Solana transaction signature material cannot resolve to exactly 64 canonical bytes.
pub const ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "signature_invalid");
/// Creates a safe canonicalization error without copying source payload material.
@@ -21,7 +21,7 @@ pub(crate) fn material_error(field: &'static str) -> ksp_core_lib::Error {
.with_context("field", field);
}
/// Creates a safe signature error without copying textual signature material.
/// Creates a safe signature error without copying source signature or transaction material.
pub(crate) fn signature_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID, "invalid canonical Solana transaction signature");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-raw-transaction-lib/src/lib.rs
// version: 1
// version: 2
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -37,12 +37,14 @@ pub use self::canonical::canonicalize_raw_transaction;
pub use self::error::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID;
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
pub use self::error::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID;
/// Error code used when textual Solana transaction signature material cannot decode to exactly 64 canonical bytes.
/// Error code used when Solana transaction signature material cannot resolve to exactly 64 canonical bytes.
pub use self::error::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID;
/// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
pub use self::signature::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
/// Minimum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
pub use self::signature::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
/// Extracts the first canonical 64-byte Solana signature from one complete Base64 transaction wire.
pub use self::signature::extract_raw_transaction_signature_from_binary_base64;
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
pub use self::signature::parse_raw_transaction_signature;

View File

@@ -1,5 +1,7 @@
// file: crates/ksp-raw-transaction-lib/src/signature.rs
// version: 1
// version: 2
use base64::Engine; // rust-rules: trait-import
/// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
pub const MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES: usize = 88;
@@ -50,6 +52,80 @@ pub fn parse_raw_transaction_signature(value: &str) -> ksp_core_lib::Result<ksp_
return std::result::Result::Ok(ksp_store_api::RawTransactionSignature::new(decoded));
}
/// Extracts the first canonical 64-byte Solana signature from one complete Base64-encoded transaction wire.
///
/// The compact signature-count prefix must use its canonical short-vector representation, contain at least one signature,
/// and the decoded transaction must retain message bytes after the declared signature array.
pub fn extract_raw_transaction_signature_from_binary_base64(value: &str) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
if value.is_empty() || value.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
return std::result::Result::Err(crate::signature_error());
}
let decoded = base64::engine::general_purpose::STANDARD.decode(value);
let decoded = match decoded {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(_) => return std::result::Result::Err(crate::signature_error()),
};
if base64::engine::general_purpose::STANDARD.encode(decoded.as_slice()) != value {
return std::result::Result::Err(crate::signature_error());
}
let prefix = decode_signature_count(decoded.as_slice());
let (signature_count, prefix_len) = match prefix {
std::result::Result::Ok(prefix) => prefix,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if signature_count == 0 {
return std::result::Result::Err(crate::signature_error());
}
let signatures_len = match signature_count.checked_mul(64) {
std::option::Option::Some(length) => length,
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
};
let message_offset = match prefix_len.checked_add(signatures_len) {
std::option::Option::Some(offset) => offset,
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
};
if message_offset >= decoded.len() {
return std::result::Result::Err(crate::signature_error());
}
let first_end = match prefix_len.checked_add(64) {
std::option::Option::Some(end) => end,
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
};
let first = match decoded.get(prefix_len..first_end) {
std::option::Option::Some(first) => first,
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
};
let mut signature = [0_u8; 64];
signature.copy_from_slice(first);
return std::result::Result::Ok(ksp_store_api::RawTransactionSignature::new(signature));
}
fn decode_signature_count(bytes: &[u8]) -> ksp_core_lib::Result<(usize, usize)> {
let mut value = 0_usize;
for index in 0..3_usize {
let byte = match bytes.get(index) {
std::option::Option::Some(byte) => *byte,
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
};
let payload = usize::from(byte & 0x7f);
let shift = index * 7;
value |= payload << shift;
if byte & 0x80 == 0 {
if index != 0 {
let minimum = 1_usize << shift;
if value < minimum {
return std::result::Result::Err(crate::signature_error());
}
}
if value > usize::from(u16::MAX) {
return std::result::Result::Err(crate::signature_error());
}
return std::result::Result::Ok((value, index + 1));
}
}
return std::result::Result::Err(crate::signature_error());
}
fn base58_digit(byte: u8) -> std::option::Option<u8> {
return match byte {
b'1'..=b'9' => std::option::Option::Some(byte - b'1'),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
// version: 2
// version: 3
//! Dependency-boundary canaries for the common RAW Transaction foundation.
@@ -18,8 +18,9 @@ fn production_sources() -> std::string::String {
}
#[test]
fn pre_002_manifest_uses_api_models_and_exact_common_dependencies() {
fn pre_004_manifest_adds_only_base64_to_the_exact_common_dependency_surface() {
let manifest = manifest();
assert!(manifest.contains("base64.workspace = true"));
assert!(manifest.contains("ksp-core-lib = { path = \"../ksp-core-lib\" }"));
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
assert!(manifest.contains("serde_json = { workspace = true }"));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-raw-transaction-lib/tests/public_api.rs
// version: 2
// version: 3
//! Integration canaries for the public common RAW Transaction crate-root surface.
@@ -77,3 +77,25 @@ fn pre_002_acquisition_assembly_accepts_store_api_producer_owned_metadata() {
assert_eq!(acquisition.transaction().reference(), acquisition.observation().transaction());
return;
}
#[test]
fn pre_004_embedded_signature_and_block_material_contract_are_available_from_crate_root() {
let encoded = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let signature = ksp_raw_transaction_lib::extract_raw_transaction_signature_from_binary_base64(encoded);
assert!(signature.is_ok());
let network = ksp_store_api::RawNetworkId::new("devnet");
assert!(network.is_ok());
if let std::result::Result::Ok(network) = network {
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
network,
1,
std::option::Option::None,
encoded,
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
ksp_raw_transaction_lib::RawTransactionWireField::Value(0),
);
assert!(material.is_ok());
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
// version: 1
// version: 2
fn network() -> std::option::Option<ksp_store_api::RawNetworkId> {
return match ksp_store_api::RawNetworkId::new("devnet") {
@@ -217,3 +217,31 @@ fn pre_002_material_and_wire_debug_do_not_render_transaction_or_meta_material()
assert_eq!(std::format!("{:?}", crate::RawTransactionWireField::Value(secret_meta)), "Value(..)");
return;
}
#[test]
fn pre_004_embedded_signature_material_preserves_block_fields_and_canonicalizes() {
let network = match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(network) => network,
std::result::Result::Err(_) => return,
};
let material = crate::RawTransactionMaterial::binary_base64_with_embedded_signature(
network,
430_000_123,
std::option::Option::Some(1_787_072_400),
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
crate::RawTransactionWireField::Value(serde_json::json!({"err":null,"fee":5000})),
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Legacy),
crate::RawTransactionWireField::Value(0_u32),
);
assert!(material.is_ok());
let transaction = match material.and_then(crate::canonicalize_raw_transaction) {
std::result::Result::Ok(transaction) => transaction,
std::result::Result::Err(_) => return,
};
assert_eq!(transaction.reference().signature().as_bytes(), &[0_u8; 64]);
assert_eq!(transaction.slot(), 430_000_123);
assert_eq!(transaction.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_072_400_000));
assert!(transaction.payload().bytes().starts_with(b"{\"transaction\":[\"AQAAAA"));
assert!(transaction.payload().bytes().ends_with(b"\"version\":\"legacy\",\"transactionIndex\":0}"));
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-raw-transaction-lib/unit_tests/signature.rs
// version: 1
// version: 2
#[test]
fn pre_002_signature_parser_accepts_exact_sixty_four_zero_bytes() {
@@ -31,3 +31,32 @@ fn pre_002_signature_parser_rejects_text_bounds_invalid_base58_and_noncanonical_
}
return;
}
#[test]
fn pre_004_embedded_signature_extracts_first_signature_from_canonical_transaction_base64() {
let encoded = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
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(), &[0_u8; 64]);
}
return;
}
#[test]
fn pre_004_embedded_signature_rejects_invalid_base64_noncanonical_short_vec_and_truncation() {
for encoded in [
"not-base64",
"gQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
] {
let result = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID);
assert!(!error.to_string().contains(encoded));
}
}
return;
}

279
deltas/0.3.10/pre.004.md Normal file
View File

@@ -0,0 +1,279 @@
<!-- file: deltas/0.3.10/pre.004.md -->
<!-- version: 1 -->
# Delta `0.3.10-pre.004` — HTTP observed block + matériau RAW block
## Base requise
```text
0.3.10-pre.003
workspace.package.version = 0.3.10-pre.3
```
Le gate opérateur communiqué le 2026-09-06 ferme intégralement `pre.003` : audits statiques propres, `cargo check --workspace` PASS, Clippy workspace `--all-targets --all-features -- -D warnings` PASS, tests de `ksp-raw-transaction-lib` PASS, tests de `ksp-job-backfill-lib` PASS avec 51 tests unitaires et 20 tests dintégration, arbres normal/features conformes.
## Objectif
Fermer `TR-B` et la preuve HTTP block de `TR-C` sans ouvrir le Worker :
```text
getBlock pool HTTP
-> valeur observée + provider/endpoint winner
-> SolanaConfirmedBlock
-> N SolanaBlockTransaction full Base64
-> matériau RAW source-neutral
```
La tranche ne crée aucun adapter productif Transport -> common. Conformément à `TR-C2`, cet adapter reste producer-owned et sera matérialisé dans le futur `ksp-worker-raw-transaction-ingest-lib`.
## Version
```text
workspace.package.version = 0.3.10-pre.4
```
Le header du `Cargo.toml` racine passe de `493` à `494`. Le nombre de membres workspace reste `20`.
## `get_block_observed`
`ksp-onchain-transport-lib` ajoute :
```text
HttpTransportPool::get_block_observed(...)
```
Le wrapper :
```text
réutilise exactement la validation/config moderne de get_block
utilise le routing/admission/timeout/retry standard du pool
retourne HttpObservedValue<Option<SolanaConfirmedBlock>>
conserve endpoint_name + provider du winner réel après retry/reroute
préserve result = null comme None
nexpose ni URL, ni headers, ni raw HTTP body
réutilise le décodage typé SolanaConfirmedBlock
```
`get_block(...)` et `get_block_observed(...)` partagent désormais le même helper privé de paramètres et la même validation du commitment `processed`.
## Provenance winner déterministe
Le canari Transport configure deux endpoints :
```text
endpoint 1 -> HTTP 429
endpoint 2 -> getBlock success
```
La valeur observée doit reporter exclusivement :
```text
endpoint_name = winner-endpoint
provider = winner-provider
```
Le Debug conserve uniquement la projection sûre et ne rend pas le transaction wire Base64.
## Matériau Base64 source-neutral
`ksp-raw-transaction-lib` ajoute `base64` à son graphe common et expose :
```text
extract_raw_transaction_signature_from_binary_base64(...)
RawTransactionMaterial::binary_base64_with_embedded_signature(...)
```
Lextracteur lit directement le wire transaction Solana Base64 sans dépendance à une crate transaction runtime :
```text
borne la représentation textuelle avant décodage
exige Base64 STANDARD canonique
lit un short_vec de signature count borné et canonique
refuse count = 0
refuse short_vec tronqué/non canonique
refuse tableau de signatures tronqué
exige au moins un byte de message après les signatures déclarées
retourne uniquement la première signature exacte de 64 octets
ne copie jamais le transaction wire dans erreur/Debug
```
Le constructeur common réutilise cette signature puis conserve les dimensions RAW v1 existantes :
```text
network
slot
block_time
transaction Base64
meta omitted/null/value
version omitted/null/legacy/number
transaction_index
```
Le format canonique reste strictement `ksp.solana.raw_transaction` v1.
## Preuve cross-layer sans nouveau couplage de production
Le nouveau canari test-only :
```text
crates/ksp-job-backfill-lib/tests/http_block_material.rs
```
utilise une crate qui dépend déjà de Transport et de common pour prouver la composition future sans créer dedge architectural supplémentaire :
```text
get_block_observed
-> SolanaConfirmedBlock
-> 2 SolanaBlockTransaction distinctes
-> full Base64 + meta + version + transactionIndex 0/1
-> RawTransactionMaterial::binary_base64_with_embedded_signature
-> canonicalize_raw_transaction
```
Les deux transaction wires minimales font `66` octets chacune et utilisent des signatures distinctes :
```text
transaction 0 -> signature [0; 64]
transaction 1 -> signature [1; 64]
```
Le canari vérifie que ces signatures et les index 0/1 restent associés à la bonne transaction, avec le slot et le block time communs du bloc.
Cette preuve reste strictement test-only. Aucun symbole `ksp_raw_transaction_lib` nest ajouté aux sources de production de `ksp-onchain-transport-lib`.
## Dépendances common
Le graphe runtime direct de `ksp-raw-transaction-lib` devient exactement :
```text
base64
ksp-core-lib
ksp-store-api
serde_json
sha2
```
Restent interdits :
```text
ksp-store-lib
ksp-onchain-transport-lib
ksp-config-lib
ksp-job-api
ksp-job-backfill-lib
ksp-worker-api
tokio
futures
```
La règle `DEP-PIPE-006` reste donc respectée.
## Documentation durable
`crates/ksp-onchain-transport-lib/USAGE.md` documente la différence entre :
```text
get_block(...)
get_block_observed(...)
```
sans transformer le guide en journal de prerelease.
`docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md` ferme `TR-B`, conserve `TR-C2` producer-owned et corrige le graphe common vers `ksp-store-api`.
Le plan `031` et la validation `027` enregistrent également la fermeture opérateur de `pre.003` et le périmètre exact de `pre.004`.
## Fichiers ajoutés
```text
crates/ksp-job-backfill-lib/tests/http_block_material.rs
crates/ksp-onchain-transport-lib/fixtures/http/get_block.observed_material.success.json
deltas/0.3.10/pre.004.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-onchain-transport-lib/USAGE.md
crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
crates/ksp-raw-transaction-lib/Cargo.toml
crates/ksp-raw-transaction-lib/src/canonical.rs
crates/ksp-raw-transaction-lib/src/error.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/unit_tests/canonical.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.
## Validations exécutées dans lenvironnement dassemblage
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
Markdown table audit: clean (339 table(s), 757 file(s))
```
Lenvironnement dassemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`.
## 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 --test http_block_material
cargo tree -p ksp-raw-transaction-lib --edges normal
cargo tree -p ksp-onchain-transport-lib --edges normal
```
## Validations non exécutées localement
```text
cargo fmt --all
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 --test http_block_material
cargo tree -p ksp-raw-transaction-lib --edges normal
cargo tree -p ksp-onchain-transport-lib --edges normal
smokes live : non requis pour cette tranche déterministe
```
## Décisions prises
```text
TR-B est fermé par get_block_observed
get_block et get_block_observed partagent validation + décodage
la common extrait la signature dun transaction wire Base64 complet
aucune dépendance Solana transaction runtime nest ajoutée à la common
aucun adapter productif Transport -> common dans Transport
TR-C2 reste Worker-owned
la preuve block -> common est cross-layer et test-only
RAW v1 reste inchangé
aucun Worker live nest ouvert dans pre.004
```
## Questions ouvertes
Aucune question ne bloque le gate de `pre.004`. `pre.005` reste dédiée à la parité WS `blockSubscribe` + Helius `transactionSubscribe`, avec golden parity ou fallback hydration explicitement qualifié selon la représentation réellement disponible.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Acquisition et alimentation `RawTransaction`
@@ -539,13 +539,13 @@ KSP possède déjà :
get_signatures_for_address
get_transaction / get_transaction_observed
get_blocks / get_blocks_with_limit
get_block
get_block / get_block_observed
get_slot
get_first_available_block
minimum_ledger_slot
```
Gap principal : `getBlock` n'a pas encore d'équivalent `get_block_observed`. Pour un pool multi-endpoint, la provenance provider/endpoint exacte doit être conservée avant de persister une observation.
`get_block_observed` conserve désormais la provenance sûre provider/endpoint du winner réel dun pool HTTP, symétriquement à `get_transaction_observed`. La projection dune transaction full issue dun bloc vers le matériau RAW commun reste volontairement hors de Transport : ladapter productif appartient au producer (Worker/Job) afin de ne créer aucun edge `Transport -> ksp-raw-transaction-lib`.
### 12.2 Transport WebSocket
@@ -615,7 +615,7 @@ Les prix/tiers d'audit ne doivent pas devenir une politique runtime.
La canonicalisation RAW v1 actuellement prouvée dans `ksp-job-backfill-lib::conversion` ne doit ni y rester enfermée ni être copiée dans le Worker.
Le handoff retient une petite crate source-neutral commune, à matérialiser pendant `0.3.10` :
Le handoff retient la crate source-neutral commune désormais matérialisée :
```text
ksp-raw-transaction-lib
@@ -650,13 +650,16 @@ backend Store physique
Graphe conceptuel :
```text
ksp-job-backfill-lib --------------------> ksp-raw-transaction-lib ----> Store façade
ksp-worker-raw-transaction-ingest-lib ---> ksp-raw-transaction-lib ----> Store façade
ksp-job-backfill-lib --------------------> ksp-raw-transaction-lib ----> ksp-store-api
ksp-worker-raw-transaction-ingest-lib ---> ksp-raw-transaction-lib ----> ksp-store-api
ksp-job-backfill-lib --------------------> ksp-store-lib
ksp-worker-raw-transaction-ingest-lib ---> ksp-store-lib
ksp-job-backfill-lib --------------------> ksp-onchain-transport-lib
ksp-worker-raw-transaction-ingest-lib ---> ksp-onchain-transport-lib
aucun edge Job <-> Worker
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é.
@@ -709,7 +712,7 @@ sources EARLY via adapter extensible
| ID | Adaptation | Motif |
|--------|-----------------------------------------------------------------------------------|------------------------------------------------------|
| `TR-B` | `get_block_observed` symétrique de `get_transaction_observed` | provenance exacte en pool HTTP |
| `TR-B` | fermé : `get_block_observed` symétrique de `get_transaction_observed` | provenance exacte en pool HTTP |
| `TR-C` | projection source-neutral des transactions full WS/Yellowstone | éviter plusieurs canonicalizers filaires |
| `TR-D` | métadonnées sûres d'acquisition live au moment de la conversion | observation uniforme |
| `TR-E` | conserver/exploiter `from_slot`, replay info et snapshots de continuité existants | ne pas créer un second moteur Yellowstone |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Plan v0.3.10 — RAW Transaction commune + Worker dingestion multi-source
@@ -1050,9 +1050,15 @@ La canonicalisation privée du Backfill est supprimée au profit de `ksp-raw-tra
Les invariants historiques restent gelés : `ksp.solana.raw_transaction` v1, golden `112` octets, SHA-256 `220792d2b15d262fda242cb220774ee9ddeffebf04dcfadabcf8ef76a9b1a7c3`, origin `Backfill`, provider/endpoint/commitment/capture-session, observation key de campagne et sémantique `Missing`. Les erreurs common sont remappées sur `ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID` afin de ne pas modifier le contrat externe du Job.
Le rejeu opérateur du 2026-09-06 ferme `pre.003` : audits statiques propres, `cargo check --workspace` PASS, Clippy strict PASS, `cargo test -p ksp-raw-transaction-lib` PASS, `cargo test -p ksp-job-backfill-lib` PASS avec 51 tests unitaires et 20 tests dintégration, et arbres normal/features conformes.
### `pre.004` — HTTP observed block + block material
Ajouter `get_block_observed`, fixtures provenance, extraction transaction par transaction et adapters HTTP block vers matériau common. Aucun Worker live complet encore.
**Statut : réalisé.**
`HttpTransportPool::get_block_observed(...)` complète la provenance HTTP observée sans exposer URL/header/body et conserve le provider/endpoint du winner réel après retry/reroute. La common RAW ajoute lextraction bornée de la première signature depuis un transaction wire Base64 complet et `RawTransactionMaterial::binary_base64_with_embedded_signature(...)` pour préserver slot, block time, meta, version et transaction index.
Conformément à `TR-C2`, aucun adapter productif Transport DTO -> `RawTransactionMaterial` nest placé dans Transport : une preuve cross-layer test-only démontre `get_block_observed -> N SolanaBlockTransaction -> RawTransactionMaterial -> RAW v1` sur deux transactions distinctes avec index 0/1, tandis que ladapter productif reste réservé au futur Worker. Aucun Worker live complet nest ouvert.
### `pre.005` — parité WS/Helius full

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Validation v0.3.10 — RAW Transaction commune + Worker dingestion
@@ -661,3 +661,109 @@ cargo tree -p ksp-job-backfill-lib --edges normal
cargo tree -p ksp-job-backfill-lib -e features
```
## 13. Fermeture opérateur `pre.003` et gate `pre.004` — HTTP observed block
### 13.1 Fermeture opérateur de `pre.003`
Le rejeu communiqué le 2026-09-06 ferme la migration Backfill :
```text
cargo fmt --all : exécuté
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (339 table(s), 756 file(s))
cargo check --workspace : PASS
cargo clippy --workspace --all-targets --all-features -- -D warnings : PASS
cargo test -p ksp-raw-transaction-lib : PASS, 9 unit + 8 intégration
cargo test -p ksp-job-backfill-lib : PASS, 51 unit + 20 intégration
cargo tree -p ksp-job-backfill-lib --edges normal : edge common présent, graphe attendu
cargo tree -p ksp-job-backfill-lib -e features : aucune feature KSP inattendue
```
`pre.004` peut donc être ouverte sur une base entièrement validée.
### 13.2 Surface Transport `pre.004`
`HttpTransportPool::get_block_observed(...)` :
```text
réutilise exactement la validation/config moderne de get_block
utilise le même routing/admission/retry standard
retourne Option<SolanaConfirmedBlock>
conserve endpoint_name + provider du winner réel
préserve result = null comme None
nexpose ni URL, ni headers, ni raw HTTP body
Debug de HttpObservedValue ne rend pas le block payload
```
Les fixtures déterministes couvrent le retry/reroute depuis un premier endpoint `429` vers un second winner et vérifient que la provenance retournée appartient au second endpoint.
### 13.3 Matériau common depuis un bloc Base64
`ksp-raw-transaction-lib` ajoute :
```text
extract_raw_transaction_signature_from_binary_base64(...)
RawTransactionMaterial::binary_base64_with_embedded_signature(...)
```
Lextracteur :
```text
borne lentrée avant décodage
exige un Base64 STANDARD canonique
lit le short_vec Solana de signatures sans dépendance Solana transaction runtime
refuse count = 0
refuse short_vec tronqué/non canonique
refuse tableau de signatures tronqué
exige des bytes message après le tableau déclaré
copie uniquement la première signature exacte de 64 octets
ne copie jamais le transaction wire dans erreur/Debug
```
Les fixtures de référence utilisent deux transactions minimales de `66` octets : prefix `1`, signatures respectives `[0; 64]` et `[1; 64]`, puis un byte de message. Les signatures extraites sont donc exactement distinctes et restent associées aux index 0 et 1 du bloc.
### 13.4 Preuve cross-layer sans mauvais edge
Le canari dintégration `ksp-job-backfill-lib/tests/http_block_material.rs` utilise les deux dépendances déjà autorisées de cette crate pour prouver :
```text
get_block_observed
-> SolanaConfirmedBlock
-> 2 SolanaBlockTransaction distinctes
-> full Base64 transaction + meta + version + index 0/1
-> RawTransactionMaterial::binary_base64_with_embedded_signature
-> canonicalize_raw_transaction
-> signatures [0;64] / [1;64] préservées
```
Cette preuve est volontairement test-only. `TR-C2` reste inchangé : ladapter productif Transport DTO -> common sera Worker-owned. Il nexiste toujours aucun edge de production `ksp-onchain-transport-lib -> ksp-raw-transaction-lib` ni linverse.
### 13.5 Audits dassemblage
Exécuté dans lenvironnement dassemblage après fermeture du delta :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
Markdown table audit: clean (339 table(s), 757 file(s))
```
Les gates Cargo restent à rejouer côté opérateur dans lenvironnement Rust complet :
```bash
cargo fmt --all
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 --test http_block_material
cargo tree -p ksp-raw-transaction-lib --edges normal
cargo tree -p ksp-onchain-transport-lib --edges normal
```