v0.4.8-pre.011

This commit is contained in:
2026-08-07 07:14:49 +02:00
parent 3167297283
commit dcc1d382d9
38 changed files with 1358 additions and 129 deletions

View File

@@ -1,8 +1,17 @@
<!-- file: kb-pipeline/CHANGELOG.md -->
<!-- version: 19 -->
<!-- version: 22 -->
# CHANGELOG — kb-pipeline
## `0.4.8-pre.011` — validation Token-2022 Token Metadata
- ajoute lextraction bornée du retour base64 produit par `Emit` ;
- aligne la fixture de simulation Token-2022 sur le variant canonique `ExApiExecutionBlockhashKind::Latest` ;
- refuse les plages `Emit` ouvertes avec `start` sans `end`, en cohérence avec le builder et la limite de retour Solana ;
- décode uniquement un retour complet et conserve un retour partiel sans inventer un état ;
- ajoute la postcondition stateful de changement ou suppression de lautorité metadata.
## 0.4.8-pre.010
- remplace lassertion runtime sur la relation entre borne RPC et borne de décodage par une assertion `const`, afin de supprimer le dernier warning `clippy::assertions_on_constants` ;

View File

@@ -1,5 +1,5 @@
# file: kb-pipeline/Cargo.toml
# version: 14
# version: 15
[package]
name = "kb-pipeline"
@@ -10,6 +10,7 @@ publish.workspace = true
[dependencies]
async-trait.workspace = true
base64.workspace = true
bs58.workspace = true
chrono.workspace = true
futures-util.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline/README.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# kb-pipeline
@@ -50,3 +50,5 @@ La crate coordonne `kb-onchain-transport`, `kb-store`, `kb-lib`, `kb-core` et `k
- [Travaux restant à réaliser](TODO.md)
- [Historique des changements](CHANGELOG.md)
- [Architecture du pipeline](../docs/architecture/PIPELINE_ARCHITECTURE.md)
- validation bornée du retour `Emit` Token-2022 et postcondition stateful de lautorité metadata ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline/USAGE.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# Utilisation de kb-pipeline
@@ -350,3 +350,7 @@ fn summarize_metaplex_postconditions(
- les comptes et projections nécessaires aux postconditions.
Lorsque `materialize_after_confirmation` est activé, une soumission exige au moins une lecture de postcondition. Après confirmation, les snapshots bornés sont décodés et projetés. Lorsque loption est désactivée, le runner conserve la simulation, la soumission, la confirmation et les postconditions demandées, mais német pas les projections de matérialisation.
## Validation Token-2022 Token Metadata
`inspect_token_2022_metadata_emit_simulation` valide le `returnData` dune simulation `Emit`. `inspect_token_2022_metadata_authority_postcondition` compare lautorité attendue au snapshot TLV autoritatif après confirmation.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline/src/lib.rs
// version: 20
// version: 21
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -24,6 +24,7 @@ mod spl_elgamal_registry_stateful;
mod spl_token_2022_correlation;
mod spl_token_2022_crypto_preflight;
mod spl_token_2022_execution_orchestration;
mod spl_token_2022_metadata;
mod spl_token_2022_preflight;
mod spl_token_2022_proof_orchestration;
mod spl_token_2022_stateful;
@@ -251,6 +252,16 @@ pub use self::spl_token_2022_execution_orchestration::Token2022ExecutionReadines
pub use self::spl_token_2022_execution_orchestration::summarize_token_2022_postconditions;
/// Validates the complete Token-2022 execution envelope before signing.
pub use self::spl_token_2022_execution_orchestration::validate_token_2022_execution_readiness;
/// Maximum Token Metadata bytes accepted from Solana return data.
pub use self::spl_token_2022_metadata::MAX_TOKEN_2022_METADATA_EMIT_BYTES;
/// Bounded evidence extracted from one successful Token Metadata `Emit` simulation.
pub use self::spl_token_2022_metadata::Token2022MetadataEmitEvidence;
/// Checks the final embedded metadata authority against one authoritative snapshot.
pub use self::spl_token_2022_metadata::inspect_token_2022_metadata_authority_postcondition;
/// Extracts and validates Token Metadata return data from one exact simulation.
pub use self::spl_token_2022_metadata::inspect_token_2022_metadata_emit_simulation;
/// Validates one Token Metadata `Emit` range against Solana return-data bounds.
pub use self::spl_token_2022_metadata::validate_token_2022_metadata_emit_range;
/// Migrated MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS contract.
pub use self::spl_token_2022_preflight::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS;
/// Migrated MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES contract.

View File

@@ -0,0 +1,376 @@
// file: kb-pipeline/src/spl_token_2022_metadata.rs
// version: 3
//! Token-2022 Token Metadata return-data and authority postcondition contracts.
use base64::Engine; // rust-rules: trait-import
/// Maximum Token Metadata bytes accepted from Solana return data.
pub const MAX_TOKEN_2022_METADATA_EMIT_BYTES: usize = 1_024;
/// Bounded evidence extracted from one successful Token Metadata `Emit` simulation.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022MetadataEmitEvidence {
/// Program that produced the return data.
pub program_id: std::string::String,
/// Optional requested range start.
pub start: std::option::Option<u64>,
/// Optional requested range end.
pub end: std::option::Option<u64>,
/// Decoded return-data bytes.
pub data: std::vec::Vec<u8>,
/// Exact complete Token Metadata projection, only for an un-ranged emit.
pub decoded_metadata: std::option::Option<serde_json::Value>,
}
/// Validates one Token Metadata `Emit` range against Solana return-data bounds.
pub fn validate_token_2022_metadata_emit_range(
start: std::option::Option<u64>,
end: std::option::Option<u64>,
) -> kb_core::Result<()> {
if let (std::option::Option::Some(start), std::option::Option::Some(end)) = (start, end) {
if start > end {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_range_invalid",
format!("Token Metadata Emit start {start} exceeds end {end}"),
));
}
if end - start > crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES as u64 {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_range_too_large",
format!(
"Token Metadata Emit range exceeds {} bytes",
crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES
),
));
}
}
if let (std::option::Option::None, std::option::Option::Some(end)) = (start, end) {
if end > crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES as u64 {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_range_too_large",
format!(
"Token Metadata Emit range from zero exceeds {} bytes",
crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES
),
));
}
}
if let (std::option::Option::Some(_), std::option::Option::None) = (start, end) {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_range_end_required",
"Token Metadata Emit with a start offset requires an explicit end to remain bounded",
));
}
return std::result::Result::Ok(());
}
/// Extracts and validates Token Metadata return data from one exact simulation.
pub fn inspect_token_2022_metadata_emit_simulation(
simulation: &kb_lib::ExApiExecutionSimulationResult,
start: std::option::Option<u64>,
end: std::option::Option<u64>,
) -> kb_core::Result<crate::Token2022MetadataEmitEvidence> {
match crate::validate_token_2022_metadata_emit_range(start, end) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if !simulation.simulated || !simulation.success {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_simulation_required",
"Token Metadata Emit evidence requires a successful simulation",
));
}
let return_data = match simulation.return_data.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_return_data_missing",
"Token Metadata Emit simulation did not return program data",
));
},
};
let program_id = match return_data.get("programId").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_program_id_missing",
"Token Metadata Emit return data is missing programId",
));
},
};
if program_id != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_program_id_mismatch",
format!("Token Metadata Emit return data came from {program_id}"),
));
}
let data = match return_data.get("data").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) if value.len() == 2 => value,
_ => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_data_invalid",
"Token Metadata Emit return data must be [payload, encoding]",
));
},
};
let payload = match data.first().and_then(|value| return value.as_str()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_payload_invalid",
"Token Metadata Emit return payload must be a base64 string",
));
},
};
let encoding = match data.get(1).and_then(|value| return value.as_str()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_encoding_invalid",
"Token Metadata Emit return data encoding is missing",
));
},
};
if encoding != "base64" {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_encoding_invalid",
"Token Metadata Emit return data encoding must be base64",
));
}
let decoded = match base64::engine::general_purpose::STANDARD.decode(payload) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_base64_invalid",
format!("Token Metadata Emit return payload is invalid base64: {error}"),
));
},
};
if decoded.len() > crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_data_too_large",
format!(
"Token Metadata Emit returned {} bytes, maximum is {}",
decoded.len(),
crate::MAX_TOKEN_2022_METADATA_EMIT_BYTES
),
));
}
let decoded_metadata = if start.is_none() && end.is_none() {
match kb_lib::decoder_spl_token_2022_parse_token_metadata_value(decoded.as_slice()) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_emit_payload_decode_failed",
error,
));
},
}
} else {
std::option::Option::None
};
return std::result::Result::Ok(crate::Token2022MetadataEmitEvidence {
program_id: program_id.to_string(),
start,
end,
data: decoded,
decoded_metadata,
});
}
/// Checks the final embedded metadata authority against one authoritative snapshot.
pub fn inspect_token_2022_metadata_authority_postcondition(
account: &kb_lib::MdPubkey,
expected_authority: std::option::Option<&kb_lib::MdPubkey>,
snapshot: &crate::Token2022StatefulSnapshotBundle,
) -> crate::Token2022ExecutionPostcondition {
if snapshot.account_key != account.0 {
return crate::Token2022ExecutionPostcondition {
role: "metadata".to_string(),
account: account.clone(),
status: crate::Token2022ExecutionPostconditionStatus::Contradicted,
diagnostic: "Token-2022 metadata snapshot belongs to a different account".to_string(),
};
}
let output = snapshot.outputs.iter().find(|output| {
return output.family == kb_lib::MdMaterializedEventFamily::Metadata
&& output.payload_json.get("projectionKind").and_then(serde_json::Value::as_str)
== std::option::Option::Some("token_metadata");
});
let observed = output.and_then(|value| {
return value.payload_json.get("valueFields").and_then(|fields| {
return fields.get("updateAuthority");
});
});
let confirmed = match (expected_authority, observed) {
(std::option::Option::Some(expected), std::option::Option::Some(value)) => {
value.as_str() == std::option::Option::Some(expected.0.as_str())
},
(std::option::Option::None, std::option::Option::Some(value)) => value.is_null(),
_ => false,
};
return crate::Token2022ExecutionPostcondition {
role: "metadata".to_string(),
account: account.clone(),
status: if confirmed {
crate::Token2022ExecutionPostconditionStatus::Confirmed
} else {
crate::Token2022ExecutionPostconditionStatus::Contradicted
},
diagnostic: if confirmed {
"authoritative Token-2022 metadata snapshot matches the expected update authority"
.to_string()
} else {
"authoritative Token-2022 metadata snapshot contradicts or omits the expected update authority"
.to_string()
},
};
}
#[cfg(test)]
mod tests {
use base64::Engine; // rust-rules: trait-import
fn simulation(data: &[u8]) -> kb_lib::ExApiExecutionSimulationResult {
return kb_lib::ExApiExecutionSimulationResult {
simulated: true,
success: true,
cluster: kb_lib::ExApiExecutionCluster::Devnet,
blockhash_kind: kb_lib::ExApiExecutionBlockhashKind::Latest,
blockhash_age_slots: std::option::Option::Some(0),
replacement_blockhash: std::option::Option::None,
replacement_last_valid_block_height: std::option::Option::None,
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
units_consumed: std::option::Option::Some(1),
estimated_fee_lamports: std::option::Option::Some(5_000),
logs: std::vec::Vec::new(),
return_data: std::option::Option::Some(serde_json::json!({
"programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"data": [base64::engine::general_purpose::STANDARD.encode(data), "base64"]
})),
error: std::option::Option::None,
};
}
fn metadata_bytes(authority: std::option::Option<[u8; 32]>) -> std::vec::Vec<u8> {
let mut value = std::vec::Vec::new();
value.extend_from_slice(authority.unwrap_or([0_u8; 32]).as_slice());
value.extend_from_slice([7_u8; 32].as_slice());
for field in ["Token", "TKN", "https://example.invalid/token.json"] {
value.extend_from_slice((field.len() as u32).to_le_bytes().as_slice());
value.extend_from_slice(field.as_bytes());
}
value.extend_from_slice(0_u32.to_le_bytes().as_slice());
return value;
}
#[test]
fn complete_emit_decodes_exact_metadata_and_ranged_emit_keeps_only_bytes() {
let complete = crate::inspect_token_2022_metadata_emit_simulation(
&simulation(metadata_bytes(std::option::Option::Some([9_u8; 32])).as_slice()),
std::option::Option::None,
std::option::Option::None,
)
.unwrap_or_else(|error| panic!("complete Emit evidence failed: {error}"));
assert_eq!(
complete
.decoded_metadata
.as_ref()
.and_then(|value| return value.get("name"))
.and_then(serde_json::Value::as_str),
std::option::Option::Some("Token")
);
let ranged = crate::inspect_token_2022_metadata_emit_simulation(
&simulation(&[1_u8, 2_u8, 3_u8]),
std::option::Option::Some(0),
std::option::Option::Some(3),
)
.unwrap_or_else(|error| panic!("ranged Emit evidence failed: {error}"));
assert!(ranged.decoded_metadata.is_none());
assert_eq!(ranged.data, vec![1_u8, 2_u8, 3_u8]);
}
#[test]
fn emit_range_and_provider_contract_fail_closed() {
assert!(
crate::validate_token_2022_metadata_emit_range(
std::option::Option::Some(2),
std::option::Option::Some(1),
)
.is_err()
);
assert!(
crate::validate_token_2022_metadata_emit_range(
std::option::Option::Some(0),
std::option::Option::Some(1_025),
)
.is_err()
);
assert!(
crate::validate_token_2022_metadata_emit_range(
std::option::Option::Some(1),
std::option::Option::None,
)
.is_err()
);
let mut invalid = simulation(&[1_u8]);
invalid.return_data = std::option::Option::Some(serde_json::json!({
"programId": kb_program_ids::SYSTEM_PROGRAM_ID,
"data": ["AQ==", "base64"]
}));
assert!(
crate::inspect_token_2022_metadata_emit_simulation(
&invalid,
std::option::Option::Some(0),
std::option::Option::Some(1)
)
.is_err()
);
}
#[test]
fn authority_postcondition_accepts_exact_value_and_null() {
let account = kb_lib::MdPubkey(bs58::encode([3_u8; 32]).into_string());
let authority = kb_lib::MdPubkey(bs58::encode([4_u8; 32]).into_string());
let mut snapshot = crate::Token2022StatefulSnapshotBundle {
account_key: account.0.clone(),
slot: 9,
state_kind: "mint".to_string(),
extension_names: vec!["token_metadata".to_string()],
outputs: vec![kb_lib::MtApiMaterializedOutput {
output_key: "metadata".to_string(),
family: kb_lib::MdMaterializedEventFamily::Metadata,
payload_json: serde_json::json!({
"projectionKind": "token_metadata",
"valueFields": {"updateAuthority": authority.0}
}),
}],
};
let confirmed = crate::inspect_token_2022_metadata_authority_postcondition(
&account,
std::option::Option::Some(&authority),
&snapshot,
);
assert_eq!(confirmed.status, crate::Token2022ExecutionPostconditionStatus::Confirmed);
snapshot.outputs[0].payload_json["valueFields"]["updateAuthority"] =
serde_json::Value::Null;
let cleared = crate::inspect_token_2022_metadata_authority_postcondition(
&account,
std::option::Option::None,
&snapshot,
);
assert_eq!(cleared.status, crate::Token2022ExecutionPostconditionStatus::Confirmed);
snapshot.account_key = bs58::encode([8_u8; 32]).into_string();
let wrong_account = crate::inspect_token_2022_metadata_authority_postcondition(
&account,
std::option::Option::None,
&snapshot,
);
assert_eq!(
wrong_account.status,
crate::Token2022ExecutionPostconditionStatus::Contradicted
);
}
}