Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add duplicate block proof sigverify #5

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

AshwinSekar
Copy link
Contributor

@AshwinSekar AshwinSekar commented Jan 9, 2025

Adds signature verification of the shreds included in a duplicate block proof to the slashing program.
Due to the limitations of zerocopying from the instructions sysvar the requirements are as follows:

  • The signature verification instruction to the Ed25519 program must be in the index immediately before the Slashing instruction
  • This instruction must specify 2 verifications, the first one for shred1 and the second for shred2 as indicated by the ordering in the proof_account specified to the slashing instruciton
  • The (pubkey, message, signature) data indicated by the Ed25519 ix's offsets must be in the instruction data for the slashing instruction.

If each (pubkey, message, signature) matches the (node_pubkey, shredx.merkle_root, shredx.signature) for both shreds, then the signature verification is considered successful.

Additionally we expose a utility for the user to create both the sigverify instruction and the slashing instruction together.

Will update the SIMD if this approach is satisfactory

@AshwinSekar AshwinSekar force-pushed the sigverify branch 10 times, most recently from b28a6da to 37888b9 Compare January 9, 2025 20:35
program/Cargo.toml Show resolved Hide resolved
@@ -79,10 +115,11 @@ pub(crate) fn decode_instruction_type(input: &[u8]) -> Result<SlashingInstructio

/// Utility function for decoding instruction data
pub(crate) fn decode_instruction_data<T: Pod>(input_with_type: &[u8]) -> Result<&T, ProgramError> {
if input_with_type.len() != pod_get_packed_len::<T>().saturating_add(1) {
let data_len = pod_get_packed_len::<T>().saturating_add(1);
if input_with_type.len() < data_len {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this to < to allow storage of the sigverify data in slashing instruction.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If DuplicateBlockProofSigverifyData is directly included in DuplicateBlockProofInstructionData, then this shouldn't be an issue!

program/src/sigverify.rs Show resolved Hide resolved
@AshwinSekar AshwinSekar requested a review from joncinque January 9, 2025 20:56
Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really great work! The design makes sense of including the sigverify offsets directly in the slashing instruction, and you've matched everything up without duplication. My comments are mostly minor

program/Cargo.toml Show resolved Hide resolved
scripts/solana.dic Show resolved Hide resolved
slot: Slot,
node_pubkey: Pubkey,
sigverify_data: &DuplicateBlockProofSigverifyData,
slashing_instruction_index: u16,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'm thinking this should be left out for now for easier usage, but the doc comment can certainly call out that these must be the first two instructions in a transaction. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya good suggestion, updated here 074e0b3

Comment on lines 65 to 82
/// Utility struct for packaging the signature verification data required
/// for `SlashingInstruction::DuplicateBlockProof`
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct DuplicateBlockProofSigverifyData {
/// 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],
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for not including this in DuplicateBlockProofInstructionData? It would be clearer to show that this needs to be part of the instruction data. Or at the very least, add a comment in DuplicateBlockProofInstructionData saying that it will be followed by this type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duh that's so much easier haha, updated here 884d9ee

@@ -79,10 +115,11 @@ pub(crate) fn decode_instruction_type(input: &[u8]) -> Result<SlashingInstructio

/// Utility function for decoding instruction data
pub(crate) fn decode_instruction_data<T: Pod>(input_with_type: &[u8]) -> Result<&T, ProgramError> {
if input_with_type.len() != pod_get_packed_len::<T>().saturating_add(1) {
let data_len = pod_get_packed_len::<T>().saturating_add(1);
if input_with_type.len() < data_len {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If DuplicateBlockProofSigverifyData is directly included in DuplicateBlockProofInstructionData, then this shouldn't be an issue!

Comment on lines 34 to 37
pub(crate) expected_shred1_merkle_root: &'a Hash,
pub(crate) expected_shred2_merkle_root: &'a Hash,
pub(crate) expected_shred1_signature: &'a [u8; SIGNATURE_BYTES],
pub(crate) expected_shred2_signature: &'a [u8; SIGNATURE_BYTES],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit: can this be laid out in the same order as the instruction? That way you could eventually just cast the instruction data to this type and avoid doing any work to create it, ie

Suggested change
pub(crate) expected_shred1_merkle_root: &'a Hash,
pub(crate) expected_shred2_merkle_root: &'a Hash,
pub(crate) expected_shred1_signature: &'a [u8; SIGNATURE_BYTES],
pub(crate) expected_shred2_signature: &'a [u8; SIGNATURE_BYTES],
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],

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep done here b5b5b0f

just cast the instruction data to this type and avoid doing any work to create it

For this are you suggesting that we don't actually copy the data over when doing the instruction introspection but instead just cast the instruction data?

@@ -243,17 +320,82 @@ 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`.
/// Leader's sign the merkle root of each shred with their pubkey.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

micro nit typo

Suggested change
/// Leader's sign the merkle root of each shred with their pubkey.
/// Leaders sign the merkle root of each shred with their pubkey.

Comment on lines 412 to 416
// Hack to simulate the merkle roots being stored in instruction data
let expected_shred1_merkle_root =
unsafe { &*Box::into_raw(Box::new(shred1.merkle_root().unwrap_or_default())) };
let expected_shred2_merkle_root =
unsafe { &*Box::into_raw(Box::new(shred2.merkle_root().unwrap_or_default())) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's only testing, but this is a bit brittle, no? If the underlying Hash ever gets cleaned up we'll get hard-to-reproduce memory issues during testing.

I appreciate it's more annoying to use, but do you mind passing in the merkle root refs as extra parameters to the function? That way we can avoid doing this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep good call I was just lazy haha ff6e4de

Comment on lines 1132 to 1134
assert!(proof_data
.verify_proof(context, SLOT, &leader_pubkey)
.is_ok());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I typically prefer doing an unwrap() to get a better error message if something goes wrong, ie

Suggested change
assert!(proof_data
.verify_proof(context, SLOT, &leader_pubkey)
.is_ok());
proof_data
.verify_proof(context, SLOT, &leader_pubkey)
.unwrap();

Comment on lines 257 to 260
assert!(
ErasureMeta::check_erasure_consistency(&shred1, &shred2),
"Expected erasure consistency failure",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to remove this check?

Copy link
Contributor Author

@AshwinSekar AshwinSekar Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's not completely accurate to say it's an erasure consistency failure here, the real failure is the merkle root of shred1 and shred2 will not be consistent. i've updated the condition here.

@AshwinSekar AshwinSekar requested a review from joncinque January 24, 2025 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants