v0.1.0-pre.010
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -286,6 +286,16 @@ pub(crate) use crate::decoder::spl_token_decode;
|
||||
pub(crate) use crate::decoder::spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use crate::decoder::spl_token_payload_tag;
|
||||
/// Canonical tracing target for the native and SPL administration materializer.
|
||||
pub(crate) use crate::materializer::admin::MATERIALIZER_ADMIN_TRACING_TARGET;
|
||||
/// Canonical processor name for the native and SPL administration materializer.
|
||||
pub(crate) use crate::materializer::admin::PROCESSOR_NAME as MATERIALIZER_ADMIN_PROCESSOR_NAME;
|
||||
/// Canonical tracing target for the native compliance audit materializer.
|
||||
pub(crate) use crate::materializer::compliance::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET;
|
||||
/// Canonical tracing target for the native lifecycle materializer.
|
||||
pub(crate) use crate::materializer::lifecycle::MATERIALIZER_LIFECYCLE_TRACING_TARGET;
|
||||
/// Canonical tracing target for the native stake and vote materializer.
|
||||
pub(crate) use crate::materializer::staking::MATERIALIZER_STAKING_TRACING_TARGET;
|
||||
|
||||
/// Exact byte length of one SPL ElGamal registry account.
|
||||
pub use crate::decoder::ELGAMAL_REGISTRY_ACCOUNT_LEN;
|
||||
@@ -405,6 +415,12 @@ pub use crate::executor::api::executor::InstructionExecutor;
|
||||
pub use crate::executor::api::executor::serialize_payload_json;
|
||||
/// Exposes the pretty JSON payload serializer.
|
||||
pub use crate::executor::api::executor::serialize_payload_json_pretty;
|
||||
/// Stable native and SPL administration materializer.
|
||||
pub use crate::materializer::admin::AdminMaterializer;
|
||||
/// Materializes one exact SPL ElGamal registry account snapshot.
|
||||
pub use crate::materializer::admin::materialize_elgamal_registry_state_snapshot;
|
||||
/// Materializes exact Token-2022 administration account snapshots.
|
||||
pub use crate::materializer::admin::materialize_token_2022_admin_state_snapshots;
|
||||
/// Stable decoded observation materializer contract used by the common pipeline.
|
||||
pub use crate::materializer::api::contracts::EventMaterializer;
|
||||
/// Explicit policy applied to successful and failed source transactions.
|
||||
@@ -429,6 +445,12 @@ pub use crate::materializer::api::contracts::validate_materialization_policy;
|
||||
pub use crate::materializer::api::contracts::validate_materialized_output_policy;
|
||||
/// Exposes the legacy common business materializer trait.
|
||||
pub use crate::materializer::api::materializer::Materializer;
|
||||
/// Stable native compliance audit materializer.
|
||||
pub use crate::materializer::compliance::ComplianceAuditMaterializer;
|
||||
/// Stable native lifecycle materializer.
|
||||
pub use crate::materializer::lifecycle::LifecycleMaterializer;
|
||||
/// Stable native stake and vote materializer.
|
||||
pub use crate::materializer::staking::StakingMaterializer;
|
||||
/// Current canonical transaction document version.
|
||||
pub use crate::model::canonical_transaction::CANONICAL_TRANSACTION_FORMAT_VERSION;
|
||||
/// Canonical address lookup table reference.
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
// file: kb-lib/src/materializer/admin.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `admin` materializer family.
|
||||
|
||||
mod constants;
|
||||
pub mod core;
|
||||
|
||||
pub(crate) use crate::materializer::admin::constants::PROCESSOR_NAME;
|
||||
pub(crate) use crate::materializer::admin::constants::TRACING_TARGET as MATERIALIZER_ADMIN_TRACING_TARGET;
|
||||
|
||||
/// Stable native and SPL administration materializer.
|
||||
pub use crate::materializer::admin::core::AdminMaterializer;
|
||||
/// Materializes one exact SPL ElGamal registry account snapshot.
|
||||
pub use crate::materializer::admin::core::materialize_elgamal_registry_state_snapshot;
|
||||
/// Materializes exact Token-2022 administration account snapshots.
|
||||
pub use crate::materializer::admin::core::materialize_token_2022_admin_state_snapshots;
|
||||
|
||||
10
kb-lib/src/materializer/admin/constants.rs
Normal file
10
kb-lib/src/materializer/admin/constants.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// file: kb-lib/src/materializer/admin/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the native and SPL administration materializer.
|
||||
|
||||
/// Canonical processor name for this component.
|
||||
pub(crate) const PROCESSOR_NAME: &str = "solana_native_admin";
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.admin";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,12 @@
|
||||
// file: kb-lib/src/materializer/compliance.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `compliance` materializer family.
|
||||
|
||||
pub mod audit;
|
||||
mod constants;
|
||||
|
||||
pub(crate) use crate::materializer::compliance::constants::TRACING_TARGET as MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET;
|
||||
|
||||
/// Stable native compliance audit materializer.
|
||||
pub use crate::materializer::compliance::audit::ComplianceAuditMaterializer;
|
||||
|
||||
@@ -1,10 +1,577 @@
|
||||
// file: kb-lib/src/materializer/compliance/audit.rs
|
||||
// version: 1
|
||||
// version: 7
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_compliance_audit`.
|
||||
//! Stable compliance-audit projections for committed native bytecode mutations and execution profiles.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_compliance_audit";
|
||||
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[crate::EventFamily::Audit];
|
||||
const COMPUTE_BUDGET_SURFACE: &str = "solana_native_compute_budget";
|
||||
const COMPUTE_BUDGET_ENTRIES: &[&str] = &[
|
||||
"request_units_deprecated",
|
||||
"request_heap_frame",
|
||||
"set_compute_unit_limit",
|
||||
"set_compute_unit_price",
|
||||
"set_loaded_accounts_data_size_limit",
|
||||
];
|
||||
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 WRITE_ENTRIES: &[&str] = &["write"];
|
||||
const LOADER_V4_COPY_ENTRIES: &[&str] = &["copy"];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ProjectionKind {
|
||||
ProgramByteWrite(&'static str),
|
||||
ProgramByteCopy,
|
||||
ComputeBudgetProfile,
|
||||
}
|
||||
|
||||
impl ProjectionKind {
|
||||
fn output_key(self, surface_code: &str, operation: &str) -> std::string::String {
|
||||
return match self {
|
||||
Self::ProgramByteWrite(_) | Self::ProgramByteCopy => {
|
||||
format!("program_loader_code:{surface_code}:{operation}:0")
|
||||
},
|
||||
Self::ComputeBudgetProfile => "transaction_compute_budget_profile:0".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn loader_surface(self) -> &'static str {
|
||||
return match self {
|
||||
Self::ProgramByteWrite(surface) => surface,
|
||||
Self::ProgramByteCopy => LOADER_V4_SURFACE,
|
||||
Self::ComputeBudgetProfile => COMPUTE_BUDGET_SURFACE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable compliance-audit materializer for native bytecode mutations and execution profiles.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ComplianceAuditMaterializer;
|
||||
|
||||
impl crate::Materializer for crate::ComplianceAuditMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_compliance_audit";
|
||||
}
|
||||
|
||||
fn materializer_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
|
||||
return event.event_family == crate::EventFamily::Audit
|
||||
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
||||
if !crate::Materializer::accepts_event(self, event) {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
name: "solana_native_compliance_audit".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
return observation.event.event_family == crate::EventFamily::Audit
|
||||
&& projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
)
|
||||
.is_some();
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulOrFailedAudit;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
let projection = match projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compliance_audit",
|
||||
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 compliance-audit projections"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
if observation.event.event_family != crate::EventFamily::Audit {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
}
|
||||
if projection != ProjectionKind::ComputeBudgetProfile
|
||||
&& (observation.transaction_failed || !observation.observation_committed)
|
||||
{
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compliance_audit",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
committed = observation.observation_committed,
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"refuse uncommitted native bytecode mutation observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::refused(
|
||||
"failed_transaction_compliance_audit_refused",
|
||||
"failed or uncommitted native observations cannot create committed bytecode mutation 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();
|
||||
if projection == ProjectionKind::ComputeBudgetProfile {
|
||||
return materialize_compute_budget_profile(observation, parameters, operation);
|
||||
}
|
||||
let output = crate::MaterializedOutput {
|
||||
output_key: projection
|
||||
.output_key(observation.event.surface_code.0.as_str(), operation.as_str()),
|
||||
family: crate::MaterializedEventFamily::ComplianceAudit,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "committed_instruction_compliance_audit_event",
|
||||
"domain": "program_loader_code_mutation",
|
||||
"loaderSurface": projection.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(projection, &accounts, ¶meters),
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compliance_audit",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
loader_surface = projection.loader_surface(),
|
||||
output_count = 1_usize,
|
||||
"materialize committed native bytecode mutation observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_compute_budget_profile(
|
||||
observation: &crate::DecodedObservation,
|
||||
parameters: serde_json::Value,
|
||||
operation: std::string::String,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
let profile = parameters.get("transactionProfile").cloned().unwrap_or(serde_json::Value::Null);
|
||||
if !compute_budget_profile_emits(&profile) {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
}
|
||||
let output = crate::MaterializedOutput {
|
||||
output_key: "transaction_compute_budget_profile:0".to_string(),
|
||||
family: crate::MaterializedEventFamily::ComplianceAudit,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "transaction_compute_budget_profile",
|
||||
"domain": "transaction_compute_budget_profile",
|
||||
"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": !observation.transaction_failed,
|
||||
"profile": profile,
|
||||
"profileSource": {
|
||||
"surfaceCode": observation.event.surface_code.0.clone(),
|
||||
"entryCode": observation.event.event_name.0.clone(),
|
||||
"eventKey": observation.event_key.clone()
|
||||
},
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compute_budget_profile",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
output_count = 1_usize,
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"materialize native Compute Budget transaction profile"
|
||||
);
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn compute_budget_profile_emits(profile: &serde_json::Value) -> bool {
|
||||
return profile
|
||||
.get("currentInstructionIsProfileEmitter")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== std::option::Option::Some(true);
|
||||
}
|
||||
|
||||
fn projection(surface_code: &str, entry_code: &str) -> std::option::Option<ProjectionKind> {
|
||||
if surface_code == COMPUTE_BUDGET_SURFACE && contains(COMPUTE_BUDGET_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::ComputeBudgetProfile);
|
||||
}
|
||||
if contains(WRITE_ENTRIES, entry_code) {
|
||||
for supported in [
|
||||
BPF_LOADER_DEPRECATED_SURFACE,
|
||||
BPF_LOADER_SURFACE,
|
||||
BPF_LOADER_UPGRADEABLE_SURFACE,
|
||||
LOADER_V4_SURFACE,
|
||||
] {
|
||||
if surface_code == supported {
|
||||
return std::option::Option::Some(ProjectionKind::ProgramByteWrite(supported));
|
||||
}
|
||||
}
|
||||
}
|
||||
if surface_code == LOADER_V4_SURFACE && contains(LOADER_V4_COPY_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::ProgramByteCopy);
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn projection_details(
|
||||
projection: ProjectionKind,
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
return match projection {
|
||||
ProjectionKind::ProgramByteWrite(_) => serde_json::json!({
|
||||
"targetAccount": first_account_key_for_roles(
|
||||
accounts,
|
||||
&["program_account", "buffer_account"],
|
||||
),
|
||||
"authorityAccount": first_account_key_for_roles(
|
||||
accounts,
|
||||
&["authority", "buffer_authority"],
|
||||
),
|
||||
"offset": parameters.get("offset").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"byteLength": parameters.get("byteLength").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"bytesSha256": parameters.get("bytesSha256").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"bytesPrefixHex": parameters.get("bytesPrefixHex").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"stateTransition": "program_bytes_written",
|
||||
"codeBytesMaterialized": false,
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
}),
|
||||
ProjectionKind::ProgramByteCopy => serde_json::json!({
|
||||
"targetAccount": account_key_for_role(accounts, "program_account"),
|
||||
"authorityAccount": account_key_for_role(accounts, "authority"),
|
||||
"sourceProgramAccount": account_key_for_role(accounts, "source_program_account"),
|
||||
"destinationOffset": parameters.get("destinationOffset").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"sourceOffset": parameters.get("sourceOffset").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"byteLength": parameters.get("length").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"stateTransition": "program_bytes_copied",
|
||||
"codeBytesMaterialized": false,
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
}),
|
||||
ProjectionKind::ComputeBudgetProfile => serde_json::Value::Null,
|
||||
};
|
||||
}
|
||||
|
||||
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(
|
||||
surface_code: &str,
|
||||
entry_code: &str,
|
||||
transaction_failed: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
event: crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature("signature".to_string()),
|
||||
slot: crate::Slot(42),
|
||||
instruction_path: crate::InstructionPath("0".to_string()),
|
||||
program_id: crate::ProgramId("program-id".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event_family: crate::EventFamily::Audit,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::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: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn account(role: &str, account_key: &str) -> serde_json::Value {
|
||||
return serde_json::json!({"role": role, "accountKey": account_key});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_write_variants_create_bounded_code_audits() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
for (surface, target_role, authority_role) in [
|
||||
(super::BPF_LOADER_DEPRECATED_SURFACE, "program_account", "missing"),
|
||||
(super::BPF_LOADER_SURFACE, "program_account", "missing"),
|
||||
(super::BPF_LOADER_UPGRADEABLE_SURFACE, "buffer_account", "buffer_authority"),
|
||||
(super::LOADER_V4_SURFACE, "program_account", "authority"),
|
||||
] {
|
||||
let observation = observation(
|
||||
surface,
|
||||
"write",
|
||||
false,
|
||||
serde_json::json!([
|
||||
account(target_role, "target111"),
|
||||
account(authority_role, "authority111"),
|
||||
]),
|
||||
serde_json::json!({
|
||||
"offset": 7,
|
||||
"byteLength": 32,
|
||||
"bytesSha256": "abc",
|
||||
"bytesPrefixHex": "0102",
|
||||
}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::ComplianceAudit);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "target111");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["bytesSha256"], "abc");
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["codeBytesMaterialized"],
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_v4_copy_preserves_source_and_bounded_ranges() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::LOADER_V4_SURFACE,
|
||||
"copy",
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("program_account", "target111"),
|
||||
account("authority", "authority111"),
|
||||
account("source_program_account", "source111"),
|
||||
]),
|
||||
serde_json::json!({"destinationOffset": 8, "sourceOffset": 4, "length": 16}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(
|
||||
result.outputs[0].output_key,
|
||||
"program_loader_code:solana_native_loader_v4:copy:0"
|
||||
);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["sourceProgramAccount"],
|
||||
"source111"
|
||||
);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["byteLength"], 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_budget_profile_is_emitted_only_by_profile_emitter() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let emitter = observation(
|
||||
super::COMPUTE_BUDGET_SURFACE,
|
||||
"set_compute_unit_limit",
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({
|
||||
"transactionProfile": {
|
||||
"currentInstructionIsProfileEmitter": true,
|
||||
"profileEmitterInstructionPath": "0",
|
||||
"computeBudgetInstructionCount": 2,
|
||||
"effectiveValues": {
|
||||
"set_compute_unit_limit": {
|
||||
"value": 200000,
|
||||
"sourceInstructionPath": "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
let emitted = crate::EventMaterializer::materialize(&materializer, &emitter);
|
||||
assert_eq!(emitted.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(emitted.outputs[0].output_key, "transaction_compute_budget_profile:0");
|
||||
assert_eq!(emitted.outputs[0].family, crate::MaterializedEventFamily::ComplianceAudit);
|
||||
assert_eq!(emitted.outputs[0].payload_json["profile"]["computeBudgetInstructionCount"], 2);
|
||||
let non_emitter = observation(
|
||||
super::COMPUTE_BUDGET_SURFACE,
|
||||
"set_compute_unit_price",
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({
|
||||
"transactionProfile": {
|
||||
"currentInstructionIsProfileEmitter": false,
|
||||
"profileEmitterInstructionPath": "0"
|
||||
}
|
||||
}),
|
||||
);
|
||||
let ignored = crate::EventMaterializer::materialize(&materializer, &non_emitter);
|
||||
assert_eq!(ignored.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_compute_budget_profile_remains_audit_materializable() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::COMPUTE_BUDGET_SURFACE,
|
||||
"set_compute_unit_price",
|
||||
true,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({
|
||||
"transactionProfile": {
|
||||
"currentInstructionIsProfileEmitter": true,
|
||||
"profileEmitterInstructionPath": "0",
|
||||
"computeBudgetInstructionCount": 1
|
||||
}
|
||||
}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].payload_json["transactionSucceeded"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_code_mutation_is_refused() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::LOADER_V4_SURFACE,
|
||||
"write",
|
||||
true,
|
||||
serde_json::json!([account("program_account", "target111")]),
|
||||
serde_json::json!({"offset": 0, "byteLength": 1, "bytesSha256": "abc"}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_audit_observation_is_ignored() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
"other_surface",
|
||||
"write",
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_serialization_is_deterministic() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::BPF_LOADER_UPGRADEABLE_SURFACE,
|
||||
"write",
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("buffer_account", "buffer111"),
|
||||
account("buffer_authority", "authority111"),
|
||||
]),
|
||||
serde_json::json!({
|
||||
"offset": 0,
|
||||
"byteLength": 2,
|
||||
"bytesSha256": "abc",
|
||||
"bytesPrefixHex": "0102",
|
||||
}),
|
||||
);
|
||||
let first = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::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 compliance 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 compliance serialization failed: {error}")
|
||||
},
|
||||
};
|
||||
assert_eq!(first_json, second_json);
|
||||
}
|
||||
}
|
||||
|
||||
7
kb-lib/src/materializer/compliance/constants.rs
Normal file
7
kb-lib/src/materializer/compliance/constants.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: kb-lib/src/materializer/compliance/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the native compliance audit materializer.
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.compliance";
|
||||
@@ -1,6 +1,12 @@
|
||||
// file: kb-lib/src/materializer/lifecycle.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `lifecycle` materializer family.
|
||||
|
||||
mod constants;
|
||||
pub mod core;
|
||||
|
||||
pub(crate) use crate::materializer::lifecycle::constants::TRACING_TARGET as MATERIALIZER_LIFECYCLE_TRACING_TARGET;
|
||||
|
||||
/// Stable native lifecycle materializer.
|
||||
pub use crate::materializer::lifecycle::core::LifecycleMaterializer;
|
||||
|
||||
7
kb-lib/src/materializer/lifecycle/constants.rs
Normal file
7
kb-lib/src/materializer/lifecycle/constants.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: kb-lib/src/materializer/lifecycle/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the native lifecycle materializer.
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.lifecycle";
|
||||
@@ -1,10 +1,960 @@
|
||||
// file: kb-lib/src/materializer/lifecycle/core.rs
|
||||
// version: 1
|
||||
// version: 14
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_lifecycle`.
|
||||
//! Stable lifecycle projections derived from exact decoded observations.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_lifecycle";
|
||||
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::EventFamily::Admin,
|
||||
crate::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",
|
||||
];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
#[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 crate::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: &crate::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: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
||||
if !crate::Materializer::accepts_event(self, event) {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventMaterializer for crate::LifecycleMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
name: "solana_native_lifecycle".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
return family_is_accepted(observation.event.event_family)
|
||||
&& observation_projection(observation).is_some();
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
let projection = match observation_projection(observation) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_LIFECYCLE_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 crate::MaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_LIFECYCLE_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 crate::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::MATERIALIZER_LIFECYCLE_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 = crate::MaterializedOutput {
|
||||
output_key,
|
||||
family: crate::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 crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn family_is_accepted(family: crate::EventFamily) -> bool {
|
||||
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
|
||||
}
|
||||
|
||||
fn observation_projection(
|
||||
observation: &crate::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: crate::EventFamily,
|
||||
transaction_failed: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
event: crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature("signature".to_string()),
|
||||
slot: crate::Slot(42),
|
||||
instruction_path: crate::InstructionPath("0".to_string()),
|
||||
program_id: crate::ProgramId("program-id".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event_family,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::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: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn observation(
|
||||
surface_code: &str,
|
||||
entry_code: &str,
|
||||
transaction_failed: bool,
|
||||
) -> crate::DecodedObservation {
|
||||
return observation_with_payload(
|
||||
surface_code,
|
||||
entry_code,
|
||||
crate::EventFamily::Lifecycle,
|
||||
transaction_failed,
|
||||
serde_json::json!([{"role": "target", "accountKey": "account"}]),
|
||||
serde_json::json!({"value": 41}),
|
||||
);
|
||||
}
|
||||
|
||||
fn nonce_observation(
|
||||
entry_code: &str,
|
||||
family: crate::EventFamily,
|
||||
transaction_failed: bool,
|
||||
) -> crate::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,
|
||||
) -> crate::DecodedObservation {
|
||||
let (family, accounts) = if entry_code == "duplicate_block_proof" {
|
||||
(
|
||||
crate::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 {
|
||||
(
|
||||
crate::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,
|
||||
) -> crate::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" {
|
||||
crate::EventFamily::Lifecycle
|
||||
} else {
|
||||
crate::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 = crate::EventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "create_lookup_table", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::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 = crate::EventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, "deploy_with_max_data_len", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::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 = crate::EventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::FEATURE_SURFACE, "revoke_pending_activation", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::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,
|
||||
crate::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 = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::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",
|
||||
crate::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 = crate::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", crate::EventFamily::Lifecycle, "initialized"),
|
||||
("advance_nonce_account", crate::EventFamily::Lifecycle, "nonce_advanced"),
|
||||
("authorize_nonce_account", crate::EventFamily::Admin, "authority_changed"),
|
||||
(
|
||||
"upgrade_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
"legacy_version_upgraded",
|
||||
),
|
||||
(
|
||||
"withdraw_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
"lamports_withdrawn_possible_closure",
|
||||
),
|
||||
] {
|
||||
let observation = nonce_observation(entry_code, family, false);
|
||||
assert!(crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::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", crate::EventFamily::Lifecycle, false);
|
||||
let result = crate::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!(!crate::EventMaterializer::accepts_observation(&materializer, &without_context,));
|
||||
let with_context = zk_observation("verify_zero_ciphertext", true, false);
|
||||
assert!(crate::EventMaterializer::accepts_observation(&materializer, &with_context,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &with_context);
|
||||
assert_eq!(result.status, crate::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 = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::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!(crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::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 = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::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",
|
||||
crate::EventFamily::Audit,
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::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!(!crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_lifecycle_is_ignored() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let result = crate::EventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::SYSTEM_SURFACE, "transfer", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::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 =
|
||||
crate::ProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string());
|
||||
assert!(!crate::EventMaterializer::accepts_observation(
|
||||
&crate::LifecycleMaterializer,
|
||||
&observation,
|
||||
));
|
||||
let result =
|
||||
crate::EventMaterializer::materialize(&crate::LifecycleMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::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", crate::EventFamily::Admin, true),
|
||||
zk_observation("verify_zero_ciphertext", true, true),
|
||||
slashing_observation("duplicate_block_proof", true),
|
||||
];
|
||||
for observation in observations {
|
||||
let policy = crate::validate_materialization_policy(&materializer, &observation);
|
||||
assert!(policy.is_err());
|
||||
let direct = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(direct.status, crate::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 = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// file: kb-lib/src/materializer/staking.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `staking` materializer family.
|
||||
|
||||
mod constants;
|
||||
pub mod core;
|
||||
|
||||
pub(crate) use crate::materializer::staking::constants::TRACING_TARGET as MATERIALIZER_STAKING_TRACING_TARGET;
|
||||
|
||||
/// Stable native stake and vote materializer.
|
||||
pub use crate::materializer::staking::core::StakingMaterializer;
|
||||
|
||||
7
kb-lib/src/materializer/staking/constants.rs
Normal file
7
kb-lib/src/materializer/staking/constants.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: kb-lib/src/materializer/staking/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the native stake and vote materializer.
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.staking";
|
||||
@@ -1,10 +1,867 @@
|
||||
// file: kb-lib/src/materializer/staking/core.rs
|
||||
// version: 1
|
||||
// version: 7
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_staking`.
|
||||
//! Instruction-level staking projections for native Stake and Vote observations.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_staking";
|
||||
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::EventFamily::Admin,
|
||||
crate::EventFamily::Staking,
|
||||
crate::EventFamily::Reward,
|
||||
];
|
||||
const STAKE_SURFACE: &str = "solana_native_stake";
|
||||
const VOTE_SURFACE: &str = "solana_native_vote";
|
||||
const STAKE_ACCOUNT_ENTRIES: &[&str] = &[
|
||||
"initialize",
|
||||
"initialize_checked",
|
||||
"delegate_stake",
|
||||
"deactivate",
|
||||
"deactivate_delinquent",
|
||||
];
|
||||
const STAKE_VALUE_ENTRIES: &[&str] = &["split", "withdraw", "merge", "move_stake", "move_lamports"];
|
||||
const STAKE_AUTHORITY_ENTRIES: &[&str] = &[
|
||||
"authorize",
|
||||
"authorize_with_seed",
|
||||
"authorize_checked",
|
||||
"authorize_checked_with_seed",
|
||||
];
|
||||
const STAKE_LOCKUP_ENTRIES: &[&str] = &["set_lockup", "set_lockup_checked"];
|
||||
const VOTE_ACCOUNT_ENTRIES: &[&str] = &["initialize_account", "initialize_account_v2"];
|
||||
const VOTE_AUTHORITY_ENTRIES: &[&str] = &[
|
||||
"authorize",
|
||||
"authorize_checked",
|
||||
"authorize_with_seed",
|
||||
"authorize_checked_with_seed",
|
||||
];
|
||||
const VOTE_ADMIN_ENTRIES: &[&str] = &[
|
||||
"update_validator_identity",
|
||||
"update_commission",
|
||||
"update_commission_collector",
|
||||
"update_commission_bps",
|
||||
];
|
||||
const VOTE_STATE_ENTRIES: &[&str] = &[
|
||||
"vote",
|
||||
"vote_switch",
|
||||
"update_vote_state",
|
||||
"update_vote_state_switch",
|
||||
"compact_update_vote_state",
|
||||
"compact_update_vote_state_switch",
|
||||
"tower_sync",
|
||||
"tower_sync_switch",
|
||||
];
|
||||
const VOTE_VALUE_ENTRIES: &[&str] = &["withdraw"];
|
||||
const VOTE_REWARD_ENTRIES: &[&str] = &["deposit_delegator_rewards"];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ProjectionKind {
|
||||
StakeAccount,
|
||||
StakeValue,
|
||||
StakeAuthority,
|
||||
StakeLockup,
|
||||
VoteAccount,
|
||||
VoteAuthority,
|
||||
VoteAdmin,
|
||||
VoteState,
|
||||
VoteValue,
|
||||
VoteReward,
|
||||
}
|
||||
|
||||
impl ProjectionKind {
|
||||
fn domain(self) -> &'static str {
|
||||
return match self {
|
||||
Self::StakeAccount => "stake_account",
|
||||
Self::StakeValue => "stake_value",
|
||||
Self::StakeAuthority => "stake_authority",
|
||||
Self::StakeLockup => "stake_lockup",
|
||||
Self::VoteAccount => "vote_account",
|
||||
Self::VoteAuthority => "vote_authority",
|
||||
Self::VoteAdmin => "vote_admin",
|
||||
Self::VoteState => "vote_state",
|
||||
Self::VoteValue => "vote_value",
|
||||
Self::VoteReward => "vote_reward",
|
||||
};
|
||||
}
|
||||
|
||||
fn family(self) -> crate::MaterializedEventFamily {
|
||||
return match self {
|
||||
Self::VoteReward => crate::MaterializedEventFamily::Reward,
|
||||
_ => crate::MaterializedEventFamily::Staking,
|
||||
};
|
||||
}
|
||||
|
||||
fn output_key(self, operation: &str) -> std::string::String {
|
||||
return format!("{}:{operation}:0", self.domain());
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable staking materializer for committed native Stake and Vote observations.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct StakingMaterializer;
|
||||
|
||||
impl crate::Materializer for crate::StakingMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_staking";
|
||||
}
|
||||
|
||||
fn materializer_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
|
||||
return family_is_accepted(event.event_family)
|
||||
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
||||
if !crate::Materializer::accepts_event(self, event) {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
name: "solana_native_staking".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
return family_is_accepted(observation.event.event_family)
|
||||
&& projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
)
|
||||
.is_some();
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
let projection = match projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
||||
action = "materialize_staking",
|
||||
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 staking projections"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
if !family_is_accepted(observation.event.event_family) {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
}
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
||||
action = "materialize_staking",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
committed = observation.observation_committed,
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"refuse uncommitted native staking observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::refused(
|
||||
"failed_transaction_staking_refused",
|
||||
"failed or uncommitted native Stake/Vote observations cannot create staking 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 = crate::MaterializedOutput {
|
||||
output_key: projection.output_key(operation.as_str()),
|
||||
family: projection.family(),
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "committed_instruction_staking_event",
|
||||
"domain": projection.domain(),
|
||||
"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(
|
||||
projection,
|
||||
&accounts,
|
||||
¶meters,
|
||||
observation.event.event_name.0.as_str(),
|
||||
),
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
||||
action = "materialize_staking",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
projection_domain = projection.domain(),
|
||||
output_count = 1_usize,
|
||||
"materialize committed native staking observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn family_is_accepted(family: crate::EventFamily) -> bool {
|
||||
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
|
||||
}
|
||||
|
||||
fn projection(surface_code: &str, entry_code: &str) -> std::option::Option<ProjectionKind> {
|
||||
if surface_code == STAKE_SURFACE {
|
||||
return stake_projection(entry_code);
|
||||
}
|
||||
if surface_code == VOTE_SURFACE {
|
||||
return vote_projection(entry_code);
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn stake_projection(entry_code: &str) -> std::option::Option<ProjectionKind> {
|
||||
if contains(STAKE_ACCOUNT_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::StakeAccount);
|
||||
}
|
||||
if contains(STAKE_VALUE_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::StakeValue);
|
||||
}
|
||||
if contains(STAKE_AUTHORITY_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::StakeAuthority);
|
||||
}
|
||||
if contains(STAKE_LOCKUP_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::StakeLockup);
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn vote_projection(entry_code: &str) -> std::option::Option<ProjectionKind> {
|
||||
if contains(VOTE_ACCOUNT_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::VoteAccount);
|
||||
}
|
||||
if contains(VOTE_AUTHORITY_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::VoteAuthority);
|
||||
}
|
||||
if contains(VOTE_ADMIN_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::VoteAdmin);
|
||||
}
|
||||
if contains(VOTE_STATE_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::VoteState);
|
||||
}
|
||||
if contains(VOTE_VALUE_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::VoteValue);
|
||||
}
|
||||
if contains(VOTE_REWARD_ENTRIES, entry_code) {
|
||||
return std::option::Option::Some(ProjectionKind::VoteReward);
|
||||
}
|
||||
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::StakeAccount => stake_account_details(accounts, parameters, operation),
|
||||
ProjectionKind::StakeValue => stake_value_details(accounts, parameters, operation),
|
||||
ProjectionKind::StakeAuthority => stake_authority_details(accounts, parameters, operation),
|
||||
ProjectionKind::StakeLockup => stake_lockup_details(accounts, parameters, operation),
|
||||
ProjectionKind::VoteAccount => vote_account_details(accounts, parameters, operation),
|
||||
ProjectionKind::VoteAuthority => vote_authority_details(accounts, parameters, operation),
|
||||
ProjectionKind::VoteAdmin => vote_admin_details(accounts, parameters, operation),
|
||||
ProjectionKind::VoteState => vote_state_details(accounts, parameters, operation),
|
||||
ProjectionKind::VoteValue => vote_value_details(accounts, parameters, operation),
|
||||
ProjectionKind::VoteReward => vote_reward_details(accounts, parameters, operation),
|
||||
};
|
||||
}
|
||||
|
||||
fn stake_account_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
||||
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
||||
"authorized": parameters.get("authorized").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"lockup": parameters.get("lockup").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"activationStateComputed": false,
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
"stateTransition": match operation {
|
||||
"initialize" | "initialize_checked" => "stake_account_initialized",
|
||||
"delegate_stake" => "stake_delegation_requested",
|
||||
"deactivate" => "stake_deactivation_requested",
|
||||
"deactivate_delinquent" => "stake_deactivation_requested_for_delinquent_vote_account",
|
||||
_ => "stake_account_state_transition_requested",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn stake_value_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
||||
"splitAccount": account_key_for_role(accounts, "split_account"),
|
||||
"destinationAccount": first_account_key_for_roles(accounts, &["destination", "recipient_account", "destination_stake_account"]),
|
||||
"sourceStakeAccount": account_key_for_role(accounts, "source_stake_account"),
|
||||
"lamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"lamportBalanceChangesDuplicated": false,
|
||||
"activationStateComputed": false,
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
"stateTransition": match operation {
|
||||
"split" => "stake_split_requested",
|
||||
"withdraw" => "stake_lamports_withdrawn_or_account_closed_conditionally",
|
||||
"merge" => "stake_accounts_merged",
|
||||
"move_stake" => "active_stake_moved",
|
||||
"move_lamports" => "inactive_lamports_moved",
|
||||
_ => "stake_value_transition_requested",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn stake_authority_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
||||
"currentAuthority": first_account_key_for_roles(accounts, &["stake_or_withdraw_authority", "current_authority", "stake_authority", "withdraw_authority", "authority_base"]),
|
||||
"newAuthority": first_parameter_or_account(
|
||||
parameters,
|
||||
&["newAuthority", "newAuthorizedPubkey"],
|
||||
accounts,
|
||||
"new_authority",
|
||||
),
|
||||
"authorizationType": parameters.get("authorizationType").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"authoritySeed": parameters.get("authoritySeed").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"authorityOwner": parameters.get("authorityOwner").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"stateTransition": match operation {
|
||||
"authorize_with_seed" | "authorize_checked_with_seed" => "stake_authority_changed_with_seed",
|
||||
"authorize_checked" => "stake_authority_changed_with_new_authority_signature",
|
||||
_ => "stake_authority_changed",
|
||||
},
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn stake_lockup_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
||||
"custodian": first_account_key_for_roles(accounts, &["lockup_custodian", "lockup_or_withdraw_authority", "new_lockup_authority"]),
|
||||
"lockup": parameters.clone(),
|
||||
"checkedCustodianFromAccount": operation == "set_lockup_checked",
|
||||
"stateTransition": "stake_lockup_updated",
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn vote_account_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
||||
"validatorIdentity": account_key_for_role(accounts, "validator_identity"),
|
||||
"authorizedVoter": parameters.get("authorizedVoter").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"authorizedWithdrawer": parameters.get("authorizedWithdrawer").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"commissionPercent": parameters.get("commissionPercent").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"inflationRewardsCommissionBasisPoints": parameters.get("inflationRewardsCommissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"blockRevenueCommissionBasisPoints": parameters.get("blockRevenueCommissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"inflationRewardsCollector": account_key_for_role(accounts, "inflation_rewards_collector"),
|
||||
"blockRevenueCollector": account_key_for_role(accounts, "block_revenue_collector"),
|
||||
"stateTransition": if operation == "initialize_account_v2" {
|
||||
"vote_account_initialized_v2"
|
||||
} else {
|
||||
"vote_account_initialized"
|
||||
},
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn vote_authority_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
||||
"currentAuthority": first_account_key_for_roles(accounts, &["current_authority", "current_authority_base"]),
|
||||
"newAuthority": first_parameter_or_account(
|
||||
parameters,
|
||||
&["newAuthority"],
|
||||
accounts,
|
||||
"new_authority",
|
||||
),
|
||||
"authorizationType": parameters.get("authorizationType").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"currentAuthorityDerivedKeyOwner": parameters.get("currentAuthorityDerivedKeyOwner").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"currentAuthorityDerivedKeySeed": parameters.get("currentAuthorityDerivedKeySeed").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"newAuthorityFromAccount": parameters.get("newAuthorityFromAccount").cloned().unwrap_or(serde_json::Value::Bool(false)),
|
||||
"stateTransition": match operation {
|
||||
"authorize_with_seed" | "authorize_checked_with_seed" => "vote_authority_changed_with_seed",
|
||||
"authorize_checked" => "vote_authority_changed_with_new_authority_signature",
|
||||
_ => "vote_authority_changed",
|
||||
},
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn vote_admin_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
||||
"withdrawAuthority": account_key_for_role(accounts, "withdraw_authority"),
|
||||
"newValidatorIdentity": account_key_for_role(accounts, "new_validator_identity"),
|
||||
"newCommissionCollector": account_key_for_role(accounts, "new_commission_collector"),
|
||||
"commissionPercent": parameters.get("commissionPercent").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"commissionBasisPoints": parameters.get("commissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"commissionKind": parameters.get("commissionKind").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"stateTransition": match operation {
|
||||
"update_validator_identity" => "vote_validator_identity_updated",
|
||||
"update_commission" | "update_commission_bps" => "vote_commission_updated",
|
||||
"update_commission_collector" => "vote_commission_collector_updated",
|
||||
_ => "vote_admin_updated",
|
||||
},
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn vote_state_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
||||
"voteAuthority": account_key_for_role(accounts, "vote_authority"),
|
||||
"lastVotedSlot": parameters.get("lastVotedSlot").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"slots": parameters.get("slots").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"root": parameters.get("root").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"hash": first_parameter_value(parameters, &["hash", "bankHash"]),
|
||||
"timestamp": parameters.get("timestamp").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"switchProofHash": parameters.get("switchProofHash").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"stateTransition": if operation.contains("switch") {
|
||||
"vote_state_updated_with_switch_proof"
|
||||
} else if operation.starts_with("tower_sync") {
|
||||
"vote_tower_synchronized"
|
||||
} else {
|
||||
"vote_state_updated"
|
||||
},
|
||||
"voteCreditsComputed": false,
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn vote_value_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
_operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
||||
"recipientAccount": account_key_for_role(accounts, "recipient_account"),
|
||||
"withdrawAuthority": account_key_for_role(accounts, "withdraw_authority"),
|
||||
"lamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"lamportBalanceChangesDuplicated": false,
|
||||
"stateTransition": "vote_lamports_withdrawn_or_account_closed_conditionally",
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn vote_reward_details(
|
||||
accounts: &serde_json::Value,
|
||||
parameters: &serde_json::Value,
|
||||
_operation: &str,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
||||
"depositSource": account_key_for_role(accounts, "deposit_source"),
|
||||
"depositLamports": parameters.get("depositLamports").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"lamportBalanceChangesDuplicated": false,
|
||||
"stateTransition": "delegator_rewards_deposited_into_vote_account",
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn first_parameter_or_account(
|
||||
parameters: &serde_json::Value,
|
||||
keys: &[&str],
|
||||
accounts: &serde_json::Value,
|
||||
fallback_role: &str,
|
||||
) -> serde_json::Value {
|
||||
let value = first_parameter_value(parameters, keys);
|
||||
if !value.is_null() {
|
||||
return value;
|
||||
}
|
||||
return account_key_for_role(accounts, fallback_role);
|
||||
}
|
||||
|
||||
fn first_parameter_value(parameters: &serde_json::Value, keys: &[&str]) -> serde_json::Value {
|
||||
for key in keys {
|
||||
if let std::option::Option::Some(value) = parameters.get(*key) {
|
||||
return value.clone();
|
||||
}
|
||||
}
|
||||
return serde_json::Value::Null;
|
||||
}
|
||||
|
||||
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(
|
||||
surface_code: &str,
|
||||
entry_code: &str,
|
||||
family: crate::EventFamily,
|
||||
transaction_failed: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
event: crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature("signature".to_string()),
|
||||
slot: crate::Slot(42),
|
||||
instruction_path: crate::InstructionPath("0".to_string()),
|
||||
program_id: crate::ProgramId("program-id".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event_family: family,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::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: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn account(role: &str, account_key: &str) -> serde_json::Value {
|
||||
return serde_json::json!({"role": role, "accountKey": account_key});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stake_initialize_creates_instructional_staking_projection() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let observation = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"initialize",
|
||||
crate::EventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([account("stake_account", "stake111")]),
|
||||
serde_json::json!({"authorized": {"staker": "staker111"}}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::Staking);
|
||||
assert_eq!(result.outputs[0].output_key, "stake_account:initialize:0");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["stakeAccount"], "stake111");
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["transactionFinalAccountStateCaptured"],
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stake_authority_and_lockup_events_are_projected() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let authority = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"authorize_with_seed",
|
||||
crate::EventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([account("stake_account", "stake111")]),
|
||||
serde_json::json!({
|
||||
"newAuthorizedPubkey": "new111",
|
||||
"authorizationType": "staker",
|
||||
"authoritySeed": "seed",
|
||||
"authorityOwner": "owner111",
|
||||
}),
|
||||
);
|
||||
let lockup = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"set_lockup_checked",
|
||||
crate::EventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("stake_account", "stake111"),
|
||||
account("lockup_custodian", "custodian111"),
|
||||
]),
|
||||
serde_json::json!({"epoch": 42}),
|
||||
);
|
||||
let authority_result = crate::EventMaterializer::materialize(&materializer, &authority);
|
||||
let lockup_result = crate::EventMaterializer::materialize(&materializer, &lockup);
|
||||
assert_eq!(authority_result.outputs[0].output_key, "stake_authority:authorize_with_seed:0");
|
||||
assert_eq!(
|
||||
authority_result.outputs[0].payload_json["projection"]["newAuthority"],
|
||||
"new111"
|
||||
);
|
||||
assert_eq!(lockup_result.outputs[0].output_key, "stake_lockup:set_lockup_checked:0");
|
||||
assert_eq!(
|
||||
lockup_result.outputs[0].payload_json["projection"]["checkedCustodianFromAccount"],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stake_value_projection_does_not_duplicate_lamport_changes() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let observation = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"withdraw",
|
||||
crate::EventFamily::Staking,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("stake_account", "stake111"),
|
||||
account("destination", "dest111"),
|
||||
]),
|
||||
serde_json::json!({"lamports": 99}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.outputs[0].output_key, "stake_value:withdraw:0");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["lamports"], 99);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["lamportBalanceChangesDuplicated"],
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_account_and_authority_events_are_projected() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let initialize = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"initialize_account_v2",
|
||||
crate::EventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
account("validator_identity", "validator111"),
|
||||
]),
|
||||
serde_json::json!({
|
||||
"authorizedVoter": "voter111",
|
||||
"authorizedWithdrawer": "withdrawer111",
|
||||
"inflationRewardsCommissionBasisPoints": 500,
|
||||
}),
|
||||
);
|
||||
let authorize = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"authorize_checked",
|
||||
crate::EventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
account("new_authority", "new111"),
|
||||
]),
|
||||
serde_json::json!({"authorizationType": {"kind": "voter"}, "newAuthorityFromAccount": true}),
|
||||
);
|
||||
let initialize_result = crate::EventMaterializer::materialize(&materializer, &initialize);
|
||||
let authorize_result = crate::EventMaterializer::materialize(&materializer, &authorize);
|
||||
assert_eq!(initialize_result.outputs[0].output_key, "vote_account:initialize_account_v2:0");
|
||||
assert_eq!(
|
||||
initialize_result.outputs[0].payload_json["projection"]["validatorIdentity"],
|
||||
"validator111"
|
||||
);
|
||||
assert_eq!(authorize_result.outputs[0].output_key, "vote_authority:authorize_checked:0");
|
||||
assert_eq!(
|
||||
authorize_result.outputs[0].payload_json["projection"]["newAuthority"],
|
||||
"new111"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_state_and_reward_outputs_are_distinguished() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let vote_state = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"tower_sync_switch",
|
||||
crate::EventFamily::Staking,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
account("vote_authority", "authority111"),
|
||||
]),
|
||||
serde_json::json!({"root": 10, "hash": "hash111", "switchProofHash": "switch111"}),
|
||||
);
|
||||
let reward = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"deposit_delegator_rewards",
|
||||
crate::EventFamily::Reward,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
account("deposit_source", "source111"),
|
||||
]),
|
||||
serde_json::json!({"depositLamports": 123}),
|
||||
);
|
||||
let vote_result = crate::EventMaterializer::materialize(&materializer, &vote_state);
|
||||
let reward_result = crate::EventMaterializer::materialize(&materializer, &reward);
|
||||
assert_eq!(vote_result.outputs[0].output_key, "vote_state:tower_sync_switch:0");
|
||||
assert_eq!(
|
||||
vote_result.outputs[0].payload_json["projection"]["switchProofHash"],
|
||||
"switch111"
|
||||
);
|
||||
assert_eq!(reward_result.outputs[0].family, crate::MaterializedEventFamily::Reward);
|
||||
assert_eq!(reward_result.outputs[0].output_key, "vote_reward:deposit_delegator_rewards:0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_staking_mutation_is_refused() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let observation = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"delegate_stake",
|
||||
crate::EventFamily::Staking,
|
||||
true,
|
||||
serde_json::json!([account("stake_account", "stake111")]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_or_disabled_observations_are_ignored() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let unrelated = observation(
|
||||
"solana_native_system",
|
||||
"create_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let redelegate = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"redelegate",
|
||||
crate::EventFamily::Staking,
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({"officiallyDisabled": true}),
|
||||
);
|
||||
let unrelated_result = crate::EventMaterializer::materialize(&materializer, &unrelated);
|
||||
let redelegate_result = crate::EventMaterializer::materialize(&materializer, &redelegate);
|
||||
assert_eq!(unrelated_result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
assert_eq!(redelegate_result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_serialization_is_deterministic() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let observation = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"update_commission_bps",
|
||||
crate::EventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([account("vote_account", "vote111")]),
|
||||
serde_json::json!({"commissionBasisPoints": 250, "commissionKind": "inflation_rewards"}),
|
||||
);
|
||||
let first = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let first_json_result = serde_json::to_string(&first.outputs[0].payload_json);
|
||||
let first_json = match first_json_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
|
||||
};
|
||||
let second_json_result = serde_json::to_string(&second.outputs[0].payload_json);
|
||||
let second_json = match second_json_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
|
||||
};
|
||||
assert_eq!(first_json, second_json);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user