578 lines
24 KiB
Rust
578 lines
24 KiB
Rust
// file: kb-lib/src/materializer/compliance/audit.rs
|
|
// version: 7
|
|
|
|
//! Stable compliance-audit projections for committed native bytecode mutations and execution profiles.
|
|
|
|
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"];
|
|
|
|
#[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);
|
|
}
|
|
}
|