Skip to content

Commit 339aa9a

Browse files
committed
SIMD-0204: Slashable event verification
1 parent fda3d17 commit 339aa9a

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
---
2+
simd: '0204'
3+
title: Slashable event verification
4+
authors:
5+
- Ashwin Sekar
6+
category: Standard
7+
type: Core
8+
status: Review
9+
created: 2024-11-26
10+
feature: (fill in with feature tracking issues once accepted)
11+
---
12+
13+
## Summary
14+
15+
This proposal describes an enshrined on-chain program to verify proofs that a
16+
validator committed a slashable infraction. This program creates reports on chain
17+
for use in future SIMDs.
18+
19+
**This proposal does not modify any stakes or rewards, the program will
20+
only verify and log infractions.**
21+
22+
## Motivation
23+
24+
There exists a class of protocol violations that are difficult to detect synchronously,
25+
but are simple to detect after the fact. In order to penalize violators we provide
26+
a means to record these violations on chain.
27+
28+
This also serves as a starting point for observability and discussions around the
29+
economics of penalizing these violators. This is a necessary step to implement
30+
slashing in the Solana Protocol.
31+
32+
## New Terminology
33+
34+
None
35+
36+
### Feature flags
37+
38+
`create_slashing_program`:
39+
40+
- `sProgVaNWkYdP2eTRAy1CPrgb3b9p8yXCASrPEqo6VJ`
41+
42+
## Detailed Design
43+
44+
On the epoch boundary where the `create_slashing_program` feature flag is first
45+
activated the following behavior will be executed in the first block for the new
46+
epoch:
47+
48+
1. Create a new program account at `S1ashing11111111111111111111111111111111111`
49+
with an upgrade authority set to the system program
50+
`11111111111111111111111111111111`
51+
52+
2. Verify that the program account
53+
`8sT74BE7sanh4iT84EyVUL8b77cVruLHXGjvTyJ4GwCe` has a verified build hash of
54+
`<FILL IN AFTER IMPLEMENTATION>` [\[1\]](#notes)
55+
56+
3. Copy the contents of `8sT74BE7sanh4iT84EyVUL8b77cVruLHXGjvTyJ4GwCe` into
57+
`S1ashing11111111111111111111111111111111111`
58+
59+
This program (hereafter referred to as the slashing program) supports 2
60+
instructions `DuplicateBlockProof`, and `CloseProofReport`.
61+
62+
`DuplicateBlockProof` requires 1 account:
63+
64+
0. `proof_account`, expected to be previously intiialized with the proof data.
65+
66+
`DuplicateBlockProof` has an instruction data of 48 bytes, containing:
67+
68+
- `0x00`, a fixed-value byte acting as the instruction discriminator
69+
- `offset`, an unaligned eight-byte little-endian unsigned integer indicating
70+
the offset from which to read the proof
71+
- `slot`, an unaligned eight-byte little-endian unsigned integer indicating the
72+
slot in which the violation occured
73+
- `node_pubkey`, an unaligned 32 byte array representing the public key of the
74+
node which committed the violation
75+
76+
We expect the contents of the `proof_account` when read from `offset` to
77+
deserialize to a struct of two byte arrays representing the duplicate shreds.
78+
The first 4 bytes correspond to the length of the first shred, and the 4 bytes
79+
after that shred correspond to the length of the second shred.
80+
81+
```rust
82+
struct DuplicateBlockProofData {
83+
shred1_length: u32,
84+
shred1: &[u8],
85+
shred2_length: u32,
86+
shred2: &[u8]
87+
}
88+
```
89+
90+
`DuplicateBlockProof` aborts if:
91+
92+
- The difference between the current slot and `slot` is greater than 1 epoch's
93+
worth of slots as reported by the `Clock` sysvar
94+
- `offset` is larger than the length of `proof_account`
95+
- `proof_account[offset..]` does not deserialize cleanly to a
96+
`DuplicateBlockProofData`.
97+
- The resulting shreds do not adhere to the Solana shred format [\[2\]](#notes)
98+
or are legacy shred variants.
99+
- The resulting shreds specify a slot that is different from `slot`.
100+
- The resulting shreds specify different shred versions.
101+
102+
After deserialization the slashing program will attempt to verify the proof, by
103+
checking that `shred1` and `shred2` constitute a valid duplicate proof for
104+
`slot` and are correctly signed by `node_pubkey`. This is similar to logic used
105+
in Solana's gossip protocol to verify duplicate proofs for use in fork choice.
106+
107+
### Proof verification
108+
109+
`shred1` and `shred2` constitute a valid duplicate proof if any of the following
110+
conditions are met:
111+
112+
- Both shreds specify the same index and shred type, however their payloads
113+
differ
114+
- Both shreds specify the same FEC set, however their merkle roots differ
115+
- Both shreds specify the same FEC set and are coding shreds, however their
116+
erasure configs conflict
117+
- At least one shred is a coding shred, and its erasure meta indicates an FEC set
118+
overlap.
119+
- The shreds are data shreds with different indices and the shred with the lower
120+
index has the `LAST_SHRED_IN_SLOT` flag set
121+
122+
Note: We do not verify that `node_pubkey` was the leader for `slot`. Any node that
123+
willingly signs duplicate shreds for a slot that they are not a leader for is
124+
eligible for slashing.
125+
126+
---
127+
128+
### Signature verification
129+
130+
In order to verify that `shred1` and `shred2` were correctly signed by
131+
`node_pubkey` we use instruction retrospection.
132+
133+
Using the `Instructions` sysvar we verify that the previous two instructions of
134+
this transaction are for the program ID
135+
`Ed25519SigVerify111111111111111111111111111`
136+
137+
For each of these instructions, verify the instruction data:
138+
139+
- The first byte is `0x01`
140+
- The second byte (padding) is `0x00`
141+
142+
And then deserialize the remaining instruction data as 2 byte little-endian
143+
unsigned integers:
144+
145+
```rust
146+
struct Ed25519SignatureOffsets {
147+
signature_offset: u16, // offset to ed25519 signature of 64 bytes
148+
signature_instruction_index: u16, // instruction index to find signature
149+
public_key_offset: u16, // offset to public key of 32 bytes
150+
public_key_instruction_index: u16, // instruction index to find public key
151+
message_data_offset: u16, // offset to start of message data
152+
message_data_size: u16, // size of message data
153+
message_instruction_index: u16, // index of instruction data to get message
154+
// data
155+
}
156+
```
157+
158+
We wish to verify that these instructions correspond to
159+
160+
```
161+
verify(pubkey = node_pubkey, message = shred1.merkle_root, signature = shred1.signature)
162+
verify(pubkey = node_pubkey, message = shred2.merkle_root, signature = shred2.signature)
163+
```
164+
165+
We use the deserialized offsets to calculate [\[3\]](#notes) the `pubkey`,
166+
`message`, and `signature` of each instruction and verify that they correspond
167+
to the `node_pubkey`, `merkle_root`, and `signature` specified by the shred payload.
168+
169+
If both proof and signer verification succeed, we continue on to store the incident.
170+
171+
---
172+
173+
### Incident reporting
174+
175+
After verifying a successful proof we store the results in a program derived
176+
address for future use. The PDA is derived using the `node_pubkey`, `slot`, and
177+
the violation type:
178+
179+
```rust
180+
let (pda, _) = find_program_address(&[
181+
node_pubkey.to_bytes(),
182+
slot.to_le_bytes(),
183+
ViolationType::DuplicateBlock.to_u8(),
184+
])
185+
```
186+
187+
At the moment `DuplicateBlock` is the only violation type but future work will
188+
add additional slashing types.
189+
190+
If the `pda` account has non-zero lamports, then we abort as the violation has
191+
already been reported. Otherwise we create the account, with the slashing program
192+
as the owner. In this account we store the following:
193+
194+
```rust
195+
struct ProofReport {
196+
reporter: Pubkey, // Fee payer, to allow the account to be closed
197+
epoch: Epoch, // Epoch in which this report was created
198+
pubkey: Pubkey, // The pubkey of the node that committed the violation
199+
slot: Slot, // Slot in which the violation occured
200+
violation_type: u8, // The violation type
201+
proof: Vec<u8> // The serialized proof
202+
proof_account: Option<Pubkey>, // Optional account where proof is stored instead
203+
}
204+
```
205+
206+
The `DuplicateBlockProofData` is serialized into the `proof` field. This provides
207+
an on chain trail of the reporting process, since the `proof_account` supplied in
208+
the `DuplicateBlockProof` account could later be modified.
209+
210+
The `pubkey` is populated with the `node_pubkey`. For future violation types that
211+
involve votes, this will instead be populated with the vote account's pubkey.
212+
The work in SIMD-0180 will allow the `node_pubkey` to be translated to a vote account
213+
if needed.
214+
215+
Note that PDA's can only be created with a 10kb initial size.
216+
Although not a problem for `DuplicateBlockProofData`, if future proof types require
217+
more space, we allow the proof to be stored in a separate account, and linked back
218+
to the PDA using the `proof_account` field.
219+
220+
---
221+
222+
### Closing the incident report
223+
224+
After the slashing violation has been processed by the runtime, the initial fee
225+
payer may wish to close their `ProofReport` account to reclaim the lamports.
226+
227+
They can accomplish this via the `CloseProofReport` instruction which requires
228+
2 accounts:
229+
230+
0. `report_account`: The PDA account storing the report: Writable, owned by the
231+
slashing program
232+
1. `destination`: Writable account to reclaim the lamports
233+
234+
`CloseProofReport` has an instruction data of 42 bytes, containing:
235+
236+
- `0x01`, a fixed-value byte acting as the instruction discriminator
237+
- `violation_type`, a one byte value acting as the violation type discriminator
238+
- `slot`, an unaligned eight-byte little-endian unsigned integer indicating the
239+
slot which was reported
240+
- `pubkey`, an unaligned 32 byte array representing the public key of the node
241+
which was reported
242+
243+
We abort if:
244+
245+
- `violation_type` is not `0x00` (corresponds to `DuplicateBlock` violation)
246+
- Deriving the pda using `pubkey`, `slot`, and `ViolationType::DuplicateBlock`
247+
as outlined above does not result in the adddress of `report_account`
248+
- `report_account` is not writeable
249+
- `report_account` does not deserialize cleanly to `ProofReport`
250+
- `report_account.reporter` is not a signer
251+
- `report_account.epoch + 3` is greater than the current epoch reported from
252+
the `Clock` sysvar. We want to ensure that these accounts do not get closed before
253+
they are observed by indexers and dashboards.
254+
255+
Otherwise we close the `report_account` and credit the `lamports` to `destination`
256+
257+
---
258+
259+
## Alternatives Considered
260+
261+
This proposal deploys the slashing program in an "enshrined" account, only upgradeable
262+
through code changes in the validator software. Alternatively we could follow the
263+
SPL program convention and deploy to an account upgradeable by a multisig. This
264+
allows for more flexibility in the case of deploying hotfixes or rapid changes,
265+
however allowing upgrade access to such a sensitive part of the system via a handful
266+
of engineers poses a security risk.
267+
268+
## Impact
269+
270+
A new program will be enshrined at `S1ashing11111111111111111111111111111111111`.
271+
272+
Reports stored in PDAs of this program might be queried for dashboards which could
273+
incur additional indexing overhead for RPC providers.
274+
275+
## Security Considerations
276+
277+
None
278+
279+
## Drawbacks
280+
281+
None
282+
283+
## Backwards Compatibility
284+
285+
The feature is not backwards compatible
286+
287+
## Notes
288+
289+
\[1\]: Sha256 of program data, see
290+
https://github.com/Ellipsis-Labs/solana-verifiable-build/blob/214ba849946be0f7ec6a13d860f43afe125beea3/src/main.rs#L331
291+
for details.
292+
293+
\[2\]: The slashing program will support any combination of merkle shreds, chained
294+
merkle shreds, and retransmitter signed chained merkle shreds, see https://github.com/anza-xyz/agave/blob/4e7f7f76f453e126b171c800bbaca2cb28637535/ledger/src/shred.rs#L6
295+
for the full specification.
296+
297+
\[3\]: Example of offset calculation can be found here https://docs.solanalabs.com/runtime/programs#ed25519-program

0 commit comments

Comments
 (0)