diff --git a/Cargo.lock b/Cargo.lock index 18995a7..734764f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,7 +34,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -614,7 +614,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -623,7 +623,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1048,7 +1048,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "typenum", ] @@ -1059,7 +1059,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ - "generic-array 0.14.7", + "generic-array", "subtle", ] @@ -1221,7 +1221,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1680,16 +1680,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "generic-array" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb8bc4c28d15ade99c7e90b219f30da4be5c88e586277e8cbe886beeb868ab2" -dependencies = [ - "serde", - "typenum", -] - [[package]] name = "gethostname" version = "0.2.3" @@ -1917,7 +1907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" dependencies = [ "digest 0.9.0", - "generic-array 0.14.7", + "generic-array", "hmac 0.8.1", ] @@ -2305,7 +2295,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -5695,7 +5685,7 @@ checksum = "29cb6cae156bc2c80ae34b2b365cf460be974cf5c68799e7024615f88f54aed4" dependencies = [ "bs58", "ed25519-dalek", - "generic-array 0.14.7", + "generic-array", "rand 0.8.5", "serde", "serde_derive", @@ -6385,7 +6375,6 @@ dependencies = [ "bincode", "bitflags 2.7.0", "bytemuck", - "generic-array 1.1.1", "lazy_static", "num-derive", "num-traits", @@ -6401,6 +6390,7 @@ dependencies = [ "solana-program", "solana-program-test", "solana-sdk", + "solana-signature", "spl-pod 0.5.0", "spl-record", "thiserror 2.0.10", diff --git a/program/Cargo.toml b/program/Cargo.toml index 5dd74cf..a6419cd 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -15,11 +15,11 @@ test-sbf = [] bitflags = { version = "2.7.0", features = ["serde"] } bytemuck = { version = "1.21.0", features = ["derive"] } num_enum = "0.7.3" -generic-array = { version = "1.1.1", features = ["serde"], default-features = false } bincode = "1.3.3" num-derive = "0.4" num-traits = "0.2" solana-program = "2.1.0" +solana-signature = "2.1.0" serde = "1.0.217" # must match the serde_derive version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 serde_bytes = "0.11.15" serde_derive = "1.0.210" # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 diff --git a/program/src/duplicate_block_proof.rs b/program/src/duplicate_block_proof.rs index 4a87c7e..ab2ed28 100644 --- a/program/src/duplicate_block_proof.rs +++ b/program/src/duplicate_block_proof.rs @@ -3,14 +3,80 @@ use { crate::{ error::SlashingError, shred::{Shred, ShredType}, + sigverify::SignatureVerification, state::{ProofType, SlashingProofData}, }, bytemuck::try_from_bytes, - solana_program::{clock::Slot, msg, pubkey::Pubkey}, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Slot, + hash::Hash, + msg, + pubkey::Pubkey, + }, + solana_signature::SIGNATURE_BYTES, spl_pod::primitives::PodU32, + std::slice::Iter, }; +/// The verification instruction occurs immediately before the slashing +/// instruction +const SIGVERIFY_INSTRUCTION_RELATIVE_INDEX: i64 = -1; +/// Both shreds are verified in the same instruction +const NUM_VERIFICATIONS_IN_INSTRUCTION: usize = 2; + +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq)] +/// Signature verification context required for duplicate block +/// proof verification +pub struct DuplicateBlockProofContext<'a> { + pub(crate) expected_pubkey: &'a Pubkey, + pub(crate) expected_shred1_merkle_root: &'a Hash, + pub(crate) expected_shred1_signature: &'a [u8; SIGNATURE_BYTES], + pub(crate) expected_shred2_merkle_root: &'a Hash, + pub(crate) expected_shred2_signature: &'a [u8; SIGNATURE_BYTES], +} + +impl<'a> DuplicateBlockProofContext<'a> { + fn unpack_context<'b>( + instruction_data: &'a [u8], + instructions_sysvar: &'a AccountInfo<'b>, + ) -> Result { + let signature_verifications = + SignatureVerification::inspect_verifications::<{ NUM_VERIFICATIONS_IN_INSTRUCTION }>( + instruction_data, + instructions_sysvar, + SIGVERIFY_INSTRUCTION_RELATIVE_INDEX, + )?; + + let expected_shred1_merkle_root: &'a Hash = + bytemuck::try_from_bytes(signature_verifications[0].message) + .map_err(|_| SlashingError::InvalidSignatureVerification)?; + let expected_shred2_merkle_root: &'a Hash = + bytemuck::try_from_bytes(signature_verifications[1].message) + .map_err(|_| SlashingError::InvalidSignatureVerification)?; + + if signature_verifications[0].pubkey != signature_verifications[1].pubkey { + msg!( + "Signature verification instruction was for 2 different pubkeys {} vs {}", + signature_verifications[0].pubkey, + signature_verifications[1].pubkey, + ); + return Err(SlashingError::InvalidSignatureVerification); + } + + Ok(Self { + expected_pubkey: signature_verifications[0].pubkey, + expected_shred1_merkle_root, + expected_shred1_signature: signature_verifications[0].signature, + expected_shred2_merkle_root, + expected_shred2_signature: signature_verifications[1].signature, + }) + } +} + /// Proof of a duplicate block violation +#[derive(Clone, Copy)] pub struct DuplicateBlockProofData<'a> { /// Shred signed by a leader pub shred1: &'a [u8], @@ -44,27 +110,20 @@ impl<'a> DuplicateBlockProofData<'a> { impl<'a> SlashingProofData<'a> for DuplicateBlockProofData<'a> { const PROOF_TYPE: ProofType = ProofType::DuplicateBlockProof; + type Context = DuplicateBlockProofContext<'a>; - fn verify_proof(self, slot: Slot, _node_pubkey: &Pubkey) -> Result<(), SlashingError> { - // TODO: verify through instruction inspection that the shreds were sigverified - // earlier in this transaction. - // Ed25519 Singature verification is performed on the merkle root: - // node_pubkey.verify_strict(merkle_root, signature). - // We will verify that the pubkey merkle root and signature match the shred and - // that the verification was successful. - let shred1 = Shred::new_from_payload(self.shred1)?; - let shred2 = Shred::new_from_payload(self.shred2)?; - check_shreds(slot, &shred1, &shred2) - } - - fn unpack(data: &'a [u8]) -> Result + fn unpack<'b>( + proof_account_data: &'a [u8], + instruction_data: &'a [u8], + account_info_iter: &'a mut Iter<'_, AccountInfo<'b>>, + ) -> Result<(Self, Self::Context), SlashingError> where Self: Sized, { - if data.len() < Self::LENGTH_SIZE { + if proof_account_data.len() < Self::LENGTH_SIZE { return Err(SlashingError::ProofBufferTooSmall); } - let (length1, data) = data.split_at(Self::LENGTH_SIZE); + let (length1, data) = proof_account_data.split_at(Self::LENGTH_SIZE); let shred1_length = try_from_bytes::(length1) .map_err(|_| SlashingError::ProofBufferDeserializationError)?; let shred1_length = u32::from(*shred1_length) as usize; @@ -86,7 +145,25 @@ impl<'a> SlashingProofData<'a> for DuplicateBlockProofData<'a> { return Err(SlashingError::ProofBufferTooSmall); } - Ok(Self { shred1, shred2 }) + let instructions_sysvar = next_account_info(account_info_iter) + .map_err(|_| SlashingError::MissingInstructionsSysvar)?; + let context = + DuplicateBlockProofContext::unpack_context(instruction_data, instructions_sysvar)?; + + Ok((Self { shred1, shred2 }, context)) + } + + fn verify_proof( + self, + context: Self::Context, + slot: Slot, + node_pubkey: &Pubkey, + ) -> Result<(), SlashingError> { + let shred1 = Shred::new_from_payload(self.shred1)?; + let shred2 = Shred::new_from_payload(self.shred2)?; + + sigverify_shreds(&context, node_pubkey, &shred1, &shred2)?; + check_shreds(slot, &shred1, &shred2) } } @@ -243,17 +320,83 @@ fn check_shreds(slot: Slot, shred1: &Shred, shred2: &Shred) -> Result<(), Slashi Err(SlashingError::InvalidErasureMetaConflict) } +/// Verify that `shred1` and `shred2` are correctly signed by `node_pubkey`. +/// Leaders sign the merkle root of each shred with their pubkey. +/// We use the context returned via instruction introspection to verify that +/// instructions representing: +/// - `node_pubkey.verify(shred1.signature, shred1.merkle_root)` +/// - `node_pubkey.verify(shred2.signature, shred2.merkle_root)` +/// were executed successfully +fn sigverify_shreds( + context: &DuplicateBlockProofContext, + node_pubkey: &Pubkey, + shred1: &Shred, + shred2: &Shred, +) -> Result<(), SlashingError> { + if context.expected_pubkey != node_pubkey { + msg!( + "Signature verification pubkey {} mismatches node pubkey {}", + context.expected_pubkey, + node_pubkey, + ); + return Err(SlashingError::SignatureVerificationMismatch); + } + + if *context.expected_shred1_merkle_root != shred1.merkle_root()? { + msg!( + "First signature verification message {} mismatches shred1 merkle root {}", + context.expected_shred1_merkle_root, + shred1.merkle_root()?, + ); + return Err(SlashingError::SignatureVerificationMismatch); + } + if *context.expected_shred2_merkle_root != shred2.merkle_root()? { + msg!( + "Second signature verification message {} mismatches shred2 merkle root {}", + context.expected_shred2_merkle_root, + shred2.merkle_root()?, + ); + return Err(SlashingError::SignatureVerificationMismatch); + } + + if context.expected_shred1_signature != shred1.signature()? { + msg!( + "First signature verification signature {:?} mismatches shred1 signature {:?}", + context.expected_shred1_signature, + shred1.signature()?, + ); + return Err(SlashingError::SignatureVerificationMismatch); + } + if context.expected_shred2_signature != shred2.signature()? { + msg!( + "Second signature verification signature {:?} mismatches shred2 signature {:?}", + context.expected_shred2_signature, + shred2.signature()?, + ); + return Err(SlashingError::SignatureVerificationMismatch); + } + + Ok(()) +} + #[cfg(test)] mod tests { use { super::*, - crate::shred::{ - tests::{new_rand_coding_shreds, new_rand_data_shred, new_rand_shreds}, - SIZE_OF_SIGNATURE, + crate::{ + instruction::{construct_instructions_and_sysvar, DuplicateBlockProofInstructionData}, + shred::{ + tests::{new_rand_coding_shreds, new_rand_data_shred, new_rand_shreds}, + SIZE_OF_SIGNATURE, + }, }, rand::Rng, solana_ledger::shred::{Shred as SolanaShred, Shredder}, - solana_sdk::signature::{Keypair, Signature, Signer}, + solana_sdk::{ + signature::{Keypair, Signature, Signer}, + sysvar::instructions, + }, + spl_pod::primitives::PodU64, std::sync::Arc, }; @@ -263,19 +406,82 @@ mod tests { const VERSION: u16 = 0; fn generate_proof_data<'a>( + leader: &'a Pubkey, shred1: &'a SolanaShred, + expected_shred1_merkle_root: &'a Hash, shred2: &'a SolanaShred, - ) -> DuplicateBlockProofData<'a> { - DuplicateBlockProofData { - shred1: shred1.payload().as_slice(), - shred2: shred2.payload().as_slice(), - } + expected_shred2_merkle_root: &'a Hash, + ) -> (DuplicateBlockProofData<'a>, DuplicateBlockProofContext<'a>) { + let context = DuplicateBlockProofContext { + expected_pubkey: leader, + expected_shred1_merkle_root, + expected_shred2_merkle_root, + expected_shred1_signature: shred1.signature().as_ref().try_into().unwrap(), + expected_shred2_signature: shred2.signature().as_ref().try_into().unwrap(), + }; + ( + DuplicateBlockProofData { + shred1: shred1.payload().as_slice(), + shred2: shred2.payload().as_slice(), + }, + context, + ) + } + + #[test] + fn test_unpack_context() { + let node_pubkey = Pubkey::new_unique(); + let slot = 100; + let instruction_data = DuplicateBlockProofInstructionData { + slot: PodU64::from(slot), + offset: PodU64::from(0), + node_pubkey, + shred_1_merkle_root: Hash::new_unique(), + shred_1_signature: Signature::new_unique().into(), + shred_2_merkle_root: Hash::new_unique(), + shred_2_signature: Signature::new_unique().into(), + }; + let (instructions, mut instructions_sysvar_data) = + construct_instructions_and_sysvar(&instruction_data); + let mut lamports = 0; + let instructions_sysvar = AccountInfo::new( + &instructions::ID, + false, + true, + &mut lamports, + &mut instructions_sysvar_data, + &instructions::ID, + false, + 0, + ); + let context = + DuplicateBlockProofContext::unpack_context(&instructions[1].data, &instructions_sysvar) + .unwrap(); + + assert_eq!(*context.expected_pubkey, node_pubkey); + assert_eq!( + *context.expected_shred1_merkle_root, + instruction_data.shred_1_merkle_root + ); + assert_eq!( + *context.expected_shred2_merkle_root, + instruction_data.shred_2_merkle_root + ); + assert_eq!( + *context.expected_shred1_signature, + instruction_data.shred_1_signature + ); + assert_eq!( + *context.expected_shred2_signature, + instruction_data.shred_2_signature + ); } #[test] fn test_legacy_shreds_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let legacy_data_shred = @@ -300,9 +506,14 @@ mod tests { (data_shred.clone(), legacy_coding_shred.clone()), ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = Hash::default(); + let shred2_mr = Hash::default(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::LegacyShreds, ); } @@ -312,6 +523,7 @@ mod tests { fn test_slot_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder_slot = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let shredder_bad_slot = Shredder::new(SLOT + 1, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); @@ -357,9 +569,14 @@ mod tests { ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::SlotMismatch ); } @@ -369,20 +586,27 @@ mod tests { fn test_payload_proof_valid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let shred1 = new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); let shred2 = new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let proof_data = generate_proof_data(&shred1, &shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, &shred1, &shred1_mr, &shred2, &shred2_mr); + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap(); } #[test] fn test_payload_proof_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let data_shred = @@ -397,9 +621,14 @@ mod tests { ]; for (shred1, shred2) in test_cases.into_iter() { - let proof_data = generate_proof_data(&shred1, &shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, &shred1, &shred1_mr, &shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::InvalidPayloadProof ); } @@ -409,6 +638,7 @@ mod tests { fn test_merkle_root_proof_valid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let (data_shreds, coding_shreds) = new_rand_shreds( @@ -441,8 +671,13 @@ mod tests { ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap(); } } @@ -450,6 +685,7 @@ mod tests { fn test_merkle_root_proof_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let (data_shreds, coding_shreds) = new_rand_shreds( @@ -483,9 +719,14 @@ mod tests { ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::ShredTypeMismatch ); } @@ -495,6 +736,7 @@ mod tests { fn test_last_index_conflict_valid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let test_cases = vec![ @@ -525,8 +767,13 @@ mod tests { ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap(); } } @@ -534,6 +781,7 @@ mod tests { fn test_last_index_conflict_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let test_cases = vec![ @@ -584,9 +832,14 @@ mod tests { ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::InvalidLastIndexConflict ); } @@ -596,6 +849,7 @@ mod tests { fn test_erasure_meta_conflict_valid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let coding_shreds = @@ -611,8 +865,13 @@ mod tests { (coding_shreds[0].clone(), coding_shreds_smaller[1].clone()), ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap(); } } @@ -620,6 +879,7 @@ mod tests { fn test_erasure_meta_conflict_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let coding_shreds = @@ -665,9 +925,14 @@ mod tests { ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::InvalidErasureMetaConflict ); } @@ -677,6 +942,7 @@ mod tests { fn test_shred_version_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let (data_shreds, coding_shreds) = new_rand_shreds( @@ -711,9 +977,14 @@ mod tests { ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::InvalidShredVersion ); } @@ -728,6 +999,7 @@ mod tests { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let data_shred = @@ -761,9 +1033,14 @@ mod tests { (coding_shred, coding_shred_different_retransmitter), ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), SlashingError::InvalidPayloadProof ); } @@ -773,6 +1050,7 @@ mod tests { fn test_overlapping_erasure_meta_proof_valid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let coding_shreds = @@ -802,8 +1080,13 @@ mod tests { ), ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap(); } } @@ -811,6 +1094,7 @@ mod tests { fn test_overlapping_erasure_meta_proof_invalid() { let mut rng = rand::thread_rng(); let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); let next_shred_index = rng.gen_range(0..32_000); let (data_shred, coding_shred) = new_rand_shreds( @@ -862,11 +1146,84 @@ mod tests { .iter() .flat_map(|(a, b, c)| [(a, b, c), (b, a, c)]) { - let proof_data = generate_proof_data(shred1, shred2); + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, shred1, &shred1_mr, shred2, &shred2_mr); assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap_err(), *expected, ); } } + + #[test] + fn test_sigverify() { + let mut rng = rand::thread_rng(); + let leader = Arc::new(Keypair::new()); + let leader_pubkey = leader.pubkey(); + let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); + let next_shred_index = rng.gen_range(0..32_000); + let shred1 = + new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); + let shred2 = + new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); + + let shred1_mr = shred1.merkle_root().unwrap(); + let shred2_mr = shred2.merkle_root().unwrap(); + let (proof_data, context) = + generate_proof_data(&leader_pubkey, &shred1, &shred1_mr, &shred2, &shred2_mr); + proof_data + .verify_proof(context, SLOT, &leader_pubkey) + .unwrap(); + + let bad_pubkey = Pubkey::new_unique(); + let bad_merkle_root = Hash::new_unique(); + let bad_signature = <[u8; SIGNATURE_BYTES]>::from(Signature::new_unique()); + + let mut bad_context = context; + bad_context.expected_pubkey = &bad_pubkey; + assert_eq!( + proof_data + .verify_proof(bad_context, SLOT, &leader_pubkey) + .unwrap_err(), + SlashingError::SignatureVerificationMismatch + ); + + let mut bad_context = context; + bad_context.expected_shred1_merkle_root = &bad_merkle_root; + assert_eq!( + proof_data + .verify_proof(bad_context, SLOT, &leader_pubkey) + .unwrap_err(), + SlashingError::SignatureVerificationMismatch + ); + let mut bad_context = context; + bad_context.expected_shred2_merkle_root = &bad_merkle_root; + assert_eq!( + proof_data + .verify_proof(bad_context, SLOT, &leader_pubkey) + .unwrap_err(), + SlashingError::SignatureVerificationMismatch + ); + + let mut bad_context = context; + bad_context.expected_shred1_signature = &bad_signature; + assert_eq!( + proof_data + .verify_proof(bad_context, SLOT, &leader_pubkey) + .unwrap_err(), + SlashingError::SignatureVerificationMismatch + ); + let mut bad_context = context; + bad_context.expected_shred2_signature = &bad_signature; + assert_eq!( + proof_data + .verify_proof(bad_context, SLOT, &leader_pubkey) + .unwrap_err(), + SlashingError::SignatureVerificationMismatch + ); + } } diff --git a/program/src/error.rs b/program/src/error.rs index 0c6d2b4..80cd5ce 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -41,14 +41,22 @@ pub enum SlashingError { #[error("Invalid shred version")] InvalidShredVersion, - /// Invalid signature on duplicate block proof shreds - #[error("Invalid signature")] - InvalidSignature, + /// Invalid signature verification instruction + #[error("Signature verification instruction is invalid")] + InvalidSignatureVerification, /// Legacy shreds are not supported #[error("Legacy shreds are not eligible for slashing")] LegacyShreds, + /// Missing instructions sysvar + #[error("Instructions sysvar is missing")] + MissingInstructionsSysvar, + + /// Missing signature verification instruction + #[error("Signature verification instruction is missing")] + MissingSignatureVerification, + /// Unable to deserialize proof buffer #[error("Proof buffer deserialization error")] ProofBufferDeserializationError, @@ -65,6 +73,10 @@ pub enum SlashingError { #[error("Shred type mismatch")] ShredTypeMismatch, + /// Signature verification instruction did not match the shred + #[error("Mismatch between signature verification and shred")] + SignatureVerificationMismatch, + /// Invalid slot on duplicate block proof shreds #[error("Slot mismatch")] SlotMismatch, diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 7531b51..2598c12 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -1,15 +1,17 @@ //! Program instructions use { - crate::{error::SlashingError, id}, + crate::{error::SlashingError, id, sigverify::Ed25519SignatureOffsets}, bytemuck::{Pod, Zeroable}, num_enum::{IntoPrimitive, TryFromPrimitive}, solana_program::{ - clock::Slot, + hash::{Hash, HASH_BYTES}, instruction::{AccountMeta, Instruction}, program_error::ProgramError, - pubkey::Pubkey, + pubkey::{Pubkey, PUBKEY_BYTES}, + sysvar, }, + solana_signature::SIGNATURE_BYTES, spl_pod::{ bytemuck::{pod_from_bytes, pod_get_packed_len}, primitives::PodU64, @@ -27,6 +29,7 @@ pub enum SlashingInstruction { /// Accounts expected by this instruction: /// 0. `[]` Proof account, must be previously initialized with the proof /// data. + /// 1. `[]` Instructions sysvar /// /// We expect the proof account to be properly sized as to hold a duplicate /// block proof. See [`ProofType`] for sizing requirements. @@ -45,11 +48,34 @@ pub enum SlashingInstruction { #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] pub struct DuplicateBlockProofInstructionData { /// Offset into the proof account to begin reading, expressed as `u64` - pub(crate) offset: PodU64, + pub offset: PodU64, /// Slot for which the violation occurred - pub(crate) slot: PodU64, + pub slot: PodU64, /// Identity pubkey of the Node that signed the duplicate block - pub(crate) node_pubkey: Pubkey, + pub node_pubkey: Pubkey, + /// The first shred's merkle root (the message of the first sigverify + /// instruction) + pub shred_1_merkle_root: Hash, + /// The first shred's signature (the signature of the first sigverify + /// instruction) + pub shred_1_signature: [u8; SIGNATURE_BYTES], + /// The second shred's merkle root (the message of the second sigverify + /// instruction) + pub shred_2_merkle_root: Hash, + /// The second shred's signature (the signature of the second sigverify + /// instruction) + pub shred_2_signature: [u8; SIGNATURE_BYTES], +} + +impl DuplicateBlockProofInstructionData { + // 1 Byte for the instruction type discriminant + const DATA_START: u16 = 1; + const NODE_PUBKEY_OFFSET: u16 = 16 + Self::DATA_START; + + const MESSAGE_1_OFFSET: u16 = Self::NODE_PUBKEY_OFFSET + PUBKEY_BYTES as u16; + const SIGNATURE_1_OFFSET: u16 = HASH_BYTES as u16 + Self::MESSAGE_1_OFFSET; + const MESSAGE_2_OFFSET: u16 = SIGNATURE_BYTES as u16 + Self::SIGNATURE_1_OFFSET; + const SIGNATURE_2_OFFSET: u16 = HASH_BYTES as u16 + Self::MESSAGE_2_OFFSET; } /// Utility function for encoding instruction data @@ -89,24 +115,97 @@ pub(crate) fn decode_instruction_data(input_with_type: &[u8]) -> Result< /// Create a `SlashingInstruction::DuplicateBlockProof` instruction pub fn duplicate_block_proof( proof_account: &Pubkey, - offset: u64, - slot: Slot, - node_pubkey: Pubkey, + instruction_data: &DuplicateBlockProofInstructionData, ) -> Instruction { + let mut accounts = vec![AccountMeta::new_readonly(*proof_account, false)]; + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); encode_instruction( - vec![AccountMeta::new_readonly(*proof_account, false)], + accounts, SlashingInstruction::DuplicateBlockProof, - &DuplicateBlockProofInstructionData { - offset: PodU64::from(offset), - slot: PodU64::from(slot), - node_pubkey, - }, + instruction_data, ) } +/// Utility to create instructions for both the signature verification and the +/// `SlashingInstruction::DuplicateBlockProof` in the expected format. +/// +/// `sigverify_data` should equal the `(shredx.merkle_root, shredx.signature)` +/// specified in the proof account +/// +/// Returns two instructions, the sigverify and the slashing instruction. These +/// must be sent consecutively as the first two instructions in a transaction +/// with the same ordering to function properly. +pub fn duplicate_block_proof_with_sigverify( + proof_account: &Pubkey, + instruction_data: &DuplicateBlockProofInstructionData, +) -> [Instruction; 2] { + let slashing_ix = duplicate_block_proof(proof_account, instruction_data); + let signature_instruction_index = 1; + let public_key_offset = DuplicateBlockProofInstructionData::NODE_PUBKEY_OFFSET; + let public_key_instruction_index = 1; + let message_data_size = HASH_BYTES as u16; + let message_instruction_index = 1; + + let shred1_sigverify_offset = Ed25519SignatureOffsets { + signature_offset: DuplicateBlockProofInstructionData::SIGNATURE_1_OFFSET, + signature_instruction_index, + public_key_offset, + public_key_instruction_index, + message_data_offset: DuplicateBlockProofInstructionData::MESSAGE_1_OFFSET, + message_data_size, + message_instruction_index, + }; + let shred2_sigverify_offset = Ed25519SignatureOffsets { + signature_offset: DuplicateBlockProofInstructionData::SIGNATURE_2_OFFSET, + signature_instruction_index, + public_key_offset, + public_key_instruction_index, + message_data_offset: DuplicateBlockProofInstructionData::MESSAGE_2_OFFSET, + message_data_size, + message_instruction_index, + }; + let sigverify_ix = Ed25519SignatureOffsets::to_instruction(&[ + shred1_sigverify_offset, + shred2_sigverify_offset, + ]); + + [sigverify_ix, slashing_ix] +} + +#[cfg(test)] +pub(crate) fn construct_instructions_and_sysvar( + instruction_data: &DuplicateBlockProofInstructionData, +) -> ([Instruction; 2], Vec) { + use solana_sdk::sysvar::instructions::{self, BorrowedAccountMeta, BorrowedInstruction}; + + fn borrow_account(account: &AccountMeta) -> BorrowedAccountMeta { + BorrowedAccountMeta { + pubkey: &account.pubkey, + is_signer: account.is_signer, + is_writable: account.is_writable, + } + } + fn borrow_instruction(ix: &Instruction) -> BorrowedInstruction { + BorrowedInstruction { + program_id: &ix.program_id, + accounts: ix.accounts.iter().map(borrow_account).collect(), + data: &ix.data, + } + } + + let instructions = + duplicate_block_proof_with_sigverify(&Pubkey::new_unique(), instruction_data); + let borrowed_instructions: Vec = + instructions.iter().map(borrow_instruction).collect(); + let mut instructions_sysvar_data = + instructions::construct_instructions_data(&borrowed_instructions); + instructions::store_current_index(&mut instructions_sysvar_data, 1); + (instructions, instructions_sysvar_data) +} + #[cfg(test)] mod tests { - use {super::*, solana_program::program_error::ProgramError}; + use {super::*, solana_program::program_error::ProgramError, solana_signature::Signature}; const TEST_BYTES: [u8; 8] = [42; 8]; @@ -115,11 +214,28 @@ mod tests { let offset = 34; let slot = 42; let node_pubkey = Pubkey::new_unique(); - let instruction = duplicate_block_proof(&Pubkey::new_unique(), offset, slot, node_pubkey); + let shred_1_merkle_root = Hash::new_unique(); + let shred_1_signature = Signature::new_unique().into(); + let shred_2_merkle_root = Hash::new_unique(); + let shred_2_signature = Signature::new_unique().into(); + let instruction_data = DuplicateBlockProofInstructionData { + offset: PodU64::from(offset), + slot: PodU64::from(slot), + node_pubkey, + shred_1_merkle_root, + shred_1_signature, + shred_2_merkle_root, + shred_2_signature, + }; + let instruction = duplicate_block_proof(&Pubkey::new_unique(), &instruction_data); let mut expected = vec![0]; expected.extend_from_slice(&offset.to_le_bytes()); expected.extend_from_slice(&slot.to_le_bytes()); expected.extend_from_slice(&node_pubkey.to_bytes()); + expected.extend_from_slice(&shred_1_merkle_root.to_bytes()); + expected.extend_from_slice(&shred_1_signature); + expected.extend_from_slice(&shred_2_merkle_root.to_bytes()); + expected.extend_from_slice(&shred_2_signature); assert_eq!(instruction.data, expected); assert_eq!( @@ -132,6 +248,10 @@ mod tests { assert_eq!(instruction_data.offset, offset.into()); assert_eq!(instruction_data.slot, slot.into()); assert_eq!(instruction_data.node_pubkey, node_pubkey); + assert_eq!(instruction_data.shred_1_merkle_root, shred_1_merkle_root); + assert_eq!(instruction_data.shred_1_signature, shred_1_signature); + assert_eq!(instruction_data.shred_2_merkle_root, shred_2_merkle_root); + assert_eq!(instruction_data.shred_2_signature, shred_2_signature); } #[test] diff --git a/program/src/lib.rs b/program/src/lib.rs index 24b7343..cc92011 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -7,6 +7,7 @@ pub mod error; pub mod instruction; pub mod processor; mod shred; +mod sigverify; pub mod state; // Export current SDK types for downstream users building with a different SDK diff --git a/program/src/processor.rs b/program/src/processor.rs index 07efa42..d8bc199 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -19,9 +19,16 @@ use { pubkey::Pubkey, sysvar::{clock::Clock, epoch_schedule::EpochSchedule, Sysvar}, }, + std::slice::Iter, }; -fn verify_proof_data<'a, T>(slot: Slot, pubkey: &Pubkey, proof_data: &'a [u8]) -> ProgramResult +fn verify_proof_data<'a, 'b, T>( + slot: Slot, + pubkey: &Pubkey, + proof_data: &'a [u8], + instruction_data: &'a [u8], + accounts_info_iter: &'a mut Iter<'_, AccountInfo<'b>>, +) -> ProgramResult where T: SlashingProofData<'a>, { @@ -35,10 +42,9 @@ where return Err(SlashingError::ExceedsStatueOfLimitations.into()); } - let proof_data: T = - T::unpack(proof_data).map_err(|_| SlashingError::ShredDeserializationError)?; + let (proof_data, context) = T::unpack(proof_data, instruction_data, accounts_info_iter)?; - SlashingProofData::verify_proof(proof_data, slot, pubkey)?; + SlashingProofData::verify_proof(proof_data, context, slot, pubkey)?; // TODO: follow up PR will record this violation in context state account. just // log for now. @@ -58,16 +64,18 @@ pub fn process_instruction( ) -> ProgramResult { let instruction_type = decode_instruction_type(input)?; let account_info_iter = &mut accounts.iter(); - let proof_data_info = next_account_info(account_info_iter); + let proof_data_info = next_account_info(account_info_iter)?; match instruction_type { SlashingInstruction::DuplicateBlockProof => { let data = decode_instruction_data::(input)?; - let proof_data = &proof_data_info?.data.borrow()[u64::from(data.offset) as usize..]; + let proof_data = &proof_data_info.data.borrow()[u64::from(data.offset) as usize..]; verify_proof_data::( data.slot.into(), &data.node_pubkey, proof_data, + input, + account_info_iter, )?; Ok(()) } @@ -79,18 +87,23 @@ mod tests { use { super::verify_proof_data, crate::{ - duplicate_block_proof::DuplicateBlockProofData, error::SlashingError, + duplicate_block_proof::DuplicateBlockProofData, + error::SlashingError, + instruction::{construct_instructions_and_sysvar, DuplicateBlockProofInstructionData}, shred::tests::new_rand_data_shred, }, rand::Rng, solana_ledger::shred::Shredder, solana_sdk::{ + account_info::AccountInfo, clock::{Clock, Slot, DEFAULT_SLOTS_PER_EPOCH}, epoch_schedule::EpochSchedule, program_error::ProgramError, signature::Keypair, signer::Signer, + sysvar::instructions::{self}, }, + spl_pod::primitives::PodU64, std::sync::{Arc, RwLock}, }; @@ -99,7 +112,7 @@ mod tests { static ref CLOCK_SLOT: Arc> = Arc::new(RwLock::new(SLOT)); } - fn generate_proof_data(leader: Arc) -> Vec { + fn generate_proof_data(leader: Arc) -> (DuplicateBlockProofInstructionData, Vec) { let mut rng = rand::thread_rng(); let (slot, parent_slot, reference_tick, version) = (SLOT, SLOT - 1, 0, 0); let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); @@ -108,11 +121,20 @@ mod tests { new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); let shred2 = new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let proof = DuplicateBlockProofData { + let sigverify_data = DuplicateBlockProofInstructionData { + slot: PodU64::from(slot), + offset: PodU64::from(0), + node_pubkey: leader.pubkey(), + shred_1_merkle_root: shred1.merkle_root().unwrap(), + shred_2_merkle_root: shred2.merkle_root().unwrap(), + shred_1_signature: shred1.signature().as_ref().try_into().unwrap(), + shred_2_signature: shred2.signature().as_ref().try_into().unwrap(), + }; + let proof_data = DuplicateBlockProofData { shred1: shred1.payload().as_slice(), shred2: shred2.payload().as_slice(), }; - proof.pack() + (sigverify_data, proof_data.pack()) } #[test] @@ -157,10 +179,27 @@ mod tests { solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); let leader = Arc::new(Keypair::new()); + let (instruction_data, proof_data) = generate_proof_data(leader.clone()); + let mut lamports = 0; + let (instructions, mut instructions_sysvar_data) = + construct_instructions_and_sysvar(&instruction_data); + let instructions_sysvar_account = AccountInfo::new( + &instructions::ID, + false, + true, + &mut lamports, + &mut instructions_sysvar_data, + &instructions::ID, + false, + 0, + ); + verify_proof_data::( SLOT, &leader.pubkey(), - &generate_proof_data(leader), + &proof_data, + &instructions[1].data, + &mut [instructions_sysvar_account].iter(), ) } } diff --git a/program/src/shred.rs b/program/src/shred.rs index 6ca4450..93f37e6 100644 --- a/program/src/shred.rs +++ b/program/src/shred.rs @@ -3,17 +3,17 @@ use { crate::error::SlashingError, bitflags::bitflags, bytemuck::Pod, - generic_array::{typenum::U64, GenericArray}, num_enum::{IntoPrimitive, TryFromPrimitive}, serde_derive::Deserialize, solana_program::{ clock::Slot, - hash::{hashv, Hash}, + hash::{hashv, Hash, HASH_BYTES}, }, + solana_signature::SIGNATURE_BYTES, spl_pod::primitives::{PodU16, PodU32, PodU64}, }; -pub(crate) const SIZE_OF_SIGNATURE: usize = 64; +pub(crate) const SIZE_OF_SIGNATURE: usize = SIGNATURE_BYTES; const SIZE_OF_SHRED_VARIANT: usize = 1; const SIZE_OF_SLOT: usize = 8; const SIZE_OF_INDEX: usize = 4; @@ -24,7 +24,7 @@ const SIZE_OF_NUM_DATA_SHREDS: usize = 2; const SIZE_OF_NUM_CODING_SHREDS: usize = 2; const SIZE_OF_POSITION: usize = 2; -const SIZE_OF_MERKLE_ROOT: usize = 32; +const SIZE_OF_MERKLE_ROOT: usize = HASH_BYTES; const SIZE_OF_MERKLE_PROOF_ENTRY: usize = 20; const OFFSET_OF_SHRED_VARIANT: usize = SIZE_OF_SIGNATURE; @@ -46,10 +46,6 @@ type MerkleProofEntry = [u8; 20]; const MERKLE_HASH_PREFIX_LEAF: &[u8] = b"\x00SOLANA_MERKLE_SHREDS_LEAF"; const MERKLE_HASH_PREFIX_NODE: &[u8] = b"\x01SOLANA_MERKLE_SHREDS_NODE"; -#[repr(transparent)] -#[derive(Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize)] -pub(crate) struct Signature(GenericArray); - bitflags! { #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize)] pub struct ShredFlags:u8 { @@ -147,6 +143,14 @@ impl<'a> Shred<'a> { .map_err(|_| SlashingError::ShredDeserializationError) } + pub(crate) fn signature(&self) -> Result<&[u8; SIGNATURE_BYTES], SlashingError> { + self.payload + .get(0..SIZE_OF_SIGNATURE) + .ok_or(SlashingError::ShredDeserializationError)? + .try_into() + .map_err(|_| SlashingError::ShredDeserializationError) + } + fn get_shred_variant(payload: &'a [u8]) -> Result { let Some(&shred_variant) = payload.get(OFFSET_OF_SHRED_VARIANT) else { return Err(SlashingError::ShredDeserializationError); @@ -414,6 +418,7 @@ pub(crate) mod tests { ProcessShredsStats, ReedSolomonCache, Shred as SolanaShred, Shredder, }, solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, system_transaction}, + solana_signature::Signature, std::sync::Arc, }; @@ -532,6 +537,10 @@ pub(crate) mod tests { let shred = Shred::new_from_payload(payload).unwrap(); assert_eq!(shred.slot().unwrap(), solana_shred.slot()); + assert_eq!( + Signature::from(*shred.signature().unwrap()), + *solana_shred.signature() + ); assert_eq!(shred.index().unwrap(), solana_shred.index()); assert_eq!(shred.version().unwrap(), solana_shred.version()); assert_eq!( diff --git a/program/src/sigverify.rs b/program/src/sigverify.rs new file mode 100644 index 0000000..02e652a --- /dev/null +++ b/program/src/sigverify.rs @@ -0,0 +1,187 @@ +//! Parsing signature verification results through instruction introspection +use { + crate::error::SlashingError, + bytemuck::{Pod, Zeroable}, + solana_program::{ + account_info::AccountInfo, + ed25519_program, + instruction::Instruction, + msg, + pubkey::{Pubkey, PUBKEY_BYTES}, + sysvar::instructions::{get_instruction_relative, load_current_index_checked}, + }, + solana_signature::SIGNATURE_BYTES, + std::mem::MaybeUninit, +}; + +const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 14; +const SIGNATURE_OFFSETS_START: usize = 2; + +#[derive(Default, Debug, Copy, Clone, Zeroable, Pod, Eq, PartialEq)] +#[repr(C)] +pub(crate) struct Ed25519SignatureOffsets { + pub(crate) signature_offset: u16, // offset to ed25519 signature of 64 bytes + pub(crate) signature_instruction_index: u16, // instruction index to find signature + pub(crate) public_key_offset: u16, // offset to public key of 32 bytes + pub(crate) public_key_instruction_index: u16, // instruction index to find public key + pub(crate) message_data_offset: u16, // offset to start of message data + pub(crate) message_data_size: u16, // size of message data + pub(crate) message_instruction_index: u16, // index of instruction data to get message data +} + +impl Ed25519SignatureOffsets { + pub(crate) fn to_instruction(offsets: &[Self]) -> Instruction { + let mut instruction_data = Vec::with_capacity( + SIGNATURE_OFFSETS_START + .saturating_add(SIGNATURE_OFFSETS_SERIALIZED_SIZE.saturating_mul(offsets.len())), + ); + + let num_signatures = offsets.len() as u16; + instruction_data.extend_from_slice(&num_signatures.to_le_bytes()); + + for offsets in offsets { + instruction_data.extend_from_slice(bytemuck::bytes_of(offsets)); + } + + Instruction { + program_id: ed25519_program::id(), + accounts: vec![], + data: instruction_data, + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) struct SignatureVerification<'a> { + pub(crate) pubkey: &'a Pubkey, + pub(crate) message: &'a [u8], + pub(crate) signature: &'a [u8; SIGNATURE_BYTES], +} + +impl<'a> SignatureVerification<'a> { + fn new( + pubkey: &'a [u8], + message: &'a [u8], + signature: &'a [u8], + ) -> Result, SlashingError> { + let pubkey: &'a Pubkey = bytemuck::try_from_bytes(pubkey).map_err(|_| { + msg!("Failed to deserialize pubkey"); + SlashingError::InvalidSignatureVerification + })?; + + let signature: &'a [u8; SIGNATURE_BYTES] = signature.try_into().map_err(|_| { + msg!("Failed to deserialize signature"); + SlashingError::InvalidSignatureVerification + })?; + + Ok(Self { + pubkey, + message, + signature, + }) + } + + fn get_data_slice<'b>( + data: &'a [u8], + _instructions_sysvar: &'a AccountInfo<'b>, + instruction_index: u16, + current_index: u16, + offset_start: u16, + size: usize, + ) -> Result<&'a [u8], SlashingError> { + if instruction_index != current_index { + // For duplicate block slashing the message is small enough to fit inside the + // slashing instruction but this might not be the case for future slashing cases + // TODO: re-implement load_instruction_at_checked(instruction_index as usize, + // instructions_sysvar) in a zero copy way. + msg!("Signature verification instruction must store the data within the slashing instruction"); + return Err(SlashingError::InvalidSignatureVerification); + } + let start = offset_start as usize; + let end = start.saturating_add(size); + if end > data.len() { + return Err(SlashingError::InvalidSignatureVerification); + } + + Ok(&data[start..end]) + } + + /// Perform instruction introspection to grab details about signature + /// verification + pub(crate) fn inspect_verifications<'b, const NUM_VERIFICATIONS: usize>( + instruction_data: &'a [u8], + instructions_sysvar: &'a AccountInfo<'b>, + relative_index: i64, + ) -> Result<[SignatureVerification<'a>; NUM_VERIFICATIONS], SlashingError> { + let mut verifications = [MaybeUninit::::uninit(); NUM_VERIFICATIONS]; + + // Instruction inspection to unpack successful signature verifications + let current_index = load_current_index_checked(instructions_sysvar) + .map_err(|_| SlashingError::InvalidSignatureVerification)?; + let sigverify_ix = get_instruction_relative(relative_index, instructions_sysvar) + .map_err(|_| SlashingError::MissingSignatureVerification)?; + if sigverify_ix.program_id != ed25519_program::id() { + return Err(SlashingError::MissingSignatureVerification); + } + let num_signatures = u16::from_le_bytes( + sigverify_ix.data[0..2] + .try_into() + .map_err(|_| SlashingError::MissingSignatureVerification)?, + ); + if num_signatures < NUM_VERIFICATIONS as u16 { + return Err(SlashingError::MissingSignatureVerification); + } + let expected_data_size = NUM_VERIFICATIONS + .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE) + .saturating_add(SIGNATURE_OFFSETS_START); + if sigverify_ix.data.len() < expected_data_size { + return Err(SlashingError::InvalidSignatureVerification); + } + + for (i, verification) in verifications.iter_mut().enumerate().take(NUM_VERIFICATIONS) { + let start = i + .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE) + .saturating_add(SIGNATURE_OFFSETS_START); + let end = start.saturating_add(SIGNATURE_OFFSETS_SERIALIZED_SIZE); + + let offsets: &Ed25519SignatureOffsets = + bytemuck::try_from_bytes(&sigverify_ix.data[start..end]) + .map_err(|_| SlashingError::InvalidSignatureVerification)?; + + // Parse out signature + let signature = Self::get_data_slice( + instruction_data, + instructions_sysvar, + offsets.signature_instruction_index, + current_index, + offsets.signature_offset, + SIGNATURE_BYTES, + )?; + + // Parse out pubkey + let pubkey = Self::get_data_slice( + instruction_data, + instructions_sysvar, + offsets.public_key_instruction_index, + current_index, + offsets.public_key_offset, + PUBKEY_BYTES, + )?; + + // Parse out message + let message = Self::get_data_slice( + instruction_data, + instructions_sysvar, + offsets.message_instruction_index, + current_index, + offsets.message_data_offset, + offsets.message_data_size as usize, + )?; + + *verification = + MaybeUninit::new(SignatureVerification::new(pubkey, message, signature)?); + } + unsafe { std::mem::transmute_copy(&verifications) } + } +} diff --git a/program/src/state.rs b/program/src/state.rs index addd3f5..1f3aaca 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -1,7 +1,8 @@ //! Program state use { crate::{duplicate_block_proof::DuplicateBlockProofData, error::SlashingError}, - solana_program::{clock::Slot, pubkey::Pubkey}, + solana_program::{account_info::AccountInfo, clock::Slot, pubkey::Pubkey}, + std::slice::Iter, }; const PACKET_DATA_SIZE: usize = 1232; @@ -61,14 +62,25 @@ impl From for ProofType { pub trait SlashingProofData<'a> { /// The type of proof this data represents const PROOF_TYPE: ProofType; + /// The context needed to verify the proof + type Context; - /// Zero copy from raw data buffer - fn unpack(data: &'a [u8]) -> Result + /// Zero copy from raw data buffers and initialize any context + fn unpack<'b>( + proof_account_data: &'a [u8], + instruction_data: &'a [u8], + account_info_iter: &'a mut Iter<'_, AccountInfo<'b>>, + ) -> Result<(Self, Self::Context), SlashingError> where Self: Sized; /// Verification logic for this type of proof data - fn verify_proof(self, slot: Slot, pubkey: &Pubkey) -> Result<(), SlashingError>; + fn verify_proof( + self, + context: Self::Context, + slot: Slot, + pubkey: &Pubkey, + ) -> Result<(), SlashingError>; } #[cfg(test)] diff --git a/program/tests/duplicate_block_proof.rs b/program/tests/duplicate_block_proof.rs index bab00c5..72fff4d 100644 --- a/program/tests/duplicate_block_proof.rs +++ b/program/tests/duplicate_block_proof.rs @@ -12,20 +12,26 @@ use { solana_sdk::{ clock::{Clock, Slot}, decode_error::DecodeError, - hash::Hash, - instruction::InstructionError, + ed25519_instruction::SIGNATURE_OFFSETS_START, + hash::{Hash, HASH_BYTES}, + instruction::{Instruction, InstructionError}, rent::Rent, signature::{Keypair, Signer}, system_instruction, system_transaction, transaction::{Transaction, TransactionError}, }, - spl_pod::bytemuck::pod_get_packed_len, + solana_signature::SIGNATURE_BYTES, + spl_pod::{bytemuck::pod_get_packed_len, primitives::PodU64}, spl_record::{instruction as record, state::RecordData}, spl_slashing::{ - duplicate_block_proof::DuplicateBlockProofData, error::SlashingError, id, instruction, - processor::process_instruction, state::ProofType, + duplicate_block_proof::DuplicateBlockProofData, + error::SlashingError, + id, + instruction::{duplicate_block_proof_with_sigverify, DuplicateBlockProofInstructionData}, + processor::process_instruction, + state::ProofType, }, - std::sync::Arc, + std::{assert_ne, sync::Arc}, }; const SLOT: Slot = 53084024; @@ -111,6 +117,25 @@ async fn write_proof( } } +fn slashing_instructions( + proof_account: &Pubkey, + slot: Slot, + node_pubkey: Pubkey, + shred1: &Shred, + shred2: &Shred, +) -> [Instruction; 2] { + let instruction_data = DuplicateBlockProofInstructionData { + slot: PodU64::from(slot), + offset: PodU64::from(RecordData::WRITABLE_START_INDEX as u64), + node_pubkey, + shred_1_merkle_root: shred1.merkle_root().unwrap(), + shred_1_signature: (*shred1.signature()).into(), + shred_2_merkle_root: shred2.merkle_root().unwrap(), + shred_2_signature: (*shred2.signature()).into(), + }; + duplicate_block_proof_with_sigverify(proof_account, &instruction_data) +} + pub fn new_rand_data_shred( rng: &mut R, next_shred_index: u32, @@ -219,12 +244,7 @@ async fn valid_proof_data() { write_proof(&mut context, &authority, &account, &data).await; let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], + &slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2), Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, @@ -254,9 +274,10 @@ async fn valid_proof_coding() { let shred2 = new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader)[1].clone(); - assert!( - ErasureMeta::check_erasure_consistency(&shred1, &shred2), - "Expected erasure consistency failure", + assert_ne!( + shred1.merkle_root().unwrap(), + shred2.merkle_root().unwrap(), + "Expected merkle root failure" ); let duplicate_proof = DuplicateBlockProofData { @@ -269,12 +290,7 @@ async fn valid_proof_coding() { write_proof(&mut context, &authority, &account, &data).await; let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], + &slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2), Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, @@ -312,12 +328,7 @@ async fn invalid_proof_data() { write_proof(&mut context, &authority, &account, &data).await; let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], + &slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2), Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, @@ -328,7 +339,7 @@ async fn invalid_proof_data() { .await .unwrap_err() .unwrap(); - let TransactionError::InstructionError(0, InstructionError::Custom(code)) = err else { + let TransactionError::InstructionError(1, InstructionError::Custom(code)) = err else { panic!("Invalid error {err:?}"); }; let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); @@ -366,12 +377,7 @@ async fn invalid_proof_coding() { write_proof(&mut context, &authority, &account, &data).await; let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], + &slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2), Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, @@ -382,9 +388,169 @@ async fn invalid_proof_coding() { .await .unwrap_err() .unwrap(); - let TransactionError::InstructionError(0, InstructionError::Custom(code)) = err else { + let TransactionError::InstructionError(1, InstructionError::Custom(code)) = err else { panic!("Invalid error {err:?}"); }; let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); assert_eq!(err, SlashingError::InvalidErasureMetaConflict); } + +#[tokio::test] +async fn missing_sigverify() { + let mut context = program_test().start_with_context().await; + setup_clock(&mut context).await; + + let authority = Keypair::new(); + let account = Keypair::new(); + + let mut rng = rand::thread_rng(); + let leader = Arc::new(Keypair::new()); + let (slot, parent_slot, reference_tick, version) = (SLOT, 53084023, 0, 0); + let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); + let next_shred_index = rng.gen_range(0..32_000); + let shred1 = + new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader)[0].clone(); + let shred2 = + new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader)[1].clone(); + + let duplicate_proof = DuplicateBlockProofData { + shred1: shred1.payload().as_slice(), + shred2: shred2.payload().as_slice(), + }; + let data = duplicate_proof.pack(); + + initialize_duplicate_proof_account(&mut context, &authority, &account).await; + write_proof(&mut context, &authority, &account, &data).await; + // Remove the sigverify + let instructions = + [ + slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2)[1] + .clone(), + ]; + + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let err = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + let TransactionError::InstructionError(0, InstructionError::Custom(code)) = err else { + panic!("Invalid error {err:?}"); + }; + let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); + assert_eq!(err, SlashingError::MissingSignatureVerification); + + // Only sigverify one of the shreds + let mut instructions = + slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2); + instructions[0].data[0] = 1; + + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let err = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + let TransactionError::InstructionError(1, InstructionError::Custom(code)) = err else { + panic!("Invalid error {err:?}"); + }; + let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); + assert_eq!(err, SlashingError::MissingSignatureVerification); +} + +#[tokio::test] +async fn improper_sigverify() { + let mut context = program_test().start_with_context().await; + setup_clock(&mut context).await; + + let authority = Keypair::new(); + let account = Keypair::new(); + + let mut rng = rand::thread_rng(); + let leader = Arc::new(Keypair::new()); + let (slot, parent_slot, reference_tick, version) = (SLOT, 53084023, 0, 0); + let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); + let next_shred_index = rng.gen_range(0..32_000); + let shred1 = + new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader)[0].clone(); + let shred2 = + new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader)[1].clone(); + + let duplicate_proof = DuplicateBlockProofData { + shred1: shred1.payload().as_slice(), + shred2: shred2.payload().as_slice(), + }; + let data = duplicate_proof.pack(); + + initialize_duplicate_proof_account(&mut context, &authority, &account).await; + write_proof(&mut context, &authority, &account, &data).await; + + // Replace one of the signature verifications with a random message instead + let message = Hash::new_unique().to_bytes(); + let signature = <[u8; SIGNATURE_BYTES]>::from(leader.sign_message(&message)); + let mut instructions = + slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2); + const MESSAGE_START: usize = 1 + 8 + 8 + 32; + const SIGNATURE_START: usize = MESSAGE_START + HASH_BYTES; + instructions[1].data[MESSAGE_START..SIGNATURE_START].copy_from_slice(&message); + instructions[1].data[SIGNATURE_START..SIGNATURE_START + SIGNATURE_BYTES] + .copy_from_slice(&signature); + + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let err = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + let TransactionError::InstructionError(1, InstructionError::Custom(code)) = err else { + panic!("Invalid error {err:?}"); + }; + let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); + assert_eq!(err, SlashingError::SignatureVerificationMismatch); + + // Put the sigverify data in the sigverify instruction (not allowed currently) + let mut instructions = + slashing_instructions(&account.pubkey(), slot, leader.pubkey(), &shred1, &shred2); + instructions[0].data[SIGNATURE_OFFSETS_START..SIGNATURE_OFFSETS_START + 2] + .copy_from_slice(&100u16.to_le_bytes()); + instructions[0].data[SIGNATURE_OFFSETS_START + 2..SIGNATURE_OFFSETS_START + 4] + .copy_from_slice(&0u16.to_le_bytes()); + instructions[0].data.extend_from_slice(&[0; 200]); + instructions[0].data[100..100 + SIGNATURE_BYTES] + .copy_from_slice(&<[u8; SIGNATURE_BYTES]>::from(*shred1.signature())); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let err = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + let TransactionError::InstructionError(1, InstructionError::Custom(code)) = err else { + panic!("Invalid error {err:?}"); + }; + let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); + assert_eq!(err, SlashingError::InvalidSignatureVerification); +} diff --git a/scripts/solana.dic b/scripts/solana.dic index 113f70b..dd6649a 100644 --- a/scripts/solana.dic +++ b/scripts/solana.dic @@ -52,3 +52,4 @@ slashable merkle retransmitter/SM FEC +sigverify