0.1.0-pre.004
This commit is contained in:
685
kb-lib/src/decoder/solana/core/slashing.rs
Normal file
685
kb-lib/src/decoder/solana/core/slashing.rs
Normal file
@@ -0,0 +1,685 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user