// file: kb-lib/src/decoder/solana/core/slashing.rs // version: 3 //! Exact structural decoding for the enshrined stateless Slashing Program. const SOURCE: &str = "solana-program/slashing@fe8da3a instruction contract and Agave v4.1.1 stateless builtin verified build"; const DUPLICATE_BLOCK_PROOF_DATA_BYTES: usize = 304; const DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES: usize = 305; const SIGNATURE_BYTES: usize = 64; const HASH_BYTES: usize = 32; const PUBKEY_BYTES: usize = 32; const CLOSE_ROLES: &[crate::SolanaCoreAccountRole] = &[ crate::SolanaCoreAccountRole::new("violation_report", true, false), crate::SolanaCoreAccountRole::new("destination", true, false), ]; const DUPLICATE_BLOCK_ROLES: &[crate::SolanaCoreAccountRole] = &[ crate::SolanaCoreAccountRole::new("proof_account", false, false), crate::SolanaCoreAccountRole::new("violation_report", true, false), crate::SolanaCoreAccountRole::new("instructions_sysvar", false, false), crate::SolanaCoreAccountRole::new("system_program", false, false), ]; /// Returns declared Slashing Program instruction coverage. pub(crate) fn slashing_coverage() -> std::vec::Vec { return vec![ crate::DecoderCoverageDeclaration { program_id: kb_program_ids::SLASHING_PROGRAM_ID.to_string(), surface_code: std::option::Option::Some( crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string(), ), entry_kind: crate::DecoderCoverageEntryKind::Instruction, entry_code: "close_violation_report".to_string(), discriminator_hex: std::option::Option::Some("00".to_string()), historical: false, }, crate::DecoderCoverageDeclaration { program_id: kb_program_ids::SLASHING_PROGRAM_ID.to_string(), surface_code: std::option::Option::Some( crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string(), ), entry_kind: crate::DecoderCoverageEntryKind::Instruction, entry_code: "duplicate_block_proof".to_string(), discriminator_hex: std::option::Option::Some("01".to_string()), historical: false, }, ]; } /// Recognizes one Slashing Program instruction without producing an event. pub(crate) fn slashing_recognize( input: &crate::CoreInstructionReplayInput, priority: u16, ) -> crate::DecoderRecognition { let bytes_result = crate::solana_core_decode_instruction_data(input); let bytes = match bytes_result { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => { return crate::DecoderRecognition::compatible( false, priority, std::option::Option::Some(crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string()), std::option::Option::Some("malformed_slashing_instruction".to_string()), std::option::Option::None, ); }, }; let (entry_code, exact) = match bytes.first() { std::option::Option::Some(0) => ("close_violation_report", bytes.len() == 1), std::option::Option::Some(1) => { ("duplicate_block_proof", bytes.len() == DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES) }, std::option::Option::Some(_value) => ("unknown_slashing_instruction", false), std::option::Option::None => ("malformed_slashing_instruction", false), }; return crate::DecoderRecognition::compatible( exact, priority, std::option::Option::Some(crate::SOLANA_CORE_SLASHING_SURFACE_CODE.to_string()), std::option::Option::Some(entry_code.to_string()), crate::solana_core_hexadecimal_prefix(bytes.as_slice(), bytes.len().min(1)), ); } /// Decodes one Slashing Program instruction. pub(crate) fn slashing_decode( input: &crate::CoreInstructionReplayInput, ) -> crate::DecoderExecutionResult { let bytes_result = crate::solana_core_decode_instruction_data(input); let bytes = match bytes_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return crate::solana_core_failed_result( std::option::Option::Some("malformed_slashing_instruction"), "slashing_payload_invalid", error.to_string(), ); }, }; let tag = match bytes.first() { std::option::Option::Some(value) => *value, std::option::Option::None => { return crate::solana_core_failed_result( std::option::Option::Some("malformed_slashing_instruction"), "slashing_payload_empty", "Slashing Program instruction payload is empty", ); }, }; return match tag { 0 => decode_close_violation_report(input, bytes.as_slice()), 1 => decode_duplicate_block_proof(input, bytes.as_slice()), _ => crate::solana_core_unsupported_result( "unknown_slashing_instruction", "slashing_tag_unknown", format!( "unknown Slashing Program instruction tag {tag}; payload_sha256={}", crate::solana_core_payload_hash(input) ), ), }; } fn decode_close_violation_report( input: &crate::CoreInstructionReplayInput, bytes: &[u8], ) -> crate::DecoderExecutionResult { if bytes.len() != 1 { return crate::solana_core_failed_result( std::option::Option::Some("close_violation_report"), "slashing_close_size_invalid", format!("close_violation_report requires exactly 1 byte but received {}", bytes.len()), ); } let accounts_result = crate::solana_core_resolve_accounts(input, CLOSE_ROLES, 2, std::option::Option::None); let accounts = match accounts_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return crate::solana_core_failed_result( std::option::Option::Some("close_violation_report"), "slashing_close_accounts_invalid", error.to_string(), ); }, }; return crate::solana_core_decoded_result( input, crate::SOLANA_CORE_SLASHING_SURFACE_CODE, "close_violation_report", crate::EventFamily::Lifecycle, false, accounts, serde_json::json!({ "proofType": "duplicate_block", "minimumRetentionEpochs": 3, "runtimeMutation": runtime_mutation( input, "violation_report_closed_lamports_transferred_owner_reset_to_system_program", ), "transactionFinalAccountStateCaptured": false, }), SOURCE, ); } fn decode_duplicate_block_proof( input: &crate::CoreInstructionReplayInput, bytes: &[u8], ) -> crate::DecoderExecutionResult { if bytes.len() != DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES { return crate::solana_core_failed_result( std::option::Option::Some("duplicate_block_proof"), "slashing_duplicate_block_size_invalid", format!( "duplicate_block_proof requires {} bytes but received {}", DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES, bytes.len() ), ); } let accounts_result = crate::solana_core_resolve_accounts( input, DUPLICATE_BLOCK_ROLES, 4, std::option::Option::None, ); let accounts = match accounts_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return crate::solana_core_failed_result( std::option::Option::Some("duplicate_block_proof"), "slashing_duplicate_block_accounts_invalid", error.to_string(), ); }, }; let fixed_accounts_result = validate_duplicate_block_fixed_accounts(&accounts); if let std::result::Result::Err(error) = fixed_accounts_result { return crate::solana_core_failed_result( std::option::Option::Some("duplicate_block_proof"), "slashing_duplicate_block_fixed_account_invalid", error.to_string(), ); } let parsed_result = parse_duplicate_block_parameters(input, bytes); let parameters = match parsed_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return crate::solana_core_failed_result( std::option::Option::Some("duplicate_block_proof"), "slashing_duplicate_block_data_invalid", error.to_string(), ); }, }; return crate::solana_core_decoded_result( input, crate::SOLANA_CORE_SLASHING_SURFACE_CODE, "duplicate_block_proof", crate::EventFamily::Audit, false, accounts, parameters, SOURCE, ); } fn parse_duplicate_block_parameters( input: &crate::CoreInstructionReplayInput, bytes: &[u8], ) -> kb_core::Result { let offset_result = crate::solana_core_read_u64_le(bytes, 1); let offset = match offset_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let slot_result = crate::solana_core_read_u64_le(bytes, 9); let slot = match slot_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let node_pubkey_result = read_pubkey(bytes, 17, "slashing node pubkey"); let node_pubkey = match node_pubkey_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let reporter_result = read_pubkey(bytes, 49, "slashing reporter"); let reporter = match reporter_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let destination_result = read_pubkey(bytes, 81, "slashing destination"); let destination = match destination_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let shred_1_root_result = crate::solana_core_bounded_slice( bytes, 113, HASH_BYTES, "slashing first shred merkle root", ); let shred_1_root = match shred_1_root_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let shred_1_signature_result = crate::solana_core_bounded_slice( bytes, 145, SIGNATURE_BYTES, "slashing first shred signature", ); let shred_1_signature = match shred_1_signature_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let shred_2_root_result = crate::solana_core_bounded_slice( bytes, 209, HASH_BYTES, "slashing second shred merkle root", ); let shred_2_root = match shred_2_root_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let shred_2_signature_result = crate::solana_core_bounded_slice( bytes, 241, SIGNATURE_BYTES, "slashing second shred signature", ); let shred_2_signature = match shred_2_signature_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(serde_json::json!({ "proofType": "duplicate_block", "proofAccountOffset": offset, "violationSlot": slot, "nodePubkey": node_pubkey, "reporter": reporter, "destination": destination, "shred1MerkleRoot": hash_summary(shred_1_root), "shred1Signature": signature_summary(shred_1_signature), "shred2MerkleRoot": hash_summary(shred_2_root), "shred2Signature": signature_summary(shred_2_signature), "proofAccountDataCaptured": false, "proofAccountDataSemantics": "external_account_not_captured_by_transaction_core", "reportAccountMustBePrefunded": true, "prefundingProvenanceInspectedByDecoder": false, "violationReportPdaDerivationVerifiedByDecoder": false, "precedingEd25519Instruction": preceding_ed25519_instruction(input), "cryptographicVerificationPerformedByDecoder": false, "runtimeVerification": if input.transaction_failed { "not_asserted_transaction_failed" } else { "accepted_in_successful_transaction" }, "runtimeMutation": runtime_mutation( input, "violation_report_pda_assigned_allocated_and_data_stored_after_duplicate_block_proof_acceptance", ), "penaltyAppliedByProgram": false, "programSemantics": "records_verified_violation_report_for_external_consensus_enforcement", "instructionDataBytes": DUPLICATE_BLOCK_PROOF_DATA_BYTES, })); } fn validate_duplicate_block_fixed_accounts(accounts: &serde_json::Value) -> kb_core::Result<()> { let array = match accounts.as_array() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "resolved Slashing Program accounts must be an array", )); }, }; for (position, expected) in [ (2_usize, kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID), (3_usize, kb_program_ids::SYSTEM_PROGRAM_ID), ] { let actual = array .get(position) .and_then(|value| return value.get("accountKey")) .and_then(serde_json::Value::as_str); if actual != std::option::Option::Some(expected) { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Slashing Program account {position} must be {expected}" ))); } } return std::result::Result::Ok(()); } fn read_pubkey(bytes: &[u8], offset: usize, label: &str) -> kb_core::Result { let slice_result = crate::solana_core_bounded_slice(bytes, offset, PUBKEY_BYTES, label); return match slice_result { std::result::Result::Ok(value) => { std::result::Result::Ok(bs58::encode(value).into_string()) }, std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn hash_summary(bytes: &[u8]) -> serde_json::Value { return serde_json::json!({ "length": bytes.len(), "hex": crate::solana_core_bounded_hexadecimal_prefix(bytes, bytes.len()), "sha256": crate::solana_core_hash_bytes(bytes), }); } fn signature_summary(bytes: &[u8]) -> serde_json::Value { return serde_json::json!({ "length": bytes.len(), "prefixHex": crate::solana_core_bounded_hexadecimal_prefix( bytes, crate::SOLANA_CORE_PRECOMPILE_COMPONENT_PREFIX_BYTES, ), "sha256": crate::solana_core_hash_bytes(bytes), }); } fn preceding_ed25519_instruction(input: &crate::CoreInstructionReplayInput) -> serde_json::Value { let target_index = match crate::solana_core_target_outer_instruction_index(input) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return serde_json::json!({ "requiredRelativeInstructionOffset": -1, "expectedProgramId": kb_program_ids::ED25519_PROGRAM_ID, "resolved": false, "matchesExpectedProgram": false, "diagnostic": error.to_string(), }); }, }; let previous_index = match target_index.checked_sub(1) { std::option::Option::Some(value) => value, std::option::Option::None => { return serde_json::json!({ "requiredRelativeInstructionOffset": -1, "currentInstructionIndex": target_index, "expectedProgramId": kb_program_ids::ED25519_PROGRAM_ID, "resolved": false, "matchesExpectedProgram": false, "diagnostic": "target instruction has no preceding outer instruction", }); }, }; let previous = input.outer_instructions_json.as_array().and_then(|instructions| { return instructions.iter().find(|instruction| { return instruction .get("instructionIndex") .and_then(serde_json::Value::as_u64) .and_then(|value| return usize::try_from(value).ok()) == std::option::Option::Some(previous_index); }); }); let program_id = previous .and_then(|instruction| return instruction.get("programId")) .and_then(serde_json::Value::as_str); let instruction_path = previous .and_then(|instruction| return instruction.get("instructionPath")) .and_then(serde_json::Value::as_str); return serde_json::json!({ "requiredRelativeInstructionOffset": -1, "currentInstructionIndex": target_index, "resolvedInstructionIndex": previous_index, "resolvedInstructionPath": instruction_path, "expectedProgramId": kb_program_ids::ED25519_PROGRAM_ID, "resolvedProgramId": program_id, "resolved": previous.is_some(), "matchesExpectedProgram": program_id == std::option::Option::Some(kb_program_ids::ED25519_PROGRAM_ID), "expectedSignatureCount": 2, "signatureTableInspectedByDecoder": false, }); } fn runtime_mutation( input: &crate::CoreInstructionReplayInput, successful_value: &'static str, ) -> &'static str { return if input.transaction_failed { "not_asserted_transaction_failed" } else { successful_value }; } #[cfg(test)] mod tests { use base64::Engine; // rust-rules: trait-import fn duplicate_payload() -> std::vec::Vec { let mut bytes = vec![1_u8]; bytes.extend_from_slice(&34_u64.to_le_bytes()); bytes.extend_from_slice(&42_u64.to_le_bytes()); bytes.extend_from_slice(&[1_u8; 32]); bytes.extend_from_slice(&[2_u8; 32]); bytes.extend_from_slice(&[3_u8; 32]); bytes.extend_from_slice(&[4_u8; 32]); bytes.extend_from_slice(&[5_u8; 64]); bytes.extend_from_slice(&[6_u8; 32]); bytes.extend_from_slice(&[7_u8; 64]); return bytes; } fn replay_input( bytes: &[u8], transaction_failed: bool, instruction_path: &str, ) -> crate::CoreInstructionReplayInput { let keys = [ "Proof1111111111111111111111111111111111111", "Report111111111111111111111111111111111111", kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID, kb_program_ids::SYSTEM_PROGRAM_ID, ]; let account_keys = keys .iter() .enumerate() .map(|(index, key)| { return serde_json::json!({ "accountIndex": index, "accountKey": key, "source": "static", "writable": index == 1, "signer": false, "executable": index == 3, }); }) .collect::>(); let instruction_accounts = keys .iter() .enumerate() .map(|(index, key)| { return serde_json::json!({"accountIndex": index, "accountKey": key}); }) .collect::>(); let current_index = instruction_path.parse::().unwrap_or(2); let previous_index = current_index.saturating_sub(1); let outer = serde_json::json!([ { "instructionIndex": previous_index, "instructionPath": previous_index.to_string(), "programId": kb_program_ids::ED25519_PROGRAM_ID, "payloadJson": {"dataBase64": ""}, "payloadHash": "ed25519-hash" }, { "instructionIndex": current_index, "instructionPath": instruction_path, "programId": kb_program_ids::SLASHING_PROGRAM_ID, "payloadJson": { "dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes), }, "payloadHash": "slashing-hash" } ]); let result = crate::CoreInstructionReplayInput::new( format!("signature:{instruction_path}"), "signature", 42, instruction_path, kb_program_ids::SLASHING_PROGRAM_ID, transaction_failed, if transaction_failed { std::option::Option::Some(serde_json::json!({"InstructionError": [2, "Custom"]})) } else { std::option::Option::None }, serde_json::Value::Array(account_keys), serde_json::Value::Array(instruction_accounts), std::option::Option::Some(serde_json::json!({ "dataBase64": base64::engine::general_purpose::STANDARD.encode(bytes), })), std::option::Option::Some("payload-hash".to_string()), outer, serde_json::json!([]), serde_json::json!([]), serde_json::json!([]), ); return match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Slashing replay input failed: {error}"), }; } fn close_replay_input(transaction_failed: bool) -> crate::CoreInstructionReplayInput { let mut input = replay_input(&[0], transaction_failed, "0"); input.account_keys_json = serde_json::json!([ { "accountIndex": 0, "accountKey": "Report111111111111111111111111111111111111", "source": "static", "writable": true, "signer": false, "executable": false }, { "accountIndex": 1, "accountKey": "Destination11111111111111111111111111111111", "source": "static", "writable": true, "signer": false, "executable": false } ]); input.instruction_accounts_json = serde_json::json!([ {"accountIndex": 0, "accountKey": "Report111111111111111111111111111111111111"}, {"accountIndex": 1, "accountKey": "Destination11111111111111111111111111111111"} ]); return input; } #[test] fn coverage_declares_exactly_two_official_instructions() { let coverage = crate::solana_core_slashing_coverage(); assert_eq!(coverage.len(), 2); assert_eq!(coverage[0].entry_code, "close_violation_report"); assert_eq!(coverage[1].entry_code, "duplicate_block_proof"); } #[test] fn close_violation_report_decodes_exact_runtime_contract() { let result = crate::solana_core_slashing_decode(&close_replay_input(false)); assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); assert_eq!( result.recognized_entry_code.as_deref(), std::option::Option::Some("close_violation_report") ); let parameters = &result.observations[0].payload_json["parameters"]; assert_eq!(parameters["minimumRetentionEpochs"], 3); assert_eq!( parameters["runtimeMutation"], "violation_report_closed_lamports_transferred_owner_reset_to_system_program" ); } #[test] fn duplicate_block_proof_decodes_exact_official_layout() { let payload = duplicate_payload(); assert_eq!(payload.len(), super::DUPLICATE_BLOCK_PROOF_INSTRUCTION_BYTES); let result = crate::solana_core_slashing_decode(&replay_input(payload.as_slice(), false, "2")); assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); let parameters = &result.observations[0].payload_json["parameters"]; assert_eq!(parameters["proofAccountOffset"], 34); assert_eq!(parameters["violationSlot"], 42); assert_eq!(parameters["shred1Signature"]["length"], 64); assert_eq!(parameters["shred2MerkleRoot"]["length"], 32); assert_eq!(parameters["proofAccountDataCaptured"], false); assert_eq!(parameters["reportAccountMustBePrefunded"], true); assert_eq!(parameters["prefundingProvenanceInspectedByDecoder"], false); assert_eq!(parameters["violationReportPdaDerivationVerifiedByDecoder"], false); assert_eq!(parameters["cryptographicVerificationPerformedByDecoder"], false); assert_eq!(parameters["penaltyAppliedByProgram"], false); } #[test] fn duplicate_block_proof_reports_preceding_ed25519_context() { let result = crate::solana_core_slashing_decode(&replay_input( duplicate_payload().as_slice(), false, "2", )); let preceding = &result.observations[0].payload_json["parameters"]["precedingEd25519Instruction"]; assert_eq!(preceding["resolvedInstructionIndex"], 1); assert_eq!(preceding["matchesExpectedProgram"], true); assert_eq!(preceding["expectedSignatureCount"], 2); assert_eq!(preceding["signatureTableInspectedByDecoder"], false); } #[test] fn malformed_unknown_and_trailing_payloads_fail_safely() { for bytes in [vec![], vec![0, 7], vec![1, 2, 3]] { let result = crate::solana_core_slashing_decode(&replay_input(bytes.as_slice(), false, "2")); assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed); } let unknown = crate::solana_core_slashing_decode(&replay_input(&[9], false, "2")); assert_eq!(unknown.status, crate::DecoderOutcomeStatus::Unsupported); } #[test] fn wrong_fixed_accounts_fail_safely() { let mut input = replay_input(duplicate_payload().as_slice(), false, "2"); input.instruction_accounts_json[2]["accountKey"] = serde_json::json!("wrong"); input.account_keys_json[2]["accountKey"] = serde_json::json!("wrong"); let result = crate::solana_core_slashing_decode(&input); assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed); } #[test] fn failed_transaction_is_decoded_without_runtime_or_mutation_claim() { let result = crate::solana_core_slashing_decode(&replay_input( duplicate_payload().as_slice(), true, "2", )); assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); assert!(!result.observations[0].observation_committed); let parameters = &result.observations[0].payload_json["parameters"]; assert_eq!(parameters["runtimeVerification"], "not_asserted_transaction_failed"); assert_eq!(parameters["runtimeMutation"], "not_asserted_transaction_failed"); } #[test] fn serialization_is_deterministic() { let input = replay_input(duplicate_payload().as_slice(), false, "2"); let first = crate::solana_core_slashing_decode(&input); let second = crate::solana_core_slashing_decode(&input); let first_json = match serde_json::to_string(&first) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("first 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 serialization failed: {error}"), }; assert_eq!(first_json, second_json); } }