v0.1.0-pre.032
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
- Migration complète de l’orchestration HTTP `backfill` dans `kb-pipeline`.
|
||||
- Conservation des sources explicites et historiques par adresse, des directions `before`/`after`, de la pagination bornée, des retries et de la reprise déterministe.
|
||||
- Adaptation aux façades `kb-onchain-transport`, `kb-store` et `kb-lib`.
|
||||
- Conservation des 15 tests bot2, dont les deux tests async d’annulation coopérative.
|
||||
- Conservation des 14 tests bot2, dont les deux tests async d’annulation coopérative.
|
||||
|
||||
|
||||
## 0.1.0-pre.030
|
||||
@@ -111,3 +111,8 @@
|
||||
|
||||
- Portage des matérialisateurs natifs lifecycle, administration, compliance et staking.
|
||||
- Conservation de leurs 50 tests historiques et ajout du premier test aval de matérialisation.
|
||||
|
||||
## 0.1.0-pre.032
|
||||
|
||||
- Migration de l’inventaire Devnet Token-2022/ElGamal et des contrats de validation machine-readable dans `kb-pipeline`.
|
||||
- Conservation des sept tests bot2 et de `docs/SPL_TOKEN_2022_VALIDATION_MATRIX.json`.
|
||||
|
||||
@@ -103,3 +103,5 @@ La tranche `0.1.0-pre.029` migre la planification et l’extraction core de `kb-
|
||||
### Backfill HTTP
|
||||
|
||||
La tranche `0.1.0-pre.031` ajoute l’orchestration HTTP de backfill : signatures explicites ou historiques par adresse, pagination bornée, hydration via `getTransaction`, retries, annulation, reprise déterministe et persistance des observations.
|
||||
|
||||
La migration `0.1.0-pre.032` ajoute à `kb-pipeline` l’inventaire stable des scénarios Devnet Token-2022/ElGamal et la validation machine-readable du jalon 0.4.6.
|
||||
|
||||
@@ -294,6 +294,8 @@
|
||||
- [x] Conserver les directions `before` et `after`, les ancres et la pagination RPC bornée.
|
||||
- [x] Conserver la reprise au dernier candidat contigu, les retries, le pacing et l’annulation coopérative.
|
||||
- [x] Adapter la persistance à `kb-store` et l’acquisition à `kb-onchain-transport`.
|
||||
- [x] Conserver les 15 tests bot2.
|
||||
- [x] Conserver les 14 tests bot2.
|
||||
- [ ] Valider avec Cargo sur la machine de développement.
|
||||
- [ ] Porter ensuite les orchestrations Solana stateful et d’exécution.
|
||||
|
||||
- [x] `0.1.0-pre.032` — migrer les scénarios Devnet Token-2022/ElGamal et la matrice de validation machine-readable.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-onchain-transport/src/get_transaction.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Standard Solana `getTransaction` adapter for the canonical transaction contract.
|
||||
|
||||
@@ -593,7 +593,7 @@ mod tests {
|
||||
include_str!("../tests/fixtures/get_transaction_legacy_failure.json")
|
||||
},
|
||||
"v0_alt" => {
|
||||
include_str!("../tests/fixtures/get_transaction_v0_alt_cpi_token_2022.json")
|
||||
include_str!("../tests/fixtures/get_transaction_v0_alt_cpi_token2022.json")
|
||||
},
|
||||
"optional_absent" => {
|
||||
include_str!("../tests/fixtures/get_transaction_optional_absent.json")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: kb-pipeline/Cargo.toml
|
||||
# version: 7
|
||||
# version: 8
|
||||
|
||||
[package]
|
||||
name = "kb-pipeline"
|
||||
@@ -18,6 +18,7 @@ kb-onchain-transport = { path = "../kb-onchain-transport" }
|
||||
kb-program-ids = { path = "../kb-program-ids" }
|
||||
kb-store = { path = "../kb-store" }
|
||||
kb-wallet = { path = "../kb-wallet" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline/src/lib.rs
|
||||
// version: 5
|
||||
// version: 7
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -12,6 +12,8 @@ mod constants;
|
||||
mod core_extraction;
|
||||
mod decode_replay;
|
||||
mod plan;
|
||||
mod solana_token2022_devnet_scenarios;
|
||||
mod solana_token2022_validation;
|
||||
|
||||
/// Address category used by one targeted backfill campaign.
|
||||
pub use self::backfill::BackfillAddressKind;
|
||||
@@ -31,8 +33,6 @@ pub use self::backfill::BackfillSource;
|
||||
pub use self::backfill::BackfillSummary;
|
||||
/// Executes one bounded HTTP transaction backfill campaign.
|
||||
pub use self::backfill::execute_http_backfill;
|
||||
/// Canonical tracing target for pipeline orchestration.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Stable core extraction processor name.
|
||||
pub use self::core_extraction::CORE_EXTRACTION_PROCESSOR_NAME;
|
||||
/// Stable core extraction processor version.
|
||||
@@ -83,3 +83,36 @@ pub use self::decode_replay::new_decode_campaign_id;
|
||||
pub use self::plan::PipelineStage;
|
||||
/// Replay selection scope.
|
||||
pub use self::plan::ReplayScope;
|
||||
/// Stable category of one independent Devnet validation scenario.
|
||||
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationFamily;
|
||||
/// Current implementation status of one Devnet validation scenario.
|
||||
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationImplementationStatus;
|
||||
/// One independent Devnet validation scenario exposed to applications.
|
||||
pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationScenario;
|
||||
/// Returns the complete ordered Devnet scenario inventory.
|
||||
pub use self::solana_token2022_devnet_scenarios::devnet_spl_validation_scenarios;
|
||||
/// Maximum number of evidence records accepted in one validation report.
|
||||
pub use self::solana_token2022_validation::MAX_TOKEN2022_VALIDATION_EVIDENCE;
|
||||
/// Required validation environment for one Token-2022 scenario.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationEnvironment;
|
||||
/// One bounded proof attached to a Token-2022 validation scenario.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationEvidence;
|
||||
/// Machine-readable Token-2022 validation matrix.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationMatrix;
|
||||
/// One scenario declared by the canonical validation matrix.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationMatrixScenario;
|
||||
/// Complete bounded Token-2022 validation report.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationReport;
|
||||
/// One declared Token-2022 validation scenario and its observed evidence.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationScenario;
|
||||
/// Exact status of one Token-2022 validation scenario.
|
||||
pub use self::solana_token2022_validation::Token2022ValidationStatus;
|
||||
/// Loads and validates the canonical Token-2022 validation matrix.
|
||||
pub use self::solana_token2022_validation::load_token2022_validation_matrix;
|
||||
/// Validates one Token-2022 validation matrix.
|
||||
pub use self::solana_token2022_validation::validate_token2022_validation_matrix;
|
||||
/// Validates one bounded Token-2022 milestone report.
|
||||
pub use self::solana_token2022_validation::validate_token2022_validation_report;
|
||||
|
||||
/// Canonical tracing target for pipeline orchestration.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
180
kb-pipeline/src/solana_token2022_devnet_scenarios.rs
Normal file
180
kb-pipeline/src/solana_token2022_devnet_scenarios.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
// file: kb-pipeline/src/solana_token2022_devnet_scenarios.rs
|
||||
// version: 4
|
||||
|
||||
//! Stable Devnet validation scenarios required to close milestone 0.4.6.
|
||||
|
||||
/// Stable category of one independent Devnet validation scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DevnetSplValidationFamily {
|
||||
/// Public Token-2022 state mutations without cryptographic proofs.
|
||||
Token2022Public,
|
||||
/// ElGamal Registry lifecycle kept technically separate from Token-2022.
|
||||
ElGamalRegistry,
|
||||
/// Token-2022 confidential operations requiring proof material.
|
||||
Token2022Confidential,
|
||||
}
|
||||
|
||||
/// Stable status of one scenario in the application validation workflow.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DevnetSplValidationImplementationStatus {
|
||||
/// The scenario can be simulated and submitted through the application.
|
||||
Executable,
|
||||
/// The typed backend exists but the application execution path remains to be connected.
|
||||
BackendReady,
|
||||
/// The scenario requires externally prepared proof material before execution.
|
||||
ProofFixtureRequired,
|
||||
}
|
||||
|
||||
/// One independent Devnet validation scenario exposed to applications.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DevnetSplValidationScenario {
|
||||
/// Stable identifier used by scripts, tests and the frontend.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Scenario family.
|
||||
pub family: crate::DevnetSplValidationFamily,
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Current implementation status.
|
||||
pub implementation_status: crate::DevnetSplValidationImplementationStatus,
|
||||
/// Whether this scenario mutates on-chain state.
|
||||
pub destructive: bool,
|
||||
/// Whether explicit operator confirmation is mandatory before submission.
|
||||
pub operator_confirmation_required: bool,
|
||||
/// Whether cryptographic proof material is required.
|
||||
pub proof_required: bool,
|
||||
/// Ordered fixture variables required by the scenario.
|
||||
pub required_fixture_variables: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Returns the complete ordered Devnet scenario inventory for milestone 0.4.6.
|
||||
pub fn devnet_spl_validation_scenarios() -> std::vec::Vec<crate::DevnetSplValidationScenario> {
|
||||
return vec![
|
||||
public_scenario("token2022_mint_to_checked", "Token-2022 MintToChecked", kb_lib::EX_SPL_TOKEN2022_MINT_TO_CHECKED_OPERATION, &["TOKEN2022_MINT", "TOKEN2022_SOURCE", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_MINT_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_transfer_checked", "Token-2022 TransferChecked", kb_lib::EX_SPL_TOKEN2022_TRANSFER_CHECKED_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_DESTINATION", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_TRANSFER_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_approve_checked", "Token-2022 ApproveChecked", kb_lib::EX_SPL_TOKEN2022_APPROVE_CHECKED_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_DELEGATE", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_APPROVE_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_revoke", "Token-2022 Revoke", kb_lib::EX_SPL_TOKEN2022_REVOKE_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_AUTHORITY"]),
|
||||
public_scenario("token2022_burn_checked", "Token-2022 BurnChecked", kb_lib::EX_SPL_TOKEN2022_BURN_CHECKED_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_AUTHORITY", "TOKEN2022_DECIMALS", "TOKEN2022_BURN_AMOUNT_RAW"]),
|
||||
public_scenario("token2022_freeze_account", "Token-2022 FreezeAccount", kb_lib::EX_SPL_TOKEN2022_FREEZE_ACCOUNT_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_FREEZE_AUTHORITY"]),
|
||||
public_scenario("token2022_thaw_account", "Token-2022 ThawAccount", kb_lib::EX_SPL_TOKEN2022_THAW_ACCOUNT_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "TOKEN2022_FREEZE_AUTHORITY"]),
|
||||
public_scenario("token2022_close_destination", "Token-2022 CloseAccount destination", kb_lib::EX_SPL_TOKEN2022_CLOSE_ACCOUNT_OPERATION, &["TOKEN2022_DESTINATION", "KB_DEVNET_WALLET_ADDRESS", "TOKEN2022_AUTHORITY"]),
|
||||
registry_scenario("elgamal_registry_create", "ElGamal Registry CreateRegistry", "spl_elgamal_registry.create_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
|
||||
registry_scenario("elgamal_registry_update", "ElGamal Registry UpdateRegistry", "spl_elgamal_registry.update_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
|
||||
confidential_scenario("token2022_configure_confidential_account", "Token-2022 ConfigureAccountWithRegistry", kb_lib::EX_SPL_TOKEN2022_CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_OPERATION, &["TOKEN2022_SOURCE", "TOKEN2022_MINT", "ELGAMAL_REGISTRY_ADDRESS", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
|
||||
];
|
||||
}
|
||||
|
||||
fn public_scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
operation_code: &str,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return scenario(
|
||||
id,
|
||||
label,
|
||||
crate::DevnetSplValidationFamily::Token2022Public,
|
||||
operation_code,
|
||||
crate::DevnetSplValidationImplementationStatus::Executable,
|
||||
true,
|
||||
false,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
|
||||
fn registry_scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
operation_code: &str,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return scenario(
|
||||
id,
|
||||
label,
|
||||
crate::DevnetSplValidationFamily::ElGamalRegistry,
|
||||
operation_code,
|
||||
crate::DevnetSplValidationImplementationStatus::ProofFixtureRequired,
|
||||
true,
|
||||
true,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
|
||||
fn confidential_scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
operation_code: &str,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return scenario(
|
||||
id,
|
||||
label,
|
||||
crate::DevnetSplValidationFamily::Token2022Confidential,
|
||||
operation_code,
|
||||
crate::DevnetSplValidationImplementationStatus::ProofFixtureRequired,
|
||||
true,
|
||||
true,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
|
||||
fn scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
family: crate::DevnetSplValidationFamily,
|
||||
operation_code: &str,
|
||||
implementation_status: crate::DevnetSplValidationImplementationStatus,
|
||||
destructive: bool,
|
||||
proof_required: bool,
|
||||
variables: &[&str],
|
||||
) -> crate::DevnetSplValidationScenario {
|
||||
return crate::DevnetSplValidationScenario {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
family,
|
||||
operation_code: operation_code.to_string(),
|
||||
implementation_status,
|
||||
destructive,
|
||||
operator_confirmation_required: destructive,
|
||||
proof_required,
|
||||
required_fixture_variables: variables
|
||||
.iter()
|
||||
.map(|value| return (*value).to_string())
|
||||
.collect(),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn milestone_inventory_is_stable_unique_and_keeps_registry_separate() {
|
||||
let scenarios = crate::devnet_spl_validation_scenarios();
|
||||
assert_eq!(scenarios.len(), 11);
|
||||
let ids = scenarios
|
||||
.iter()
|
||||
.map(|scenario| return scenario.id.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
assert_eq!(ids.len(), scenarios.len());
|
||||
assert!(
|
||||
scenarios.iter().any(|scenario| return scenario.family
|
||||
== crate::DevnetSplValidationFamily::ElGamalRegistry)
|
||||
);
|
||||
assert!(
|
||||
scenarios
|
||||
.iter()
|
||||
.filter(|scenario| return scenario.family
|
||||
== crate::DevnetSplValidationFamily::Token2022Public)
|
||||
.all(|scenario| return !scenario.proof_required)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_mutating_scenario_requires_operator_confirmation() {
|
||||
assert!(crate::devnet_spl_validation_scenarios().iter().all(|scenario| {
|
||||
return !scenario.destructive || scenario.operator_confirmation_required;
|
||||
}));
|
||||
}
|
||||
}
|
||||
440
kb-pipeline/src/solana_token2022_validation.rs
Normal file
440
kb-pipeline/src/solana_token2022_validation.rs
Normal file
@@ -0,0 +1,440 @@
|
||||
// file: kb-pipeline/src/solana_token2022_validation.rs
|
||||
// version: 4
|
||||
|
||||
//! Machine-readable validation evidence contract for the Token-2022 milestone.
|
||||
|
||||
/// Maximum number of evidence records accepted in one validation report.
|
||||
pub const MAX_TOKEN2022_VALIDATION_EVIDENCE: usize = 64;
|
||||
|
||||
/// Required validation environment for one scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Token2022ValidationEnvironment {
|
||||
/// Deterministic offline fixtures and builder comparisons.
|
||||
Offline,
|
||||
/// A local validator under operator control.
|
||||
Localnet,
|
||||
/// Solana Devnet.
|
||||
Devnet,
|
||||
/// Scenario may run on Localnet or Devnet according to deployment availability.
|
||||
LocalnetOrDevnet,
|
||||
/// Mainnet observations without mutable execution.
|
||||
MainnetObservation,
|
||||
/// PostgreSQL persistence and replay validation.
|
||||
Postgres,
|
||||
/// Tauri application smoke validation.
|
||||
Tauri,
|
||||
}
|
||||
|
||||
/// Exact status of one validation scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Token2022ValidationStatus {
|
||||
/// Scenario is declared but has not been run.
|
||||
NotRun,
|
||||
/// Scenario was simulated without submission.
|
||||
Simulated,
|
||||
/// Scenario was submitted but confirmation evidence is incomplete.
|
||||
Submitted,
|
||||
/// Scenario was confirmed and its required postconditions passed.
|
||||
Confirmed,
|
||||
/// Scenario is not available in the selected environment.
|
||||
Unavailable,
|
||||
/// Scenario ran and failed.
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// One bounded proof attached to a validation scenario.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ValidationEvidence {
|
||||
/// Stable evidence kind such as `signature`, `test_suite`, or `replay`.
|
||||
pub kind: std::string::String,
|
||||
/// Bounded evidence value.
|
||||
pub value: std::string::String,
|
||||
}
|
||||
|
||||
/// One declared Token-2022 validation scenario and its observed evidence.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ValidationScenario {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Validation environment.
|
||||
pub environment: crate::Token2022ValidationEnvironment,
|
||||
/// Exact observed status.
|
||||
pub status: crate::Token2022ValidationStatus,
|
||||
/// Whether canonical hydration is required.
|
||||
pub requires_canonical_hydration: bool,
|
||||
/// Whether core extraction is required.
|
||||
pub requires_core_extraction: bool,
|
||||
/// Whether decode replay is required.
|
||||
pub requires_decode_replay: bool,
|
||||
/// Whether materialization is required.
|
||||
pub requires_materialization: bool,
|
||||
/// Whether a second idempotent replay is required.
|
||||
pub requires_second_replay: bool,
|
||||
/// Ordered evidence records.
|
||||
pub evidence: std::vec::Vec<crate::Token2022ValidationEvidence>,
|
||||
}
|
||||
|
||||
/// Machine-readable validation matrix loaded from the canonical JSON document.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token2022ValidationMatrix {
|
||||
/// Matrix schema version.
|
||||
pub matrix_version: u32,
|
||||
/// Milestone owning this matrix.
|
||||
pub milestone: std::string::String,
|
||||
/// Aggregate matrix status.
|
||||
pub status: std::string::String,
|
||||
/// Exact accepted status vocabulary.
|
||||
pub status_vocabulary: std::vec::Vec<std::string::String>,
|
||||
/// Scenarios in stable roadmap order.
|
||||
pub scenarios: std::vec::Vec<crate::Token2022ValidationMatrixScenario>,
|
||||
}
|
||||
|
||||
/// One scenario declared by the canonical validation matrix.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token2022ValidationMatrixScenario {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Required execution environment.
|
||||
pub environment: crate::Token2022ValidationEnvironment,
|
||||
/// Exact observed status.
|
||||
pub status: crate::Token2022ValidationStatus,
|
||||
/// Evidence kinds required before this scenario may be confirmed.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
/// Observed bounded evidence.
|
||||
#[serde(default)]
|
||||
pub evidence: std::vec::Vec<crate::Token2022ValidationEvidence>,
|
||||
}
|
||||
|
||||
/// Loads and validates the canonical Token-2022 validation matrix.
|
||||
pub fn load_token2022_validation_matrix() -> kb_core::Result<crate::Token2022ValidationMatrix> {
|
||||
let parsed = match serde_json::from_str::<crate::Token2022ValidationMatrix>(include_str!(
|
||||
"../../docs/SPL_TOKEN2022_VALIDATION_MATRIX.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_invalid_json",
|
||||
format!("Token-2022 validation matrix JSON is invalid: {error}"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = validate_token2022_validation_matrix(&parsed) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
/// Validates schema, scenario inventory, statuses, and observed evidence.
|
||||
pub fn validate_token2022_validation_matrix(
|
||||
matrix: &crate::Token2022ValidationMatrix,
|
||||
) -> kb_core::Result<()> {
|
||||
if matrix.matrix_version != 2 || matrix.milestone != "0.4.6" {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_contract_mismatch",
|
||||
"Token-2022 validation matrix must use version 2 for milestone 0.4.6",
|
||||
));
|
||||
}
|
||||
let expected_statuses =
|
||||
["not_run", "simulated", "submitted", "confirmed", "unavailable", "failed"];
|
||||
let actual_statuses = matrix
|
||||
.status_vocabulary
|
||||
.iter()
|
||||
.map(|status| return status.as_str())
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
if actual_statuses.as_slice() != expected_statuses {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_status_vocabulary_mismatch",
|
||||
"Token-2022 validation status vocabulary differs from the compiled contract",
|
||||
));
|
||||
}
|
||||
let expected_ids = [
|
||||
"offline_full_regression",
|
||||
"token2022_public_devnet",
|
||||
"elgamal_registry_devnet",
|
||||
"confidential_transfer_localnet_or_devnet",
|
||||
"confidential_mint_burn_localnet_or_devnet",
|
||||
"permissioned_confidential_burn_localnet_or_devnet",
|
||||
"mainnet_observation_corpus",
|
||||
"postgres_double_replay",
|
||||
"tauri_smoke",
|
||||
];
|
||||
let actual_ids = matrix
|
||||
.scenarios
|
||||
.iter()
|
||||
.map(|scenario| return scenario.id.as_str())
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
if actual_ids.as_slice() != expected_ids {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_scenario_inventory_mismatch",
|
||||
"Token-2022 validation scenario inventory or order differs from the compiled contract",
|
||||
));
|
||||
}
|
||||
for scenario in &matrix.scenarios {
|
||||
if scenario.required_evidence.is_empty()
|
||||
|| scenario.required_evidence.len() > crate::MAX_TOKEN2022_VALIDATION_EVIDENCE
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_required_evidence_invalid",
|
||||
"Every Token-2022 validation scenario must declare bounded required evidence",
|
||||
));
|
||||
}
|
||||
let mut required = std::collections::BTreeSet::<&str>::new();
|
||||
for evidence_kind in &scenario.required_evidence {
|
||||
if evidence_kind.trim().is_empty() || !required.insert(evidence_kind.as_str()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_required_evidence_invalid",
|
||||
"Required Token-2022 validation evidence must be non-empty and unique",
|
||||
));
|
||||
}
|
||||
}
|
||||
if scenario.status == crate::Token2022ValidationStatus::Confirmed {
|
||||
let observed = scenario
|
||||
.evidence
|
||||
.iter()
|
||||
.map(|evidence| return evidence.kind.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
if !required.iter().all(|kind| return observed.contains(kind)) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_confirmed_without_required_evidence",
|
||||
format!(
|
||||
"Confirmed Token-2022 validation scenario {} lacks required evidence",
|
||||
scenario.id
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if matches!(
|
||||
scenario.status,
|
||||
crate::Token2022ValidationStatus::NotRun
|
||||
| crate::Token2022ValidationStatus::Unavailable
|
||||
) && !scenario.evidence.is_empty()
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_matrix_unobserved_with_evidence",
|
||||
"Not-run or unavailable Token-2022 scenarios must not retain observed evidence",
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Complete validation report checked before milestone closure.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022ValidationReport {
|
||||
/// Scenarios in stable roadmap order.
|
||||
pub scenarios: std::vec::Vec<crate::Token2022ValidationScenario>,
|
||||
}
|
||||
|
||||
/// Validates that a milestone report is bounded, unique, and does not overclaim evidence.
|
||||
pub fn validate_token2022_validation_report(
|
||||
report: &crate::Token2022ValidationReport,
|
||||
) -> kb_core::Result<()> {
|
||||
if report.scenarios.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 validation report must contain at least one scenario",
|
||||
));
|
||||
}
|
||||
let mut ids = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for scenario in &report.scenarios {
|
||||
if scenario.id.trim().is_empty() || !ids.insert(scenario.id.clone()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_scenario_identity_invalid",
|
||||
"Token-2022 validation scenario ids must be non-empty and unique",
|
||||
));
|
||||
}
|
||||
if scenario.evidence.len() > crate::MAX_TOKEN2022_VALIDATION_EVIDENCE {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_evidence_limit_exceeded",
|
||||
format!(
|
||||
"Token-2022 validation accepts at most {} evidence records per scenario",
|
||||
crate::MAX_TOKEN2022_VALIDATION_EVIDENCE
|
||||
),
|
||||
));
|
||||
}
|
||||
for evidence in &scenario.evidence {
|
||||
if evidence.kind.trim().is_empty() || evidence.value.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_evidence_invalid",
|
||||
"Token-2022 validation evidence kind and value must be non-empty",
|
||||
));
|
||||
}
|
||||
}
|
||||
let completed = matches!(
|
||||
scenario.status,
|
||||
crate::Token2022ValidationStatus::Simulated
|
||||
| crate::Token2022ValidationStatus::Submitted
|
||||
| crate::Token2022ValidationStatus::Confirmed
|
||||
| crate::Token2022ValidationStatus::Failed
|
||||
);
|
||||
if completed && scenario.evidence.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_completed_without_evidence",
|
||||
"A completed Token-2022 validation scenario must retain evidence",
|
||||
));
|
||||
}
|
||||
if scenario.status == crate::Token2022ValidationStatus::Confirmed
|
||||
&& (scenario.requires_canonical_hydration
|
||||
|| scenario.requires_core_extraction
|
||||
|| scenario.requires_decode_replay
|
||||
|| scenario.requires_materialization
|
||||
|| scenario.requires_second_replay)
|
||||
&& !has_pipeline_evidence(scenario)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token2022_validation_confirmed_without_pipeline_evidence",
|
||||
"A confirmed end-to-end Token-2022 scenario must retain hydration, extraction, replay, materialization, and idempotence evidence",
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn has_pipeline_evidence(scenario: &crate::Token2022ValidationScenario) -> bool {
|
||||
let kinds = scenario
|
||||
.evidence
|
||||
.iter()
|
||||
.map(|evidence| return evidence.kind.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
if scenario.requires_canonical_hydration && !kinds.contains("canonical_hydration") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_core_extraction && !kinds.contains("core_extraction") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_decode_replay && !kinds.contains("decode_replay") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_materialization && !kinds.contains("materialization") {
|
||||
return false;
|
||||
}
|
||||
if scenario.requires_second_replay && !kinds.contains("second_replay") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn scenario(status: crate::Token2022ValidationStatus) -> crate::Token2022ValidationScenario {
|
||||
return crate::Token2022ValidationScenario {
|
||||
id: "confidential_transfer_devnet".to_string(),
|
||||
environment: crate::Token2022ValidationEnvironment::Devnet,
|
||||
status,
|
||||
requires_canonical_hydration: true,
|
||||
requires_core_extraction: true,
|
||||
requires_decode_replay: true,
|
||||
requires_materialization: true,
|
||||
requires_second_replay: true,
|
||||
evidence: vec![
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "signature".to_string(),
|
||||
value: "signature".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "canonical_hydration".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "core_extraction".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "decode_replay".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "materialization".to_string(),
|
||||
value: "1".to_string(),
|
||||
},
|
||||
crate::Token2022ValidationEvidence {
|
||||
kind: "second_replay".to_string(),
|
||||
value: "0_new_outputs".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_end_to_end_scenario_requires_complete_pipeline_evidence() {
|
||||
let report = crate::Token2022ValidationReport {
|
||||
scenarios: vec![scenario(crate::Token2022ValidationStatus::Confirmed)],
|
||||
};
|
||||
assert!(crate::validate_token2022_validation_report(&report).is_ok());
|
||||
let mut incomplete = scenario(crate::Token2022ValidationStatus::Confirmed);
|
||||
incomplete.evidence.retain(|evidence| return evidence.kind != "second_replay");
|
||||
assert!(
|
||||
crate::validate_token2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![incomplete]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_run_and_unavailable_scenarios_do_not_invent_evidence() {
|
||||
let mut not_run = scenario(crate::Token2022ValidationStatus::NotRun);
|
||||
not_run.evidence.clear();
|
||||
let mut unavailable = scenario(crate::Token2022ValidationStatus::Unavailable);
|
||||
unavailable.id = "permissioned_burn_devnet".to_string();
|
||||
unavailable.evidence.clear();
|
||||
assert!(
|
||||
crate::validate_token2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![not_run, unavailable]
|
||||
})
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_ids_empty_evidence_and_completed_without_evidence_fail_closed() {
|
||||
let first = scenario(crate::Token2022ValidationStatus::Confirmed);
|
||||
let duplicate = first.clone();
|
||||
assert!(
|
||||
crate::validate_token2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![first, duplicate]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let mut failed = scenario(crate::Token2022ValidationStatus::Failed);
|
||||
failed.evidence.clear();
|
||||
assert!(
|
||||
crate::validate_token2022_validation_report(&crate::Token2022ValidationReport {
|
||||
scenarios: vec![failed]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_matrix_matches_compiled_inventory_and_observed_offline_evidence() {
|
||||
let matrix = crate::load_token2022_validation_matrix();
|
||||
assert!(matrix.is_ok());
|
||||
let matrix = match matrix {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected matrix error: {error}"),
|
||||
};
|
||||
assert_eq!(matrix.scenarios.len(), 9);
|
||||
assert_eq!(matrix.scenarios[0].status, crate::Token2022ValidationStatus::Confirmed);
|
||||
assert_eq!(matrix.scenarios[0].evidence.len(), 4);
|
||||
assert!(
|
||||
matrix.scenarios[1..]
|
||||
.iter()
|
||||
.all(|scenario| return scenario.status == crate::Token2022ValidationStatus::NotRun)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_matrix_scenario_requires_every_declared_evidence_kind() {
|
||||
let matrix = crate::load_token2022_validation_matrix();
|
||||
assert!(matrix.is_ok());
|
||||
let mut matrix = match matrix {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected matrix error: {error}"),
|
||||
};
|
||||
matrix.scenarios[0].evidence.retain(|evidence| return evidence.kind != "clippy");
|
||||
assert!(crate::validate_token2022_validation_matrix(&matrix).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user