Просмотр исходного кода

SIMD-0204: Slashable event verification

* SIMD-0204: Slashable event verification

* pr feedback

* make close account permissionless

* spell out three epoch window rational

* duplicate proof -> duplicate block proof

* remove lamport check in favor of data check

* instructions sysvar

* specify the default loader, and move program-data

* pr feedback: lower shred is coding for overlap

* pr feedback: use separate destination account for closing report

* Add chained merkle root condition

* update sigverify to store data in the slashing instruction data

* update proof reporting specification

* reorder system program as last account

* add reporter in ix data, prefund requirements

* include the destination account in CloseViolationReport

* update buffer address, add destination check, remove report proof account

* fill in verifiable build hash

* update the deployment approach

* rename fields in ProofReport and CloseViolationReport

* fix missing //
Ashwin Sekar 6 месяцев назад
Родитель
Сommit
a4843b1841
1 измененных файлов с 357 добавлено и 0 удалено
  1. 357 0
      proposals/0204-slashable-event-verification.md

+ 357 - 0
proposals/0204-slashable-event-verification.md

@@ -0,0 +1,357 @@
+---
+simd: '0204'
+title: Slashable event verification
+authors:
+  - Ashwin Sekar
+category: Standard
+type: Core
+status: Review
+created: 2024-11-26
+feature: (fill in with feature tracking issues once accepted)
+---
+
+## Summary
+
+This proposal describes an enshrined on-chain program to verify proofs that a
+validator committed a slashable infraction. This program creates reports on chain
+for use in future SIMDs.
+
+**This proposal does not modify any stakes or rewards, the program will
+only verify and log infractions.**
+
+## Motivation
+
+There exists a class of protocol violations that are difficult to detect synchronously,
+but are simple to detect after the fact. In order to penalize violators we provide
+a means to record these violations on chain.
+
+This also serves as a starting point for observability and discussions around the
+economics of penalizing these violators. This is a necessary step to implement
+slashing in the Solana Protocol.
+
+## New Terminology
+
+None
+
+### Feature flags
+
+`create_slashing_program`:
+
+- `sProgVaNWkYdP2eTRAy1CPrgb3b9p8yXCASrPEqo6VJ`
+
+## Detailed Design
+
+On the epoch boundary where the `create_slashing_program` feature flag is first
+activated the following behavior will be executed in the first block for the new
+epoch:
+
+1. Create a new program account at `S1ashing11111111111111111111111111111111111`
+  owned by the default upgradeable loader with an upgrade authority set to `None`,
+  and create the associated program data account.
+
+2. Verify that the buffer account
+  `S1asHs4je6wPb2kWiHqNNdpNRiDaBEDQyfyCThhsrgv` has a verified build hash of
+  `192ed727334abe822d5accba8b886e25f88b03c76973c2e7290cfb55b9e1115f` [\[1\]](#notes)
+
+3. Serialize the contents of `S1asHs4je6wPb2kWiHqNNdpNRiDaBEDQyfyCThhsrgv` into
+  the program data account for `S1ashing11111111111111111111111111111111111`
+
+4. Invoke the loader to deploy the new program account and program data account.
+  This step also updates the program cache.
+
+4. Zero out the buffer account `S1asHs4je6wPb2kWiHqNNdpNRiDaBEDQyfyCThhsrgv`, and
+  update the changes to capitalization and account data lengths accordingly.
+
+This is the only protocol change that clients need to implement. The remaining
+proposal describes the function of this program, hereafter referred to as the
+slashing program.
+
+The buffer account `S1asHs4je6wPb2kWiHqNNdpNRiDaBEDQyfyCThhsrgv` will hold the
+v1.0.0 release of the slashing program
+https://github.com/solana-program/slashing/releases/tag/v1.0.0
+
+### Slashing Program
+
+This slashing program supports two instructions `DuplicateBlockProof`, and
+`CloseViolationReport`.
+
+`DuplicateBlockProof` requires 2 accounts, the `Instructions` sysvar, and the
+system program :
+
+0. `proof_account`, expected to be previously initialized with the proof data.
+1. `report_account`, the PDA in which to store the violation report. See the below
+    section for details. Must be writable.
+2. `instructions`, Instructions sysvar
+3. `system_program_account`, required to create the violation report.
+
+`DuplicateBlockProof` has an instruction data of 305 bytes, containing:
+
+- `0x01`, a fixed-value byte acting as the instruction discriminator
+- `offset`, an unaligned eight-byte little-endian unsigned integer indicating
+  the offset from which to read the proof
+- `slot`, an unaligned eight-byte little-endian unsigned integer indicating the
+  slot in which the violation occured
+- `node_pubkey`, an unaligned 32 byte array representing the public key of the
+  node which committed the violation
+- `reporter`, an unaligned 32 byte array representing the account to credit
+  as the first reporter of this violation if a successful slashing report is
+  written.
+- `destination`, an unaligned 32 byte array representing the account to reclaim
+  the lamports if a successful slashing report account is created and then later
+  closed.
+- `shred_1_merkle_root`, an unaligned 32 byte array representing the merkle root
+  of the first shred in the `proof_account`
+- `shred_1_signature`, an unaligned 64 byte array representing the signature
+  of `node_pubkey` on the first shred in the `proof_account`
+- `shred_2_merkle_root`, an unaligned 32 byte array representing the merkle root
+  of the second shred in the `proof_account`
+- `shred_2_signature`, an unaligned 64 byte array representing the signature
+  of `node_pubkey` on the second shred in the `proof_account`
+
+We expect the contents of the `proof_account` when read from `offset` to
+deserialize to two byte arrays representing the duplicate shreds.
+The first 4 bytes correspond to the length of the first shred, and the 4 bytes
+after that shred correspond to the length of the second shred.
+
+```rust
+struct DuplicateBlockProofData {
+  shred1_length: u32      // Unaligned four-byte little-endian unsigned integer,
+  shred1: &[u8]           // `shred1_length` bytes representing a shred,
+  shred2_length: u32      // Unaligned four-byte little-endian unsigned integer,
+  shred2: &[u8]           // `shred2_length` bytes representing a shred,
+}
+```
+
+Users are expected to populate the `proof_account` themselves, using an onchain
+program such as the Record program.
+
+`DuplicateBlockProof` aborts if:
+
+- The difference between the current slot and `slot` is greater than 1 epoch's
+  worth of slots as reported by the `Clock` sysvar
+- The `destination` is equal to the address of `pda_account`
+- `offset` is larger than the length of `proof_account`
+- `proof_account[offset..]` does not deserialize cleanly to a
+  `DuplicateBlockProofData`.
+- The resulting shreds do not adhere to the Solana shred format [\[2\]](#notes)
+  or are legacy shred variants.
+- The resulting shreds specify a slot that is different from `slot`.
+- The resulting shreds specify different shred versions.
+
+After deserialization the slashing program will attempt to verify the proof, by
+checking that `shred1` and `shred2` constitute a valid duplicate block proof for
+`slot` and are correctly signed by `node_pubkey`. This is similar to logic used
+in Solana's gossip protocol to verify duplicate block proofs for use in fork choice.
+
+#### Proof verification
+
+`shred1` and `shred2` constitute a valid duplicate block proof if any of the
+following conditions are met:
+
+- Both shreds specify the same index and shred type, however their payloads
+  differ
+- Both shreds specify the same FEC set, however their merkle roots differ
+- Both shreds specify the same FEC set and are coding shreds, however their
+  erasure configs conflict
+- The shreds specify different FEC sets, the lower index shred is a coding shred,
+  and its erasure meta indicates an FEC set overlap
+- The shreds specify different FEC sets, the lower index shred has a merkle root
+  that is not equal to the chained merkle root of the higher index shred
+- The shreds are data shreds with different indices and the shred with the lower
+  index has the `LAST_SHRED_IN_SLOT` flag set
+
+Note: We do not verify that `node_pubkey` was the leader for `slot`. Any node that
+willingly signs duplicate shreds for a slot that they are not a leader for is
+eligible for slashing.
+
+---
+
+#### Signature verification
+
+In order to verify that `shred1` and `shred2` were correctly signed by
+`node_pubkey` we use instruction introspection.
+
+Using the `Instructions` sysvar we verify that the previous instruction of
+this transaction are for the program ID
+`Ed25519SigVerify111111111111111111111111111`
+
+For this instruction, verify the instruction data:
+
+- The first byte is `0x02`
+- The second byte (padding) is `0x00`
+
+Verify that the remaining instruction data represents two signature offsets
+which is specified as 2 byte little-endian unsigned integers:
+
+```rust
+struct Ed25519SignatureOffsets {
+    signature_offset: u16,             // offset to ed25519 signature of 64 bytes
+    signature_instruction_index: u16,  // instruction index to find signature
+    public_key_offset: u16,            // offset to public key of 32 bytes
+    public_key_instruction_index: u16, // instruction index to find public key
+    message_data_offset: u16,          // offset to start of message data
+    message_data_size: u16,            // size of message data
+    message_instruction_index: u16,    // index of instruction data to get message
+                                       // data
+}
+```
+
+We wish to verify that these instructions correspond to
+
+```
+verify(pubkey = node_pubkey, message = shred1.merkle_root, signature = shred1.signature)
+verify(pubkey = node_pubkey, message = shred2.merkle_root, signature = shred2.signature)
+```
+
+We use the deserialized offsets to calculate [\[3\]](#notes) the `pubkey`,
+`message`, and `signature` of each instruction and verify that they correspond
+to the `node_pubkey`, `merkle_root`, and `signature` specified by the shred payload.
+
+The instruction indices must point to the `DuplicateBlockProof` instruction and
+the offsets into the instruction data where these values are stored.
+
+If both proof and signer verification succeed, we continue on to store the incident.
+
+---
+
+#### Incident reporting
+
+After verifying a successful proof we store the results in a program derived
+address for future use. The PDA is derived using the `node_pubkey`, `slot`, and
+the violation type:
+
+```rust
+let (pda, _) = find_program_address(&[
+  node_pubkey.to_bytes(),        // 32 byte array representing the public key
+  slot.to_le_bytes(),            // Unsigned little-endian eight-byte integer
+  1u8,                           // Byte representing the violation type
+])
+```
+
+If the `pda` is not equal to the addres of the `pda_account` then we abort.
+
+At the moment `DuplicateBlock` is the only violation type but future work will
+add additional slashing types.
+
+We expect the `pda` account to be prefund-ed by the user to contain enough lamports
+to store a `ProofReport`.
+
+If the `pda` account has any data, is owned by the slashing program, and the version
+number is non zero then we abort as the violation has already been reported.
+Otherwise we allocate space in the account, and assign the slashing program as
+the owner. In this account we store the following:
+
+```rust
+struct ProofReport {
+  version: u8,                     // 1 byte specifying the version number,
+                                   // currently 1
+
+  reporter: Pubkey,                // 32 byte array representing the pubkey of the
+                                   // Fee payer, who reported this violation
+
+  destination: Pubkey,             // 32 byte array representing the account to
+                                   // credit the lamports when this proof report
+                                   // is closed.
+
+  epoch: Epoch,                    // Unaligned unsigned eight-byte little endian
+                                   // integer representing the epoch in which this
+                                   // report was created
+
+  violator: Pubkey,                // 32 byte array representing the pubkey of the
+                                   // node that committed the violation
+
+  slot: Slot,                      // Unaligned unsigned eight-byte little endian
+                                   // integer representing the slot in which the
+                                   // violation occured
+
+  violation_type: u8,              // Byte representing the violation type
+}
+```
+
+immediately followed by a `DuplicateBlockProofData`.
+
+This proof data provides an on chain trail of the reporting process, since the
+`proof_account` supplied in the `DuplicateBlockProof` instruction could later
+be modified.
+
+The `pubkey` is populated with the `node_pubkey`. For future violation types that
+involve votes, this will instead be populated with the vote account's pubkey.
+The work in SIMD-0180 will allow the `node_pubkey` to be translated to a vote account
+if needed.
+
+---
+
+#### Closing the incident report
+
+In a future SIMD the reports will be used for runtime processing. This is out of
+scope, but after this period has passed,  the initial fee payer may wish to close
+their `ProofReport` account to reclaim the lamports.
+
+They can accomplish this via the `CloseViolationReport` instruction which requires
+two accounts
+
+0. `report_account`: The PDA account storing the report: Writable, owned by the
+  slashing program
+1. `destination_account`: The destination account to receive the lamports: Writable
+
+`CloseViolationReport` has an instruction data of one byte, containing:
+
+- `0x00`, a fixed-value byte acting as the instruction discriminator
+
+We abort if:
+
+- `report_account` is not owned by the slashing program
+- `report_account` does not deserialize cleanly to `ProofReport`
+- `report_account.destination` does not match `destination_account`
+- `report_account.epoch + 3` is greater than the current epoch reported from
+  the `Clock` sysvar. We want to ensure that these accounts do not get closed before
+  they are observed by indexers and dashboards.
+
+The three epoch window is somewhat arbitrary, we only need the `report_account` to
+last at least one epoch in order to for it to be observed by the runtime as part
+of a future SIMD.
+
+Otherwise we set the owner of `report_account` to the system program, rellocate
+the account to 0 bytes, and credit the `lamports` to `destination_account`
+
+---
+
+## Alternatives Considered
+
+This proposal deploys the slashing program in an "enshrined" account, only upgradeable
+through code changes in the validator software. Alternatively we could follow the
+SPL program convention and deploy to an account upgradeable by a multisig. This
+allows for more flexibility in the case of deploying hotfixes or rapid changes,
+however allowing upgrade access to such a sensitive part of the system via a handful
+of engineers poses a security risk.
+
+## Impact
+
+A new program will be enshrined at `S1ashing11111111111111111111111111111111111`.
+
+Reports stored in PDAs of this program might be queried for dashboards which could
+incur additional indexing overhead for RPC providers.
+
+## Security Considerations
+
+None
+
+## Drawbacks
+
+None
+
+## Backwards Compatibility
+
+The feature is not backwards compatible
+
+## Notes
+
+\[1\]: Sha256 of program data, see
+  https://github.com/Ellipsis-Labs/solana-verifiable-build/blob/214ba849946be0f7ec6a13d860f43afe125beea3/src/main.rs#L331
+  for details.
+
+\[2\]: The slashing program will support any combination of merkle shreds, chained
+  merkle shreds, and retransmitter signed chained merkle shreds, see https://github.com/anza-xyz/agave/blob/4e7f7f76f453e126b171c800bbaca2cb28637535/ledger/src/shred.rs#L6
+  for the full specification.
+
+\[3\]: Example of offset calculation can be found here https://docs.solanalabs.com/runtime/programs#ed25519-program