v0.4.8-pre.007
This commit is contained in:
@@ -1,611 +1,25 @@
|
||||
// file: kb-lib/src/materializer/transaction/annotations.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Exact committed SPL Memo transaction annotation projection.
|
||||
//! Committed Memo transaction annotation materializer component.
|
||||
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Audit];
|
||||
const MEMO_ENTRY_NAMES: &[&str] = &["add_memo", "memo_intent", "invalid_memo_attempt"];
|
||||
mod constants;
|
||||
mod materializer;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum MemoGeneration {
|
||||
V1,
|
||||
V3,
|
||||
V4,
|
||||
}
|
||||
/// Stable committed Memo transaction annotation materializer.
|
||||
pub use self::materializer::MtTransactionAnnotationMaterializer;
|
||||
|
||||
impl MemoGeneration {
|
||||
fn from_event(event: &crate::MdDecodedProtocolEvent) -> 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 MtTransactionAnnotationMaterializer;
|
||||
|
||||
impl crate::MtMaterializer for crate::MtTransactionAnnotationMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb-lib.materializer.transaction.annotations";
|
||||
}
|
||||
|
||||
fn materializer_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return accepts_event(event);
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
_event: &crate::MdDecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::MtApiEventMaterializer for crate::MtTransactionAnnotationMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME.to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
return accepts_event(&observation.event);
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
if !accepts_event(&observation.event) {
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"uncommitted_transaction_annotation_refused",
|
||||
"failed or uncommitted Memo observations cannot create transaction annotations",
|
||||
);
|
||||
}
|
||||
if observation.event.event_name.0 != "add_memo" {
|
||||
return crate::MtApiMaterializerExecutionResult::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 = crate::MtApiMaterializedOutput {
|
||||
output_key: "transaction_annotation:spl_memo:0".to_string(),
|
||||
family: crate::MdMaterializedEventFamily::TransactionAnnotation,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_TRANSACTION_ANNOTATIONS_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 {
|
||||
crate::MdEventSourceKind::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::MT_TRANSACTION_ANNOTATIONS_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_MATERIALIZER_TRANSACTION_ANNOTATIONS,
|
||||
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 crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return event.event_family == crate::MdEventFamily::Audit
|
||||
&& event.protocol_code.0 == "spl.memo"
|
||||
&& (event.source_kind == crate::MdEventSourceKind::Instruction
|
||||
|| event.source_kind == crate::MdEventSourceKind::InnerInstruction)
|
||||
&& MemoGeneration::from_event(event).is_some()
|
||||
&& MEMO_ENTRY_NAMES.contains(&event.event_name.0.as_str());
|
||||
}
|
||||
|
||||
fn parse_projection(
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> 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::MT_TRANSACTION_ANNOTATIONS_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) -> crate::MtApiMaterializerExecutionResult {
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Failed,
|
||||
outputs: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![crate::MtApiMaterializerDiagnostic {
|
||||
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,
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: "memo:0".to_string(),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("1/0".to_string()),
|
||||
program_id: crate::MdProgramId(program_id.to_string()),
|
||||
protocol_code: crate::MdProtocolCode("spl.memo".to_string()),
|
||||
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::MdEventCode(format!("{surface_code}.{event_name}")),
|
||||
event_name: crate::MdEventName(event_name.to_string()),
|
||||
event_family: crate::MdEventFamily::Audit,
|
||||
source_kind: crate::MdEventSourceKind::InnerInstruction,
|
||||
confidence: crate::MdDecoderConfidence::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: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["memo".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn v4() -> crate::DcApiDecodedObservation {
|
||||
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::MtTransactionAnnotationMaterializer;
|
||||
let first = crate::MtApiEventMaterializer::materialize(&materializer, &v4());
|
||||
let second = crate::MtApiEventMaterializer::materialize(&materializer, &v4());
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(first.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(first.outputs.len(), 1);
|
||||
assert_eq!(
|
||||
first.outputs[0].family,
|
||||
crate::MdMaterializedEventFamily::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!(crate::MtApiEventMaterializer::accepts_observation(
|
||||
&crate::MtTransactionAnnotationMaterializer,
|
||||
&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 = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTransactionAnnotationMaterializer,
|
||||
&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::MtTransactionAnnotationMaterializer;
|
||||
let policy =
|
||||
crate::materializer_api_validate_materialization_policy(&materializer, &observation);
|
||||
assert!(policy.is_err());
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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 = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTransactionAnnotationMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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 = crate::MdProtocolCode("foreign".to_string());
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&crate::MtTransactionAnnotationMaterializer,
|
||||
&observation,
|
||||
));
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTransactionAnnotationMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_readable_matrix_declares_transaction_annotation_policy() {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(include_str!(
|
||||
"../../../../test-fixtures/contract-matrices/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-lib");
|
||||
assert_eq!(
|
||||
matrix["materializationContract"]["transactionPolicy"],
|
||||
"SuccessfulCommittedOnly"
|
||||
);
|
||||
assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false);
|
||||
}
|
||||
}
|
||||
/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES` component constant.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_ACCEPTED_FAMILIES;
|
||||
/// Stable runtime component name for transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_COMPONENT_NAME;
|
||||
/// Maximum Memo payload accepted by transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES;
|
||||
/// Crate-visible `MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES` component constant.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MEMO_ENTRY_NAMES;
|
||||
/// Stable processor name for transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for transaction annotations.
|
||||
pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_TRANSACTION_ANNOTATIONS;
|
||||
|
||||
Reference in New Issue
Block a user