686 lines
28 KiB
Rust
686 lines
28 KiB
Rust
// 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<crate::DecoderCoverageDeclaration> {
|
|
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<serde_json::Value> {
|
|
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<std::string::String> {
|
|
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<u8> {
|
|
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::<std::vec::Vec<_>>();
|
|
let instruction_accounts = keys
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, key)| {
|
|
return serde_json::json!({"accountIndex": index, "accountKey": key});
|
|
})
|
|
.collect::<std::vec::Vec<_>>();
|
|
let current_index = instruction_path.parse::<usize>().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);
|
|
}
|
|
}
|