|
|
|
|
@@ -0,0 +1,993 @@
|
|
|
|
|
// file: kb_materializer_lifecycle/src/materializer.rs
|
|
|
|
|
// version: 13
|
|
|
|
|
|
|
|
|
|
//! Stable lifecycle projections derived from exact decoded observations.
|
|
|
|
|
|
|
|
|
|
const ACCEPTED_FAMILIES: &[kb_model::EventFamily] = &[
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
kb_model::EventFamily::Admin,
|
|
|
|
|
kb_model::EventFamily::Audit,
|
|
|
|
|
];
|
|
|
|
|
const ADDRESS_LOOKUP_TABLE_SURFACE: &str = "solana_native_address_lookup_table";
|
|
|
|
|
const ADDRESS_LOOKUP_TABLE_ENTRIES: &[&str] = &[
|
|
|
|
|
"create_lookup_table",
|
|
|
|
|
"freeze_lookup_table",
|
|
|
|
|
"extend_lookup_table",
|
|
|
|
|
"deactivate_lookup_table",
|
|
|
|
|
"close_lookup_table",
|
|
|
|
|
];
|
|
|
|
|
const BPF_LOADER_DEPRECATED_SURFACE: &str = "solana_native_bpf_loader_deprecated";
|
|
|
|
|
const BPF_LOADER_SURFACE: &str = "solana_native_bpf_loader";
|
|
|
|
|
const BPF_LOADER_UPGRADEABLE_SURFACE: &str = "solana_native_bpf_loader_upgradeable";
|
|
|
|
|
const LOADER_V4_SURFACE: &str = "solana_native_loader_v4";
|
|
|
|
|
const FEATURE_SURFACE: &str = "solana_native_feature";
|
|
|
|
|
const FEATURE_ENTRIES: &[&str] = &["revoke_pending_activation"];
|
|
|
|
|
const IMMUTABLE_LOADER_ENTRIES: &[&str] = &["finalize"];
|
|
|
|
|
const UPGRADEABLE_LOADER_ENTRIES: &[&str] = &[
|
|
|
|
|
"initialize_buffer",
|
|
|
|
|
"deploy_with_max_data_len",
|
|
|
|
|
"upgrade",
|
|
|
|
|
"close",
|
|
|
|
|
"extend_program",
|
|
|
|
|
];
|
|
|
|
|
const LOADER_V4_ENTRIES: &[&str] = &["set_program_length", "deploy", "retract", "finalize"];
|
|
|
|
|
const SLASHING_SURFACE: &str = "solana_native_slashing";
|
|
|
|
|
const SLASHING_ENTRIES: &[&str] = &["close_violation_report", "duplicate_block_proof"];
|
|
|
|
|
const SYSTEM_SURFACE: &str = "solana_native_system";
|
|
|
|
|
const SYSTEM_ACCOUNT_ENTRIES: &[&str] = &[
|
|
|
|
|
"create_account",
|
|
|
|
|
"create_account_with_seed",
|
|
|
|
|
"allocate",
|
|
|
|
|
"allocate_with_seed",
|
|
|
|
|
"create_account_allow_prefund",
|
|
|
|
|
];
|
|
|
|
|
const SYSTEM_NONCE_ENTRIES: &[&str] = &[
|
|
|
|
|
"advance_nonce_account",
|
|
|
|
|
"withdraw_nonce_account",
|
|
|
|
|
"initialize_nonce_account",
|
|
|
|
|
"authorize_nonce_account",
|
|
|
|
|
"upgrade_nonce_account",
|
|
|
|
|
];
|
|
|
|
|
const ZK_ELGAMAL_PROOF_SURFACE: &str = "solana_native_zk_elgamal_proof";
|
|
|
|
|
const ZK_ELGAMAL_CLOSE_ENTRIES: &[&str] = &["close_context_state"];
|
|
|
|
|
const ZK_ELGAMAL_VERIFY_ENTRIES: &[&str] = &[
|
|
|
|
|
"verify_zero_ciphertext",
|
|
|
|
|
"verify_ciphertext_ciphertext_equality",
|
|
|
|
|
"verify_ciphertext_commitment_equality",
|
|
|
|
|
"verify_pubkey_validity",
|
|
|
|
|
"verify_percentage_with_cap",
|
|
|
|
|
"verify_batched_range_proof_u64",
|
|
|
|
|
"verify_batched_range_proof_u128",
|
|
|
|
|
"verify_batched_range_proof_u256",
|
|
|
|
|
"verify_grouped_ciphertext_2_handles_validity",
|
|
|
|
|
"verify_batched_grouped_ciphertext_2_handles_validity",
|
|
|
|
|
"verify_grouped_ciphertext_3_handles_validity",
|
|
|
|
|
"verify_batched_grouped_ciphertext_3_handles_validity",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
enum ProjectionKind {
|
|
|
|
|
AddressLookupTable,
|
|
|
|
|
FeatureGate,
|
|
|
|
|
ProgramLoader(&'static str),
|
|
|
|
|
SlashingViolationReportInitialization,
|
|
|
|
|
SlashingViolationReportClosure,
|
|
|
|
|
SystemAccountLifecycle,
|
|
|
|
|
DurableNonceAccount,
|
|
|
|
|
ZkProofContextInitialization,
|
|
|
|
|
ZkProofContextClosure,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProjectionKind {
|
|
|
|
|
fn domain(self) -> &'static str {
|
|
|
|
|
return match self {
|
|
|
|
|
Self::AddressLookupTable => "address_lookup_table",
|
|
|
|
|
Self::FeatureGate => "feature_gate",
|
|
|
|
|
Self::ProgramLoader(_) => "program_loader",
|
|
|
|
|
Self::SlashingViolationReportInitialization | Self::SlashingViolationReportClosure => {
|
|
|
|
|
"slashing_violation_report"
|
|
|
|
|
},
|
|
|
|
|
Self::SystemAccountLifecycle => "system_account",
|
|
|
|
|
Self::DurableNonceAccount => "durable_nonce_account",
|
|
|
|
|
Self::ZkProofContextInitialization | Self::ZkProofContextClosure => "zk_proof_context",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn loader_surface(self) -> std::option::Option<&'static str> {
|
|
|
|
|
return match self {
|
|
|
|
|
Self::ProgramLoader(surface) => std::option::Option::Some(surface),
|
|
|
|
|
_ => std::option::Option::None,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn output_key(self, surface_code: &str, operation: &str) -> std::string::String {
|
|
|
|
|
return match self {
|
|
|
|
|
Self::AddressLookupTable => format!("address_lookup_table:{operation}:0"),
|
|
|
|
|
Self::FeatureGate => format!("feature_gate:{operation}:0"),
|
|
|
|
|
Self::ProgramLoader(_) => {
|
|
|
|
|
format!("program_loader:{surface_code}:{operation}:0")
|
|
|
|
|
},
|
|
|
|
|
Self::SlashingViolationReportInitialization | Self::SlashingViolationReportClosure => {
|
|
|
|
|
format!("slashing_violation_report:{operation}:0")
|
|
|
|
|
},
|
|
|
|
|
Self::SystemAccountLifecycle => format!("system_account:{operation}:0"),
|
|
|
|
|
Self::DurableNonceAccount => format!("durable_nonce_account:{operation}:0"),
|
|
|
|
|
Self::ZkProofContextInitialization | Self::ZkProofContextClosure => {
|
|
|
|
|
format!("zk_proof_context:{operation}:0")
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Stable lifecycle materializer for supported native lifecycle projections.
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
|
pub struct LifecycleMaterializer;
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::Materializer for crate::LifecycleMaterializer {
|
|
|
|
|
fn materializer_name(&self) -> &'static str {
|
|
|
|
|
return "kb_materializer_lifecycle";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materializer_version(&self) -> &'static str {
|
|
|
|
|
return env!("CARGO_PKG_VERSION");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_event(&self, event: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return family_is_accepted(event.event_family)
|
|
|
|
|
&& static_projection(event.surface_code.0.as_str(), event.event_name.0.as_str())
|
|
|
|
|
.is_some();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materialize_event(
|
|
|
|
|
&self,
|
|
|
|
|
event: &kb_model::DecodedProtocolEvent,
|
|
|
|
|
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>> {
|
|
|
|
|
if !kb_materializer_api::Materializer::accepts_event(self, event) {
|
|
|
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
|
|
|
}
|
|
|
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::EventMaterializer for crate::LifecycleMaterializer {
|
|
|
|
|
fn identity(&self) -> kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
return kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
name: "solana_native_lifecycle".to_string(),
|
|
|
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepted_families(&self) -> &'static [kb_model::EventFamily] {
|
|
|
|
|
return ACCEPTED_FAMILIES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_observation(&self, observation: &kb_decoder_api::DecodedObservation) -> bool {
|
|
|
|
|
return family_is_accepted(observation.event.event_family)
|
|
|
|
|
&& observation_projection(observation).is_some();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn transaction_policy(
|
|
|
|
|
&self,
|
|
|
|
|
_family: kb_model::EventFamily,
|
|
|
|
|
) -> kb_materializer_api::MaterializationTransactionPolicy {
|
|
|
|
|
return kb_materializer_api::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materialize(
|
|
|
|
|
&self,
|
|
|
|
|
observation: &kb_decoder_api::DecodedObservation,
|
|
|
|
|
) -> kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
let projection = match observation_projection(observation) {
|
|
|
|
|
std::option::Option::Some(value) => value,
|
|
|
|
|
std::option::Option::None => {
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
target: crate::TRACING_TARGET,
|
|
|
|
|
action = "materialize_lifecycle",
|
|
|
|
|
surface_code = %observation.event.surface_code.0.as_str(),
|
|
|
|
|
entry_code = %observation.event.event_name.0.as_str(),
|
|
|
|
|
accepted = false,
|
|
|
|
|
"ignore observation outside supported native lifecycle projections"
|
|
|
|
|
);
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::ignored();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if observation.transaction_failed || !observation.observation_committed {
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
target: crate::TRACING_TARGET,
|
|
|
|
|
action = "materialize_lifecycle",
|
|
|
|
|
surface_code = %observation.event.surface_code.0.as_str(),
|
|
|
|
|
entry_code = %observation.event.event_name.0.as_str(),
|
|
|
|
|
accepted = true,
|
|
|
|
|
committed = observation.observation_committed,
|
|
|
|
|
transaction_failed = observation.transaction_failed,
|
|
|
|
|
"refuse uncommitted native lifecycle observation"
|
|
|
|
|
);
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::refused(
|
|
|
|
|
"failed_transaction_lifecycle_refused",
|
|
|
|
|
"failed or uncommitted native lifecycle observations cannot create lifecycle outputs",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let accounts = observation
|
|
|
|
|
.payload_json
|
|
|
|
|
.get("accounts")
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or(serde_json::Value::Null);
|
|
|
|
|
let parameters = observation
|
|
|
|
|
.payload_json
|
|
|
|
|
.get("parameters")
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or(serde_json::Value::Null);
|
|
|
|
|
let operation = observation.event.event_name.0.clone();
|
|
|
|
|
let output_key =
|
|
|
|
|
projection.output_key(observation.event.surface_code.0.as_str(), operation.as_str());
|
|
|
|
|
let domain = projection.domain();
|
|
|
|
|
let loader_surface = projection.loader_surface();
|
|
|
|
|
let projection_details =
|
|
|
|
|
projection_details(projection, &accounts, ¶meters, operation.as_str());
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
target: crate::TRACING_TARGET,
|
|
|
|
|
action = "materialize_lifecycle",
|
|
|
|
|
surface_code = %observation.event.surface_code.0.as_str(),
|
|
|
|
|
entry_code = %observation.event.event_name.0.as_str(),
|
|
|
|
|
projection_domain = domain,
|
|
|
|
|
accepted = true,
|
|
|
|
|
committed = true,
|
|
|
|
|
output_count = 1_usize,
|
|
|
|
|
"materialize committed native lifecycle observation"
|
|
|
|
|
);
|
|
|
|
|
let output = kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key,
|
|
|
|
|
family: kb_model::MaterializedEventFamily::Lifecycle,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"projectionVersion": 1,
|
|
|
|
|
"projectionSemantics": "committed_instruction_lifecycle_event",
|
|
|
|
|
"domain": domain,
|
|
|
|
|
"loaderSurface": loader_surface,
|
|
|
|
|
"operation": operation,
|
|
|
|
|
"programId": observation.event.program_id.0.clone(),
|
|
|
|
|
"signature": observation.event.signature.0.clone(),
|
|
|
|
|
"slot": observation.event.slot.0,
|
|
|
|
|
"instructionPath": observation.event.instruction_path.0.clone(),
|
|
|
|
|
"transactionSucceeded": true,
|
|
|
|
|
"accounts": accounts,
|
|
|
|
|
"parameters": parameters,
|
|
|
|
|
"projection": projection_details,
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
status: kb_materializer_api::MaterializerOutcomeStatus::Inserted,
|
|
|
|
|
outputs: std::vec![output],
|
|
|
|
|
diagnostics: std::vec::Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn family_is_accepted(family: kb_model::EventFamily) -> bool {
|
|
|
|
|
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn observation_projection(
|
|
|
|
|
observation: &kb_decoder_api::DecodedObservation,
|
|
|
|
|
) -> std::option::Option<ProjectionKind> {
|
|
|
|
|
let surface_code = observation.event.surface_code.0.as_str();
|
|
|
|
|
let entry_code = observation.event.event_name.0.as_str();
|
|
|
|
|
let static_projection = static_projection(surface_code, entry_code);
|
|
|
|
|
if static_projection.is_some() {
|
|
|
|
|
return static_projection;
|
|
|
|
|
}
|
|
|
|
|
if surface_code == ZK_ELGAMAL_PROOF_SURFACE
|
|
|
|
|
&& contains(ZK_ELGAMAL_VERIFY_ENTRIES, entry_code)
|
|
|
|
|
&& observation
|
|
|
|
|
.payload_json
|
|
|
|
|
.get("parameters")
|
|
|
|
|
.and_then(|parameters| return parameters.get("contextStateRequested"))
|
|
|
|
|
.and_then(serde_json::Value::as_bool)
|
|
|
|
|
== std::option::Option::Some(true)
|
|
|
|
|
{
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::ZkProofContextInitialization);
|
|
|
|
|
}
|
|
|
|
|
return std::option::Option::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn static_projection(surface_code: &str, entry_code: &str) -> std::option::Option<ProjectionKind> {
|
|
|
|
|
if surface_code == FEATURE_SURFACE && contains(FEATURE_ENTRIES, entry_code) {
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::FeatureGate);
|
|
|
|
|
}
|
|
|
|
|
if surface_code == ADDRESS_LOOKUP_TABLE_SURFACE
|
|
|
|
|
&& contains(ADDRESS_LOOKUP_TABLE_ENTRIES, entry_code)
|
|
|
|
|
{
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::AddressLookupTable);
|
|
|
|
|
}
|
|
|
|
|
if (surface_code == BPF_LOADER_DEPRECATED_SURFACE || surface_code == BPF_LOADER_SURFACE)
|
|
|
|
|
&& contains(IMMUTABLE_LOADER_ENTRIES, entry_code)
|
|
|
|
|
{
|
|
|
|
|
let loader_surface = if surface_code == BPF_LOADER_DEPRECATED_SURFACE {
|
|
|
|
|
BPF_LOADER_DEPRECATED_SURFACE
|
|
|
|
|
} else {
|
|
|
|
|
BPF_LOADER_SURFACE
|
|
|
|
|
};
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::ProgramLoader(loader_surface));
|
|
|
|
|
}
|
|
|
|
|
if surface_code == BPF_LOADER_UPGRADEABLE_SURFACE
|
|
|
|
|
&& contains(UPGRADEABLE_LOADER_ENTRIES, entry_code)
|
|
|
|
|
{
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::ProgramLoader(
|
|
|
|
|
BPF_LOADER_UPGRADEABLE_SURFACE,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if surface_code == LOADER_V4_SURFACE && contains(LOADER_V4_ENTRIES, entry_code) {
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::ProgramLoader(LOADER_V4_SURFACE));
|
|
|
|
|
}
|
|
|
|
|
if surface_code == SLASHING_SURFACE && contains(SLASHING_ENTRIES, entry_code) {
|
|
|
|
|
return if entry_code == "duplicate_block_proof" {
|
|
|
|
|
std::option::Option::Some(ProjectionKind::SlashingViolationReportInitialization)
|
|
|
|
|
} else {
|
|
|
|
|
std::option::Option::Some(ProjectionKind::SlashingViolationReportClosure)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if surface_code == SYSTEM_SURFACE && contains(SYSTEM_ACCOUNT_ENTRIES, entry_code) {
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::SystemAccountLifecycle);
|
|
|
|
|
}
|
|
|
|
|
if surface_code == SYSTEM_SURFACE && contains(SYSTEM_NONCE_ENTRIES, entry_code) {
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::DurableNonceAccount);
|
|
|
|
|
}
|
|
|
|
|
if surface_code == ZK_ELGAMAL_PROOF_SURFACE && contains(ZK_ELGAMAL_CLOSE_ENTRIES, entry_code) {
|
|
|
|
|
return std::option::Option::Some(ProjectionKind::ZkProofContextClosure);
|
|
|
|
|
}
|
|
|
|
|
return std::option::Option::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn projection_details(
|
|
|
|
|
projection: ProjectionKind,
|
|
|
|
|
accounts: &serde_json::Value,
|
|
|
|
|
parameters: &serde_json::Value,
|
|
|
|
|
operation: &str,
|
|
|
|
|
) -> serde_json::Value {
|
|
|
|
|
return match projection {
|
|
|
|
|
ProjectionKind::SlashingViolationReportInitialization => {
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"targetAccount": account_key_for_role(accounts, "violation_report"),
|
|
|
|
|
"proofAccount": account_key_for_role(accounts, "proof_account"),
|
|
|
|
|
"violationSlot": parameters.get("violationSlot").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"nodePubkey": parameters.get("nodePubkey").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"reporter": parameters.get("reporter").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"lamportDestination": parameters.get("destination").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"stateTransition": "pda_assigned_allocated_and_data_stored_after_successful_duplicate_block_proof_acceptance",
|
|
|
|
|
"proofMaterialized": false,
|
|
|
|
|
"penaltyAppliedByProgram": false,
|
|
|
|
|
"enforcementSemantics": "violation_report_only_external_consensus_enforcement",
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
ProjectionKind::SlashingViolationReportClosure => serde_json::json!({
|
|
|
|
|
"targetAccount": account_key_for_role(accounts, "violation_report"),
|
|
|
|
|
"lamportDestination": account_key_for_role(accounts, "destination"),
|
|
|
|
|
"stateTransition": "closed_after_minimum_retention_lamports_reclaimed_owner_reset_to_system_program",
|
|
|
|
|
"minimumRetentionEpochs": 3,
|
|
|
|
|
"transactionFinalAccountStateCaptured": false,
|
|
|
|
|
}),
|
|
|
|
|
ProjectionKind::SystemAccountLifecycle => serde_json::json!({
|
|
|
|
|
"targetAccount": first_account_key_for_roles(
|
|
|
|
|
accounts,
|
|
|
|
|
&["new_account", "allocated_account", "derived_account"],
|
|
|
|
|
),
|
|
|
|
|
"fundingAccount": account_key_for_role(accounts, "funding_account"),
|
|
|
|
|
"baseAccount": account_key_for_role(accounts, "base_account"),
|
|
|
|
|
"requestedLamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"requestedSpace": parameters.get("space").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"requestedOwner": parameters.get("owner").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"seed": parameters.get("seed").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"stateTransition": system_account_transition(operation),
|
|
|
|
|
"lamportTransferRepresentedByCoreBalanceChanges": operation == "create_account"
|
|
|
|
|
|| operation == "create_account_with_seed"
|
|
|
|
|
|| operation == "create_account_allow_prefund",
|
|
|
|
|
"transactionFinalAccountStateCaptured": false,
|
|
|
|
|
}),
|
|
|
|
|
ProjectionKind::DurableNonceAccount => serde_json::json!({
|
|
|
|
|
"targetAccount": account_key_for_role(accounts, "nonce_account"),
|
|
|
|
|
"authorityAccount": account_key_for_role(accounts, "nonce_authority"),
|
|
|
|
|
"lamportDestination": account_key_for_role(accounts, "recipient_account"),
|
|
|
|
|
"stateTransition": nonce_transition(operation),
|
|
|
|
|
"newAuthority": parameters.get("authority").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"withdrawLamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"closureSemantics": if operation == "withdraw_nonce_account" {
|
|
|
|
|
serde_json::Value::String("closes_when_entire_balance_is_withdrawn".to_string())
|
|
|
|
|
} else {
|
|
|
|
|
serde_json::Value::Null
|
|
|
|
|
},
|
|
|
|
|
"transactionFinalAccountStateCaptured": false,
|
|
|
|
|
}),
|
|
|
|
|
ProjectionKind::ZkProofContextInitialization => serde_json::json!({
|
|
|
|
|
"targetAccount": account_key_for_role(accounts, "proof_context_state"),
|
|
|
|
|
"contextStateAuthority": account_key_for_role(accounts, "context_state_authority"),
|
|
|
|
|
"proofType": parameters.get("proofType").cloned().unwrap_or(serde_json::Value::Null),
|
|
|
|
|
"stateTransition": "initialized_after_successful_runtime_verification",
|
|
|
|
|
"proofMaterialized": false,
|
|
|
|
|
}),
|
|
|
|
|
ProjectionKind::ZkProofContextClosure => serde_json::json!({
|
|
|
|
|
"targetAccount": account_key_for_role(accounts, "proof_context_state"),
|
|
|
|
|
"lamportDestination": account_key_for_role(accounts, "lamport_destination"),
|
|
|
|
|
"contextStateAuthority": account_key_for_role(accounts, "context_state_authority"),
|
|
|
|
|
"stateTransition": "closed_lamports_reclaimed_owner_reset_to_system_program",
|
|
|
|
|
"proofMaterialized": false,
|
|
|
|
|
}),
|
|
|
|
|
ProjectionKind::AddressLookupTable
|
|
|
|
|
| ProjectionKind::FeatureGate
|
|
|
|
|
| ProjectionKind::ProgramLoader(_) => serde_json::json!({
|
|
|
|
|
"stateTransition": operation,
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn system_account_transition(operation: &str) -> &'static str {
|
|
|
|
|
return match operation {
|
|
|
|
|
"create_account" => "created_allocated_funded_and_assigned",
|
|
|
|
|
"create_account_with_seed" => "derived_account_created_allocated_funded_and_assigned",
|
|
|
|
|
"create_account_allow_prefund" => "prefunded_account_initialized_allocated_and_assigned",
|
|
|
|
|
"allocate" => "account_data_space_allocated",
|
|
|
|
|
"allocate_with_seed" => "derived_account_data_space_allocated",
|
|
|
|
|
_ => "unknown",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn nonce_transition(operation: &str) -> &'static str {
|
|
|
|
|
return match operation {
|
|
|
|
|
"initialize_nonce_account" => "initialized",
|
|
|
|
|
"advance_nonce_account" => "nonce_advanced",
|
|
|
|
|
"authorize_nonce_account" => "authority_changed",
|
|
|
|
|
"upgrade_nonce_account" => "legacy_version_upgraded",
|
|
|
|
|
"withdraw_nonce_account" => "lamports_withdrawn_possible_closure",
|
|
|
|
|
_ => "unknown",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn first_account_key_for_roles(accounts: &serde_json::Value, roles: &[&str]) -> serde_json::Value {
|
|
|
|
|
for role in roles {
|
|
|
|
|
let value = account_key_for_role(accounts, role);
|
|
|
|
|
if !value.is_null() {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return serde_json::Value::Null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
|
|
|
|
|
let account_key = accounts.as_array().and_then(|values| {
|
|
|
|
|
return values.iter().find_map(|value| {
|
|
|
|
|
if value.get("role").and_then(serde_json::Value::as_str)
|
|
|
|
|
!= std::option::Option::Some(role)
|
|
|
|
|
{
|
|
|
|
|
return std::option::Option::None;
|
|
|
|
|
}
|
|
|
|
|
return value
|
|
|
|
|
.get("accountKey")
|
|
|
|
|
.and_then(serde_json::Value::as_str)
|
|
|
|
|
.map(|account_key| return account_key.to_string());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return match account_key {
|
|
|
|
|
std::option::Option::Some(value) => serde_json::Value::String(value),
|
|
|
|
|
std::option::Option::None => serde_json::Value::Null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn contains(entries: &[&str], entry_code: &str) -> bool {
|
|
|
|
|
return entries.iter().any(|entry| return *entry == entry_code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
fn observation_with_payload(
|
|
|
|
|
surface_code: &str,
|
|
|
|
|
entry_code: &str,
|
|
|
|
|
event_family: kb_model::EventFamily,
|
|
|
|
|
transaction_failed: bool,
|
|
|
|
|
accounts: serde_json::Value,
|
|
|
|
|
parameters: serde_json::Value,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return kb_decoder_api::DecodedObservation {
|
|
|
|
|
event_key: format!("{entry_code}:0"),
|
|
|
|
|
event: kb_model::DecodedProtocolEvent {
|
|
|
|
|
signature: kb_model::Signature("signature".to_string()),
|
|
|
|
|
slot: kb_model::Slot(42),
|
|
|
|
|
instruction_path: kb_model::InstructionPath("0".to_string()),
|
|
|
|
|
program_id: kb_model::ProgramId("program-id".to_string()),
|
|
|
|
|
protocol_code: kb_model::ProtocolCode("solana_native".to_string()),
|
|
|
|
|
surface_code: kb_model::SurfaceCode(surface_code.to_string()),
|
|
|
|
|
event_code: kb_model::EventCode(format!("{surface_code}.{entry_code}")),
|
|
|
|
|
event_name: kb_model::EventName(entry_code.to_string()),
|
|
|
|
|
event_family,
|
|
|
|
|
source_kind: kb_model::EventSourceKind::Instruction,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::ManualExact,
|
|
|
|
|
},
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"accounts": accounts,
|
|
|
|
|
"parameters": parameters,
|
|
|
|
|
}),
|
|
|
|
|
transaction_failed,
|
|
|
|
|
transaction_error: if transaction_failed {
|
|
|
|
|
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
|
|
|
|
} else {
|
|
|
|
|
std::option::Option::None
|
|
|
|
|
},
|
|
|
|
|
observation_committed: !transaction_failed,
|
|
|
|
|
proof: kb_decoder_api::DecoderProof {
|
|
|
|
|
kind: kb_decoder_api::DecoderProofKind::Manual,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::ManualExact,
|
|
|
|
|
evidence: std::vec!["fixture".to_string()],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn observation(
|
|
|
|
|
surface_code: &str,
|
|
|
|
|
entry_code: &str,
|
|
|
|
|
transaction_failed: bool,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return observation_with_payload(
|
|
|
|
|
surface_code,
|
|
|
|
|
entry_code,
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
transaction_failed,
|
|
|
|
|
serde_json::json!([{"role": "target", "accountKey": "account"}]),
|
|
|
|
|
serde_json::json!({"value": 41}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn nonce_observation(
|
|
|
|
|
entry_code: &str,
|
|
|
|
|
family: kb_model::EventFamily,
|
|
|
|
|
transaction_failed: bool,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return observation_with_payload(
|
|
|
|
|
super::SYSTEM_SURFACE,
|
|
|
|
|
entry_code,
|
|
|
|
|
family,
|
|
|
|
|
transaction_failed,
|
|
|
|
|
serde_json::json!([
|
|
|
|
|
{"role": "nonce_account", "accountKey": "nonce111"},
|
|
|
|
|
{"role": "recipient_account", "accountKey": "recipient111"},
|
|
|
|
|
{"role": "nonce_authority", "accountKey": "authority111"}
|
|
|
|
|
]),
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"authority": "new-authority111",
|
|
|
|
|
"lamports": 55,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn slashing_observation(
|
|
|
|
|
entry_code: &str,
|
|
|
|
|
transaction_failed: bool,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
let (family, accounts) = if entry_code == "duplicate_block_proof" {
|
|
|
|
|
(
|
|
|
|
|
kb_model::EventFamily::Audit,
|
|
|
|
|
serde_json::json!([
|
|
|
|
|
{"role": "proof_account", "accountKey": "proof111"},
|
|
|
|
|
{"role": "violation_report", "accountKey": "report111"},
|
|
|
|
|
{"role": "instructions_sysvar", "accountKey": kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID},
|
|
|
|
|
{"role": "system_program", "accountKey": kb_program_ids::SYSTEM_PROGRAM_ID}
|
|
|
|
|
]),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
(
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
serde_json::json!([
|
|
|
|
|
{"role": "violation_report", "accountKey": "report111"},
|
|
|
|
|
{"role": "destination", "accountKey": "destination111"}
|
|
|
|
|
]),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
return observation_with_payload(
|
|
|
|
|
super::SLASHING_SURFACE,
|
|
|
|
|
entry_code,
|
|
|
|
|
family,
|
|
|
|
|
transaction_failed,
|
|
|
|
|
accounts,
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"proofType": "duplicate_block",
|
|
|
|
|
"violationSlot": 42,
|
|
|
|
|
"nodePubkey": "node111",
|
|
|
|
|
"reporter": "reporter111",
|
|
|
|
|
"destination": "destination111",
|
|
|
|
|
"penaltyAppliedByProgram": false,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn zk_observation(
|
|
|
|
|
entry_code: &str,
|
|
|
|
|
context_state_requested: bool,
|
|
|
|
|
transaction_failed: bool,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
let accounts = if entry_code == "close_context_state" {
|
|
|
|
|
serde_json::json!([
|
|
|
|
|
{"role": "proof_context_state", "accountKey": "context111"},
|
|
|
|
|
{"role": "lamport_destination", "accountKey": "destination111"},
|
|
|
|
|
{"role": "context_state_authority", "accountKey": "authority111"}
|
|
|
|
|
])
|
|
|
|
|
} else if context_state_requested {
|
|
|
|
|
serde_json::json!([
|
|
|
|
|
{"role": "proof_context_state", "accountKey": "context111"},
|
|
|
|
|
{"role": "context_state_authority", "accountKey": "authority111"}
|
|
|
|
|
])
|
|
|
|
|
} else {
|
|
|
|
|
serde_json::json!([])
|
|
|
|
|
};
|
|
|
|
|
let family = if entry_code == "close_context_state" {
|
|
|
|
|
kb_model::EventFamily::Lifecycle
|
|
|
|
|
} else {
|
|
|
|
|
kb_model::EventFamily::Audit
|
|
|
|
|
};
|
|
|
|
|
return observation_with_payload(
|
|
|
|
|
super::ZK_ELGAMAL_PROOF_SURFACE,
|
|
|
|
|
entry_code,
|
|
|
|
|
family,
|
|
|
|
|
transaction_failed,
|
|
|
|
|
accounts,
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"proofType": entry_code,
|
|
|
|
|
"contextStateRequested": context_state_requested,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn successful_alt_lifecycle_creates_one_stable_output() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "create_lookup_table", false),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs.len(), 1);
|
|
|
|
|
assert_eq!(result.outputs[0].output_key, "address_lookup_table:create_lookup_table:0");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["domain"], "address_lookup_table");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["loaderSurface"], serde_json::Value::Null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn successful_loader_lifecycle_creates_program_projection() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, "deploy_with_max_data_len", false),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].output_key,
|
|
|
|
|
"program_loader:solana_native_bpf_loader_upgradeable:deploy_with_max_data_len:0"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["domain"], "program_loader");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["loaderSurface"],
|
|
|
|
|
super::BPF_LOADER_UPGRADEABLE_SURFACE
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn successful_feature_revoke_creates_feature_gate_projection() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation(super::FEATURE_SURFACE, "revoke_pending_activation", false),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs[0].output_key, "feature_gate:revoke_pending_activation:0");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["domain"], "feature_gate");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn every_committed_system_account_operation_creates_a_lifecycle_projection() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
for (operation, role, transition) in [
|
|
|
|
|
("create_account", "new_account", "created_allocated_funded_and_assigned"),
|
|
|
|
|
(
|
|
|
|
|
"create_account_with_seed",
|
|
|
|
|
"new_account",
|
|
|
|
|
"derived_account_created_allocated_funded_and_assigned",
|
|
|
|
|
),
|
|
|
|
|
("allocate", "allocated_account", "account_data_space_allocated"),
|
|
|
|
|
("allocate_with_seed", "derived_account", "derived_account_data_space_allocated"),
|
|
|
|
|
(
|
|
|
|
|
"create_account_allow_prefund",
|
|
|
|
|
"new_account",
|
|
|
|
|
"prefunded_account_initialized_allocated_and_assigned",
|
|
|
|
|
),
|
|
|
|
|
] {
|
|
|
|
|
let observation = observation_with_payload(
|
|
|
|
|
super::SYSTEM_SURFACE,
|
|
|
|
|
operation,
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
false,
|
|
|
|
|
serde_json::json!([
|
|
|
|
|
{"role": role, "accountKey": "target111"},
|
|
|
|
|
{"role": "funding_account", "accountKey": "funding111"},
|
|
|
|
|
{"role": "base_account", "accountKey": "base111"},
|
|
|
|
|
]),
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"lamports": 55,
|
|
|
|
|
"space": 64,
|
|
|
|
|
"owner": "owner111",
|
|
|
|
|
"seed": "seed",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs[0].output_key, format!("system_account:{operation}:0"));
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "target111");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["stateTransition"], transition);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn system_account_creation_does_not_duplicate_balance_changes() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observation = observation_with_payload(
|
|
|
|
|
super::SYSTEM_SURFACE,
|
|
|
|
|
"create_account",
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
false,
|
|
|
|
|
serde_json::json!([
|
|
|
|
|
{"role": "funding_account", "accountKey": "funding111"},
|
|
|
|
|
{"role": "new_account", "accountKey": "target111"},
|
|
|
|
|
]),
|
|
|
|
|
serde_json::json!({"lamports": 55, "space": 64, "owner": "owner111"}),
|
|
|
|
|
);
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["requestedLamports"], 55);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["lamportTransferRepresentedByCoreBalanceChanges"],
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
assert!(result.outputs[0].payload_json["projection"].get("balanceChanges").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn every_committed_system_nonce_operation_creates_a_lifecycle_projection() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
for (entry_code, family, transition) in [
|
|
|
|
|
("initialize_nonce_account", kb_model::EventFamily::Lifecycle, "initialized"),
|
|
|
|
|
("advance_nonce_account", kb_model::EventFamily::Lifecycle, "nonce_advanced"),
|
|
|
|
|
("authorize_nonce_account", kb_model::EventFamily::Admin, "authority_changed"),
|
|
|
|
|
(
|
|
|
|
|
"upgrade_nonce_account",
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
"legacy_version_upgraded",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"withdraw_nonce_account",
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
"lamports_withdrawn_possible_closure",
|
|
|
|
|
),
|
|
|
|
|
] {
|
|
|
|
|
let observation = nonce_observation(entry_code, family, false);
|
|
|
|
|
assert!(kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation,
|
|
|
|
|
));
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["domain"], "durable_nonce_account");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "nonce111");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["authorityAccount"],
|
|
|
|
|
"authority111"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["stateTransition"], transition);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn nonce_withdrawal_preserves_conditional_closure_semantics() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observation =
|
|
|
|
|
nonce_observation("withdraw_nonce_account", kb_model::EventFamily::Lifecycle, false);
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["withdrawLamports"], 55);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["lamportDestination"],
|
|
|
|
|
"recipient111"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["closureSemantics"],
|
|
|
|
|
"closes_when_entire_balance_is_withdrawn"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["transactionFinalAccountStateCaptured"]
|
|
|
|
|
.as_bool(),
|
|
|
|
|
std::option::Option::Some(false)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn zk_context_is_materialized_only_when_requested() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let without_context = zk_observation("verify_zero_ciphertext", false, false);
|
|
|
|
|
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&materializer,
|
|
|
|
|
&without_context,
|
|
|
|
|
));
|
|
|
|
|
let with_context = zk_observation("verify_zero_ciphertext", true, false);
|
|
|
|
|
assert!(kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&materializer,
|
|
|
|
|
&with_context,
|
|
|
|
|
));
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &with_context);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["domain"], "zk_proof_context");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "context111");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["stateTransition"],
|
|
|
|
|
"initialized_after_successful_runtime_verification"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["proofMaterialized"].as_bool(),
|
|
|
|
|
std::option::Option::Some(false)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn zk_context_close_creates_a_stable_lifecycle_projection() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observation = zk_observation("close_context_state", false, false);
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs[0].output_key, "zk_proof_context:close_context_state:0");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["lamportDestination"],
|
|
|
|
|
"destination111"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["stateTransition"],
|
|
|
|
|
"closed_lamports_reclaimed_owner_reset_to_system_program"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn successful_slashing_proof_initializes_violation_report_projection() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observation = slashing_observation("duplicate_block_proof", false);
|
|
|
|
|
assert!(kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation,
|
|
|
|
|
));
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].output_key,
|
|
|
|
|
"slashing_violation_report:duplicate_block_proof:0"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["domain"], "slashing_violation_report");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "report111");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["proofMaterialized"], false);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["penaltyAppliedByProgram"], false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn successful_slashing_close_creates_stable_closure_projection() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observation = slashing_observation("close_violation_report", false);
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["stateTransition"],
|
|
|
|
|
"closed_after_minimum_retention_lamports_reclaimed_owner_reset_to_system_program"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["minimumRetentionEpochs"], 3);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["projection"]["lamportDestination"],
|
|
|
|
|
"destination111"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn historical_zk_token_proof_is_not_materialized() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observation = observation_with_payload(
|
|
|
|
|
"solana_native_zk_token_proof",
|
|
|
|
|
"close_context_state",
|
|
|
|
|
kb_model::EventFamily::Audit,
|
|
|
|
|
false,
|
|
|
|
|
serde_json::json!([]),
|
|
|
|
|
serde_json::json!({}),
|
|
|
|
|
);
|
|
|
|
|
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation,
|
|
|
|
|
));
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Ignored);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn loader_write_and_admin_events_are_not_lifecycle_projections() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
for entry_code in ["write", "set_authority", "transfer_authority"] {
|
|
|
|
|
let observation = observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, entry_code, false);
|
|
|
|
|
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unrelated_lifecycle_is_ignored() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&materializer,
|
|
|
|
|
&observation(super::SYSTEM_SURFACE, "transfer", false),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Ignored);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ata_lifecycle_is_owned_by_token_accounts_materializer_only() {
|
|
|
|
|
let mut observation = observation("spl_associated_token_account", "create", false);
|
|
|
|
|
observation.event.program_id =
|
|
|
|
|
kb_model::ProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string());
|
|
|
|
|
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&crate::LifecycleMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
));
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::LifecycleMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Ignored);
|
|
|
|
|
assert!(result.outputs.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failed_native_lifecycle_is_refused() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observations = [
|
|
|
|
|
observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "close_lookup_table", true),
|
|
|
|
|
observation(super::LOADER_V4_SURFACE, "deploy", true),
|
|
|
|
|
nonce_observation("authorize_nonce_account", kb_model::EventFamily::Admin, true),
|
|
|
|
|
zk_observation("verify_zero_ciphertext", true, true),
|
|
|
|
|
slashing_observation("duplicate_block_proof", true),
|
|
|
|
|
];
|
|
|
|
|
for observation in observations {
|
|
|
|
|
let policy =
|
|
|
|
|
kb_materializer_api::validate_materialization_policy(&materializer, &observation);
|
|
|
|
|
assert!(policy.is_err());
|
|
|
|
|
let direct =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(direct.status, kb_materializer_api::MaterializerOutcomeStatus::Refused);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn output_serialization_is_deterministic() {
|
|
|
|
|
let materializer = crate::LifecycleMaterializer;
|
|
|
|
|
let observation = zk_observation("verify_batched_range_proof_u64", true, false);
|
|
|
|
|
let first =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
let second =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
let first_json = match serde_json::to_string(&first) {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(error) => {
|
|
|
|
|
panic!("first lifecycle serialization failed: {error}")
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let second_json = match serde_json::to_string(&second) {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(error) => {
|
|
|
|
|
panic!("second lifecycle serialization failed: {error}")
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(first_json, second_json);
|
|
|
|
|
}
|
|
|
|
|
}
|