v0.4.8-pre.011
This commit is contained in:
@@ -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.
|
||||
|
||||
376
kb-pipeline/src/spl_token_2022_metadata.rs
Normal file
376
kb-pipeline/src/spl_token_2022_metadata.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user