Files
khadhroony-bot3/kb-lib/src/decoder/solana/core/slashing.rs
2026-07-23 18:25:10 +02:00

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);
}
}