v0.3.10-pre.004
This commit is contained in:
@@ -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"
|
||||
|
||||
141
crates/ksp-job-backfill-lib/tests/http_block_material.rs
Normal file
141
crates/ksp-job-backfill-lib/tests/http_block_material.rs
Normal 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;
|
||||
}
|
||||
@@ -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 d’un 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 l’endpoint et le provider réellement gagnants après sélection/retry/reroute. Cette projection n’expose 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 d’une 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
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 }"));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
279
deltas/0.3.10/pre.004.md
Normal 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 d’inté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
|
||||
n’expose 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(...)
|
||||
```
|
||||
|
||||
L’extracteur 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 d’edge 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` n’est 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 l’environnement d’assemblage
|
||||
|
||||
```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))
|
||||
```
|
||||
|
||||
L’environnement d’assemblage 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 d’un transaction wire Base64 complet
|
||||
aucune dépendance Solana transaction runtime n’est 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 n’est 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.
|
||||
@@ -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 d’un pool HTTP, symétriquement à `get_transaction_observed`. La projection d’une transaction full issue d’un bloc vers le matériau RAW commun reste volontairement hors de Transport : l’adapter 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 |
|
||||
|
||||
@@ -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 d’ingestion 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 d’inté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 l’extraction 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` n’est 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 l’adapter productif reste réservé au futur Worker. Aucun Worker live complet n’est ouvert.
|
||||
|
||||
### `pre.005` — parité WS/Helius full
|
||||
|
||||
|
||||
@@ -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 d’ingestion
|
||||
|
||||
@@ -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
|
||||
n’expose 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(...)
|
||||
```
|
||||
|
||||
L’extracteur :
|
||||
|
||||
```text
|
||||
borne l’entré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 d’inté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é : l’adapter productif Transport DTO -> common sera Worker-owned. Il n’existe toujours aucun edge de production `ksp-onchain-transport-lib -> ksp-raw-transaction-lib` ni l’inverse.
|
||||
|
||||
### 13.5 Audits d’assemblage
|
||||
|
||||
Exécuté dans l’environnement d’assemblage 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 l’environnement 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
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user