|
|
|
|
@@ -0,0 +1,618 @@
|
|
|
|
|
// file: kb_materializer_transaction_annotations/src/materializer.rs
|
|
|
|
|
// version: 6
|
|
|
|
|
|
|
|
|
|
//! Exact committed SPL Memo transaction annotation projection.
|
|
|
|
|
|
|
|
|
|
const ACCEPTED_FAMILIES: &[kb_model::EventFamily] = &[kb_model::EventFamily::Audit];
|
|
|
|
|
const MEMO_ENTRY_NAMES: &[&str] = &["add_memo", "memo_intent", "invalid_memo_attempt"];
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
enum MemoGeneration {
|
|
|
|
|
V1,
|
|
|
|
|
V3,
|
|
|
|
|
V4,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl MemoGeneration {
|
|
|
|
|
fn from_event(event: &kb_model::DecodedProtocolEvent) -> std::option::Option<Self> {
|
|
|
|
|
if event.program_id.0 == kb_program_ids::SPL_MEMO_V1_PROGRAM_ID
|
|
|
|
|
&& event.surface_code.0 == "spl_memo_v1"
|
|
|
|
|
{
|
|
|
|
|
return std::option::Option::Some(Self::V1);
|
|
|
|
|
}
|
|
|
|
|
if event.program_id.0 == kb_program_ids::SPL_MEMO_V3_PROGRAM_ID
|
|
|
|
|
&& event.surface_code.0 == "spl_memo_v3"
|
|
|
|
|
{
|
|
|
|
|
return std::option::Option::Some(Self::V3);
|
|
|
|
|
}
|
|
|
|
|
if event.program_id.0 == kb_program_ids::SPL_MEMO_V4_PROGRAM_ID
|
|
|
|
|
&& event.surface_code.0 == "spl_memo_v4"
|
|
|
|
|
{
|
|
|
|
|
return std::option::Option::Some(Self::V4);
|
|
|
|
|
}
|
|
|
|
|
return std::option::Option::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn code(self) -> &'static str {
|
|
|
|
|
return match self {
|
|
|
|
|
Self::V1 => "v1",
|
|
|
|
|
Self::V3 => "v3",
|
|
|
|
|
Self::V4 => "v4",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn signer_status(self) -> &'static str {
|
|
|
|
|
return match self {
|
|
|
|
|
Self::V1 => "not_applicable",
|
|
|
|
|
Self::V3 | Self::V4 => "valid",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct MemoProjection {
|
|
|
|
|
generation: MemoGeneration,
|
|
|
|
|
memo_text: std::string::String,
|
|
|
|
|
payload_length_bytes: u64,
|
|
|
|
|
payload_sha256: std::string::String,
|
|
|
|
|
required_signers: std::vec::Vec<std::string::String>,
|
|
|
|
|
observed_signers: std::vec::Vec<std::string::String>,
|
|
|
|
|
verified_signers: std::vec::Vec<std::string::String>,
|
|
|
|
|
signer_validation_status: std::string::String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Stable transaction annotation materializer.
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
|
pub struct TransactionAnnotationMaterializer;
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::Materializer for crate::TransactionAnnotationMaterializer {
|
|
|
|
|
fn materializer_name(&self) -> &'static str {
|
|
|
|
|
return "kb_materializer_transaction_annotations";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materializer_version(&self) -> &'static str {
|
|
|
|
|
return env!("CARGO_PKG_VERSION");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_event(&self, event: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return accepts_event(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materialize_event(
|
|
|
|
|
&self,
|
|
|
|
|
_event: &kb_model::DecodedProtocolEvent,
|
|
|
|
|
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>> {
|
|
|
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::EventMaterializer for crate::TransactionAnnotationMaterializer {
|
|
|
|
|
fn identity(&self) -> kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
return kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
name: crate::PROCESSOR_NAME.to_string(),
|
|
|
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepted_families(&self) -> &'static [kb_model::EventFamily] {
|
|
|
|
|
return ACCEPTED_FAMILIES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_observation(&self, observation: &kb_decoder_api::DecodedObservation) -> bool {
|
|
|
|
|
return accepts_event(&observation.event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn transaction_policy(
|
|
|
|
|
&self,
|
|
|
|
|
_family: kb_model::EventFamily,
|
|
|
|
|
) -> kb_materializer_api::MaterializationTransactionPolicy {
|
|
|
|
|
return kb_materializer_api::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materialize(
|
|
|
|
|
&self,
|
|
|
|
|
observation: &kb_decoder_api::DecodedObservation,
|
|
|
|
|
) -> kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
if !accepts_event(&observation.event) {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::ignored();
|
|
|
|
|
}
|
|
|
|
|
if observation.transaction_failed || !observation.observation_committed {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::refused(
|
|
|
|
|
"uncommitted_transaction_annotation_refused",
|
|
|
|
|
"failed or uncommitted Memo observations cannot create transaction annotations",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if observation.event.event_name.0 != "add_memo" {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::refused(
|
|
|
|
|
"invalid_transaction_annotation_source_refused",
|
|
|
|
|
"only a committed add_memo observation can create a transaction annotation",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let projection_result = parse_projection(observation);
|
|
|
|
|
let projection = match projection_result {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(message) => {
|
|
|
|
|
return failed("memo_annotation_payload_invalid", message);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let idempotence_key = format!(
|
|
|
|
|
"spl_memo:{}:{}:{}:{}",
|
|
|
|
|
observation.event.signature.0,
|
|
|
|
|
observation.event.instruction_path.0,
|
|
|
|
|
observation.event.program_id.0,
|
|
|
|
|
projection.payload_sha256
|
|
|
|
|
);
|
|
|
|
|
let output = kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key: "transaction_annotation:spl_memo:0".to_string(),
|
|
|
|
|
family: kb_model::MaterializedEventFamily::TransactionAnnotation,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"projectionVersion": crate::PROJECTION_VERSION,
|
|
|
|
|
"domain": "transaction_annotation",
|
|
|
|
|
"annotationKind": "spl_memo",
|
|
|
|
|
"idempotenceKey": idempotence_key,
|
|
|
|
|
"signature": observation.event.signature.0.clone(),
|
|
|
|
|
"slot": observation.event.slot.0,
|
|
|
|
|
"instructionPath": observation.event.instruction_path.0.clone(),
|
|
|
|
|
"sourceKind": match observation.event.source_kind {
|
|
|
|
|
kb_model::EventSourceKind::InnerInstruction => "inner_instruction",
|
|
|
|
|
_ => "instruction"
|
|
|
|
|
},
|
|
|
|
|
"generation": projection.generation.code(),
|
|
|
|
|
"programId": observation.event.program_id.0.clone(),
|
|
|
|
|
"text": projection.memo_text,
|
|
|
|
|
"payloadLengthBytes": projection.payload_length_bytes,
|
|
|
|
|
"payloadSha256": projection.payload_sha256,
|
|
|
|
|
"signers": {
|
|
|
|
|
"validationStatus": projection.signer_validation_status,
|
|
|
|
|
"required": projection.required_signers,
|
|
|
|
|
"observed": projection.observed_signers,
|
|
|
|
|
"verified": projection.verified_signers
|
|
|
|
|
},
|
|
|
|
|
"committed": true,
|
|
|
|
|
"provenance": {
|
|
|
|
|
"processorName": crate::PROCESSOR_NAME,
|
|
|
|
|
"processorVersion": env!("CARGO_PKG_VERSION"),
|
|
|
|
|
"sourceProtocol": observation.event.protocol_code.0.clone(),
|
|
|
|
|
"sourceSurface": observation.event.surface_code.0.clone(),
|
|
|
|
|
"sourceEventCode": observation.event.event_code.0.clone(),
|
|
|
|
|
"sourceEventKey": observation.event_key.clone()
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
target: crate::TRACING_TARGET,
|
|
|
|
|
action = "materialize_transaction_annotation",
|
|
|
|
|
signature = %observation.event.signature.0,
|
|
|
|
|
instruction_path = %observation.event.instruction_path.0,
|
|
|
|
|
program_id = %observation.event.program_id.0,
|
|
|
|
|
generation = projection.generation.code(),
|
|
|
|
|
payload_length_bytes = projection.payload_length_bytes,
|
|
|
|
|
output_count = 1_usize,
|
|
|
|
|
"materialize committed SPL Memo transaction annotation"
|
|
|
|
|
);
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
status: kb_materializer_api::MaterializerOutcomeStatus::Inserted,
|
|
|
|
|
outputs: std::vec![output],
|
|
|
|
|
diagnostics: std::vec::Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_event(event: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return event.event_family == kb_model::EventFamily::Audit
|
|
|
|
|
&& event.protocol_code.0 == "spl_memo"
|
|
|
|
|
&& (event.source_kind == kb_model::EventSourceKind::Instruction
|
|
|
|
|
|| event.source_kind == kb_model::EventSourceKind::InnerInstruction)
|
|
|
|
|
&& MemoGeneration::from_event(event).is_some()
|
|
|
|
|
&& MEMO_ENTRY_NAMES.contains(&event.event_name.0.as_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_projection(
|
|
|
|
|
observation: &kb_decoder_api::DecodedObservation,
|
|
|
|
|
) -> std::result::Result<MemoProjection, std::string::String> {
|
|
|
|
|
let generation = match MemoGeneration::from_event(&observation.event) {
|
|
|
|
|
std::option::Option::Some(value) => value,
|
|
|
|
|
std::option::Option::None => {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo generation and Program ID do not match".to_string(),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let payload = &observation.payload_json;
|
|
|
|
|
if payload.get("committed").and_then(serde_json::Value::as_bool)
|
|
|
|
|
!= std::option::Option::Some(true)
|
|
|
|
|
{
|
|
|
|
|
return std::result::Result::Err("Memo payload is not marked committed".to_string());
|
|
|
|
|
}
|
|
|
|
|
if payload.get("transactionSucceeded").and_then(serde_json::Value::as_bool)
|
|
|
|
|
!= std::option::Option::Some(true)
|
|
|
|
|
|| payload.get("payloadComplete").and_then(serde_json::Value::as_bool)
|
|
|
|
|
!= std::option::Option::Some(true)
|
|
|
|
|
|| string_field(payload, "runtimeValidation")
|
|
|
|
|
!= std::option::Option::Some("proven_by_successful_transaction")
|
|
|
|
|
|| payload
|
|
|
|
|
.get("utf8Validation")
|
|
|
|
|
.and_then(|value| return string_field(value, "status"))
|
|
|
|
|
!= std::option::Option::Some("valid")
|
|
|
|
|
{
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo successful runtime validation proof is incomplete".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if string_field(payload, "generation") != std::option::Option::Some(generation.code()) {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo payload generation does not match its surface".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if string_field(payload, "programId")
|
|
|
|
|
!= std::option::Option::Some(observation.event.program_id.0.as_str())
|
|
|
|
|
{
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo payload Program ID does not match its event".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if string_field(payload, "instructionPath")
|
|
|
|
|
!= std::option::Option::Some(observation.event.instruction_path.0.as_str())
|
|
|
|
|
{
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo payload instruction path does not match its event".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let memo_text = match string_field(payload, "memoText") {
|
|
|
|
|
std::option::Option::Some(value) => value.to_string(),
|
|
|
|
|
std::option::Option::None => {
|
|
|
|
|
return std::result::Result::Err("committed Memo has no UTF-8 text".to_string());
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let payload_length_bytes =
|
|
|
|
|
match payload.get("payloadLengthBytes").and_then(serde_json::Value::as_u64) {
|
|
|
|
|
std::option::Option::Some(value) => value,
|
|
|
|
|
std::option::Option::None => {
|
|
|
|
|
return std::result::Result::Err("committed Memo has no byte length".to_string());
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let measured_length = match u64::try_from(memo_text.len()) {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(error) => {
|
|
|
|
|
return std::result::Result::Err(format!(
|
|
|
|
|
"Memo text length conversion failed: {error}"
|
|
|
|
|
));
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if measured_length != payload_length_bytes {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo byte length does not match its UTF-8 text".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if payload_length_bytes > crate::MAX_MEMO_PAYLOAD_BYTES {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo byte length exceeds the projection limit".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let payload_sha256 = match string_field(payload, "payloadSha256") {
|
|
|
|
|
std::option::Option::Some(value) if is_sha256(value) => value.to_string(),
|
|
|
|
|
_ => return std::result::Result::Err("Memo payload SHA-256 is invalid".to_string()),
|
|
|
|
|
};
|
|
|
|
|
let signer_validation = match payload.get("signerValidation") {
|
|
|
|
|
std::option::Option::Some(value) => value,
|
|
|
|
|
std::option::Option::None => {
|
|
|
|
|
return std::result::Result::Err("Memo signer validation is absent".to_string());
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let signer_validation_status = match string_field(signer_validation, "status") {
|
|
|
|
|
std::option::Option::Some(value) if value == generation.signer_status() => {
|
|
|
|
|
value.to_string()
|
|
|
|
|
},
|
|
|
|
|
_ => {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Memo signer validation status is inconsistent".to_string(),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let missing_signers = match string_array(signer_validation, "missingRequiredSigners") {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
|
|
|
|
};
|
|
|
|
|
if !missing_signers.is_empty() {
|
|
|
|
|
return std::result::Result::Err("committed Memo reports missing signers".to_string());
|
|
|
|
|
}
|
|
|
|
|
let required_signers = match string_array(signer_validation, "orderedRequiredSigners") {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
|
|
|
|
};
|
|
|
|
|
let observed_signers = match string_array(signer_validation, "orderedObservedSigners") {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
|
|
|
|
};
|
|
|
|
|
let verified_signers = if generation == MemoGeneration::V1 {
|
|
|
|
|
if !required_signers.is_empty() {
|
|
|
|
|
return std::result::Result::Err("Memo v1 cannot require signers".to_string());
|
|
|
|
|
}
|
|
|
|
|
std::vec::Vec::new()
|
|
|
|
|
} else {
|
|
|
|
|
if required_signers != observed_signers {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"committed Memo required and observed signers differ".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
observed_signers.clone()
|
|
|
|
|
};
|
|
|
|
|
return std::result::Result::Ok(MemoProjection {
|
|
|
|
|
generation,
|
|
|
|
|
memo_text,
|
|
|
|
|
payload_length_bytes,
|
|
|
|
|
payload_sha256,
|
|
|
|
|
required_signers,
|
|
|
|
|
observed_signers,
|
|
|
|
|
verified_signers,
|
|
|
|
|
signer_validation_status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn string_field<'a>(value: &'a serde_json::Value, key: &str) -> std::option::Option<&'a str> {
|
|
|
|
|
return value.get(key).and_then(serde_json::Value::as_str);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn string_array(
|
|
|
|
|
value: &serde_json::Value,
|
|
|
|
|
key: &str,
|
|
|
|
|
) -> std::result::Result<std::vec::Vec<std::string::String>, std::string::String> {
|
|
|
|
|
let values = match value.get(key).and_then(serde_json::Value::as_array) {
|
|
|
|
|
std::option::Option::Some(values) => values,
|
|
|
|
|
std::option::Option::None => {
|
|
|
|
|
return std::result::Result::Err(format!("Memo signer field {key} is not an array"));
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let mut output = std::vec::Vec::with_capacity(values.len());
|
|
|
|
|
for item in values {
|
|
|
|
|
let text = match item.as_str() {
|
|
|
|
|
std::option::Option::Some(text) if !text.trim().is_empty() => text,
|
|
|
|
|
_ => {
|
|
|
|
|
return std::result::Result::Err(format!(
|
|
|
|
|
"Memo signer field {key} contains an invalid pubkey"
|
|
|
|
|
));
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
output.push(text.to_string());
|
|
|
|
|
}
|
|
|
|
|
return std::result::Result::Ok(output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_sha256(value: &str) -> bool {
|
|
|
|
|
return value.len() == 64
|
|
|
|
|
&& value
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|byte| return byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn failed(
|
|
|
|
|
code: &str,
|
|
|
|
|
message: std::string::String,
|
|
|
|
|
) -> kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
status: kb_materializer_api::MaterializerOutcomeStatus::Failed,
|
|
|
|
|
outputs: std::vec::Vec::new(),
|
|
|
|
|
diagnostics: std::vec![kb_materializer_api::MaterializerDiagnostic {
|
|
|
|
|
code: code.to_string(),
|
|
|
|
|
message,
|
|
|
|
|
retriable: false,
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
fn observation(
|
|
|
|
|
program_id: &str,
|
|
|
|
|
surface_code: &str,
|
|
|
|
|
generation: &str,
|
|
|
|
|
event_name: &str,
|
|
|
|
|
failed: bool,
|
|
|
|
|
committed: bool,
|
|
|
|
|
required_signers: serde_json::Value,
|
|
|
|
|
observed_signers: serde_json::Value,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return kb_decoder_api::DecodedObservation {
|
|
|
|
|
event_key: "memo:0".to_string(),
|
|
|
|
|
event: kb_model::DecodedProtocolEvent {
|
|
|
|
|
signature: kb_model::Signature("signature".to_string()),
|
|
|
|
|
slot: kb_model::Slot(42),
|
|
|
|
|
instruction_path: kb_model::InstructionPath("1/0".to_string()),
|
|
|
|
|
program_id: kb_model::ProgramId(program_id.to_string()),
|
|
|
|
|
protocol_code: kb_model::ProtocolCode("spl_memo".to_string()),
|
|
|
|
|
surface_code: kb_model::SurfaceCode(surface_code.to_string()),
|
|
|
|
|
event_code: kb_model::EventCode(format!("{surface_code}.{event_name}")),
|
|
|
|
|
event_name: kb_model::EventName(event_name.to_string()),
|
|
|
|
|
event_family: kb_model::EventFamily::Audit,
|
|
|
|
|
source_kind: kb_model::EventSourceKind::InnerInstruction,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::ManualExact,
|
|
|
|
|
},
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"generation": generation,
|
|
|
|
|
"programId": program_id,
|
|
|
|
|
"instructionPath": "1/0",
|
|
|
|
|
"transactionSucceeded": !failed,
|
|
|
|
|
"committed": committed,
|
|
|
|
|
"memoText": "memo",
|
|
|
|
|
"payloadLengthBytes": 4,
|
|
|
|
|
"payloadSha256": "1111111111111111111111111111111111111111111111111111111111111111",
|
|
|
|
|
"payloadComplete": true,
|
|
|
|
|
"utf8Validation": {"status":"valid","invalidFromByte":null},
|
|
|
|
|
"signerValidation": {
|
|
|
|
|
"status": if generation == "v1" { "not_applicable" } else { "valid" },
|
|
|
|
|
"orderedRequiredSigners": required_signers,
|
|
|
|
|
"orderedObservedSigners": observed_signers,
|
|
|
|
|
"missingRequiredSigners": []
|
|
|
|
|
},
|
|
|
|
|
"runtimeValidation": if failed {
|
|
|
|
|
"not_proven_transaction_failed"
|
|
|
|
|
} else {
|
|
|
|
|
"proven_by_successful_transaction"
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
transaction_failed: failed,
|
|
|
|
|
transaction_error: if failed {
|
|
|
|
|
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
|
|
|
|
|
} else {
|
|
|
|
|
std::option::Option::None
|
|
|
|
|
},
|
|
|
|
|
observation_committed: committed,
|
|
|
|
|
proof: kb_decoder_api::DecoderProof {
|
|
|
|
|
kind: kb_decoder_api::DecoderProofKind::Manual,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::ManualExact,
|
|
|
|
|
evidence: std::vec!["memo".to_string()],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn v4() -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return observation(
|
|
|
|
|
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
|
|
|
|
"spl_memo_v4",
|
|
|
|
|
"v4",
|
|
|
|
|
"add_memo",
|
|
|
|
|
false,
|
|
|
|
|
true,
|
|
|
|
|
serde_json::json!(["signer", "signer"]),
|
|
|
|
|
serde_json::json!(["signer", "signer"]),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn committed_v4_memo_becomes_deterministic_transaction_annotation() {
|
|
|
|
|
let materializer = crate::TransactionAnnotationMaterializer;
|
|
|
|
|
let first = kb_materializer_api::EventMaterializer::materialize(&materializer, &v4());
|
|
|
|
|
let second = kb_materializer_api::EventMaterializer::materialize(&materializer, &v4());
|
|
|
|
|
assert_eq!(first, second);
|
|
|
|
|
assert_eq!(first.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(first.outputs.len(), 1);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.outputs[0].family,
|
|
|
|
|
kb_model::MaterializedEventFamily::TransactionAnnotation
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.outputs[0].payload_json["signers"]["verified"],
|
|
|
|
|
serde_json::json!(["signer", "signer"])
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(first.outputs[0].payload_json["committed"], true);
|
|
|
|
|
assert!(first.validate().is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn exact_v1_v3_and_v4_surfaces_are_accepted() {
|
|
|
|
|
for (program_id, surface, generation) in [
|
|
|
|
|
(kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, "spl_memo_v1", "v1"),
|
|
|
|
|
(kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, "spl_memo_v3", "v3"),
|
|
|
|
|
(kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, "spl_memo_v4", "v4"),
|
|
|
|
|
] {
|
|
|
|
|
let observation = observation(
|
|
|
|
|
program_id,
|
|
|
|
|
surface,
|
|
|
|
|
generation,
|
|
|
|
|
"add_memo",
|
|
|
|
|
false,
|
|
|
|
|
true,
|
|
|
|
|
serde_json::json!([]),
|
|
|
|
|
serde_json::json!([]),
|
|
|
|
|
);
|
|
|
|
|
assert!(kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&crate::TransactionAnnotationMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn v1_observed_accounts_are_not_claimed_as_runtime_verified() {
|
|
|
|
|
let observation = observation(
|
|
|
|
|
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
|
|
|
|
|
"spl_memo_v1",
|
|
|
|
|
"v1",
|
|
|
|
|
"add_memo",
|
|
|
|
|
false,
|
|
|
|
|
true,
|
|
|
|
|
serde_json::json!([]),
|
|
|
|
|
serde_json::json!(["observed_only"]),
|
|
|
|
|
);
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::TransactionAnnotationMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["signers"]["verified"], serde_json::json!([]));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["signers"]["observed"],
|
|
|
|
|
serde_json::json!(["observed_only"])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failed_or_uncommitted_memo_is_refused_without_output() {
|
|
|
|
|
let observation = observation(
|
|
|
|
|
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
|
|
|
|
"spl_memo_v4",
|
|
|
|
|
"v4",
|
|
|
|
|
"memo_intent",
|
|
|
|
|
true,
|
|
|
|
|
false,
|
|
|
|
|
serde_json::json!([]),
|
|
|
|
|
serde_json::json!([]),
|
|
|
|
|
);
|
|
|
|
|
let materializer = crate::TransactionAnnotationMaterializer;
|
|
|
|
|
let policy =
|
|
|
|
|
kb_materializer_api::validate_materialization_policy(&materializer, &observation);
|
|
|
|
|
assert!(policy.is_err());
|
|
|
|
|
let result =
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Refused);
|
|
|
|
|
assert!(result.outputs.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn malformed_committed_payload_fails_closed() {
|
|
|
|
|
let mut observation = v4();
|
|
|
|
|
observation.payload_json["payloadSha256"] = serde_json::Value::String("bad".to_string());
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::TransactionAnnotationMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Failed);
|
|
|
|
|
assert!(result.outputs.is_empty());
|
|
|
|
|
assert_eq!(result.diagnostics[0].code, "memo_annotation_payload_invalid");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn foreign_audit_observation_is_not_accepted() {
|
|
|
|
|
let mut observation = v4();
|
|
|
|
|
observation.event.protocol_code = kb_model::ProtocolCode("foreign".to_string());
|
|
|
|
|
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&crate::TransactionAnnotationMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
));
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::TransactionAnnotationMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Ignored);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn machine_readable_matrix_declares_transaction_annotation_policy() {
|
|
|
|
|
let parsed = serde_json::from_str::<serde_json::Value>(include_str!(
|
|
|
|
|
"../../docs/SPL_MEMO_MATRIX.json"
|
|
|
|
|
));
|
|
|
|
|
let matrix = match parsed {
|
|
|
|
|
std::result::Result::Ok(value) => value,
|
|
|
|
|
std::result::Result::Err(error) => panic!("memo matrix is invalid JSON: {error}"),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(matrix["matrixVersion"], 4);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
matrix["materializationContract"]["crate"],
|
|
|
|
|
"kb_materializer_transaction_annotations"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
matrix["materializationContract"]["transactionPolicy"],
|
|
|
|
|
"SuccessfulCommittedOnly"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false);
|
|
|
|
|
}
|
|
|
|
|
}
|