-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
--- | ||
simd: '0152' | ||
title: Precompiles | ||
authors: | ||
- Emanuele Cesena | ||
category: Standard | ||
type: Core | ||
status: Draft | ||
created: 2024-06-03 | ||
feature: | ||
supersedes: | ||
superseded-by: | ||
extends: | ||
--- | ||
|
||
## Summary | ||
|
||
Define a unified behavior for precompiles, and highlight a few minor | ||
changes to the existing precompiles to minimize differences. | ||
|
||
## Motivation | ||
|
||
Precompiles are special native programs designed to verify additional | ||
transaction signatures. | ||
They run without the VM and without loading any account, and they can access | ||
data from other instructions, within the same transaction. | ||
|
||
At the time of writing, two precompiles exist to verify Ed25519 and | ||
Ethereum-like Secp256k1 signatures, and another one is being proposed to | ||
support Secp256r1 signatures for FIDO Passkeys. | ||
|
||
Historically, the two precompiles were built at different times and | ||
by different people, so naturally there are some subtle differences | ||
in how they behave, especially in edge cases. | ||
|
||
The main goal of this document is to provide a specification for how | ||
a precompile should behave, remove differences and provide guidelines | ||
for future proposals. | ||
|
||
In addition, we highlight 3 minor changes to the existing precompiles | ||
that will simplify their behavior and make it easier to develop | ||
alternative validator clients. | ||
|
||
## Alternatives Considered | ||
|
||
Leave as is. | ||
|
||
## New Terminology | ||
|
||
n/a | ||
|
||
## Detailed Design | ||
|
||
### Generic Precompile | ||
|
||
Precompiles are special native programs designed to verify additional | ||
signatures. Each precompile consists of a single `verify` instruction. | ||
|
||
Precompiles are executed right after transaction signature verification, | ||
they run without the VM and without loading any account. | ||
|
||
If a transaction contains more than 8 precompile signatures, it must fail. | ||
``` | ||
Check failure on line 63 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterFenced code blocks should be surrounded by blank lines [Context: "```"]
Check failure on line 63 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterFenced code blocks should be surrounded by blank lines [Context: "```"]
|
||
MAX_ALLOWED_PRECOMPILE_SIGNATURES = 8 | ||
``` | ||
|
||
The precompile instruction `verify` accepts the following data: | ||
``` | ||
Check failure on line 68 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterFenced code blocks should be surrounded by blank lines [Context: "```"]
Check failure on line 68 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterFenced code blocks should be surrounded by blank lines [Context: "```"]
|
||
struct PrecompileVerifyInstruction { | ||
num_signatures: u8, // Number of signatures to verify | ||
padding: u8, // Single byte padding | ||
offsets: Array<PrecompileOffsets>, // Array of offsets | ||
additionalData?: Bytes, // Optional additional data, e.g. | ||
// signatures included in the same | ||
// instruction | ||
} | ||
struct PrecompileOffsets { | ||
signature_offset: u16 LE, // Offset to signature | ||
signature_instruction_index: u16 LE, // Instruction index to signature | ||
public_key_offset: u16 LE, // Offset to public key | ||
public_key_instruction_index: u16 LE, // Instruction index to public key | ||
message_offset: u16 LE, // Offset to start of message data | ||
message_length: u16 LE, // Size of message data | ||
message_instruction_index: u16 LE, // Instruction index to message | ||
} | ||
``` | ||
|
||
The behavior of the precompile instruction `verify` is as follow: | ||
1. If instruction `data` is empty, return error. | ||
Check failure on line 90 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterLists should be surrounded by blank lines [Context: "1. If instruction `data` is em..."]
Check failure on line 90 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterLists should be surrounded by blank lines [Context: "1. If instruction `data` is em..."]
|
||
2. The first byte of `data` is the number of signatures `num_signatures`. | ||
3. If `num_signatures` is 0, return error. | ||
4. Expect (enough bytes of `data` for) `num_signatures` instances of | ||
`PrecompileOffsets`. | ||
5. For each signature: | ||
a. Read `offsets`: an instance of `PrecompileOffsets` | ||
b. Based on the `offsets`, retrieve `signature`, `public_key`, and | ||
`message` bytes. If any of the three fails, return error. | ||
c. Invoke the actual `sigverify` function. If it fails, return error. | ||
|
||
To retrieve `signature`, `public_key`, and `message`: | ||
1. Get the `instruction_index`-th `instruction_data` | ||
Check failure on line 102 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterLists should be surrounded by blank lines [Context: "1. Get the `instruction_index`..."]
Check failure on line 102 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterLists should be surrounded by blank lines [Context: "1. Get the `instruction_index`..."]
|
||
- The special value `0xFFFF` means "current instruction" | ||
- If the index is invalid, return Error | ||
2. Return `length` bytes starting from `offset` | ||
- If this exceeds the `instruction_data` length, return Error | ||
|
||
If the precompile `verify` function returns any error, the whole transaction | ||
should fail. Therefore, the type of error is irrelevant and is left as an | ||
implementation detail. | ||
|
||
In pseudo-code: | ||
``` | ||
Check failure on line 113 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterFenced code blocks should be surrounded by blank lines [Context: "```"]
Check failure on line 113 in proposals/0152-precompiles.md GitHub Actions / Markdown LinterFenced code blocks should be surrounded by blank lines [Context: "```"]
|
||
fn verify() { | ||
if data_length == 0 { | ||
return Error | ||
} | ||
num_signatures = data[0] | ||
if num_signatures == 0 { | ||
return Error | ||
} | ||
if data_length < (2 + num_signatures * size_of_offsets) { | ||
return Error | ||
} | ||
all_tx_data = { data, instruction_datas } | ||
data_position = 2 | ||
for i in 0..num_signatures { | ||
offsets = (PrecompileOffsets) | ||
data[data_position..data_position+size_of_offsets] | ||
data_position += size_of_offsets | ||
signature = get_data_slice(all_tx_data, | ||
offsets.signature_instruction_index, | ||
offsets.signature_offset | ||
signature_length) | ||
if !signature { | ||
return Error | ||
} | ||
public_key = get_data_slice(all_tx_data, | ||
offsets.public_key_instruction_index, | ||
offsets.public_key_offset, | ||
public_key_length) | ||
if !public_key { | ||
return Error | ||
} | ||
message = get_data_slice(all_tx_data, | ||
offsets.message_instruction_index, | ||
offsets.message_offset | ||
offsets.message_length) | ||
if !message { | ||
return Error | ||
} | ||
// sigverify includes validating signature and public_key | ||
result = sigverify(signature, public_key, message) | ||
if result != Success { | ||
return Error | ||
} | ||
} | ||
return Success | ||
} | ||
fn get_data_slice(all_tx_data, instruction_index, offset, length) { | ||
// Get the right instruction_data | ||
if instruction_index == 0xFFFF { | ||
instruction_data = all_tx_data.data | ||
} else { | ||
if instruction_index >= num_instructions { | ||
return Error | ||
} | ||
instruction_data = all_tx_data.instruction_datas[instruction_index] | ||
} | ||
start = offset | ||
end = offset + length | ||
if end > instruction_data_length { | ||
return Error | ||
} | ||
return instruction_data[start..end] | ||
} | ||
``` | ||
|
||
### Changes to `Ed25519SigVerify111111111111111111111111111` | ||
|
||
**Summary.** | ||
|
||
- Change #1. Replace sigverify function with Dalek `strict_verify()`, | ||
the same used for transactions sigverify. | ||
|
||
- Change #2. Implement step 3 above: "If `num_signatures` is 0, return error." | ||
|
||
**Context.** | ||
|
||
In Solana, transactions use Ed25199 signatures, and are validated using | ||
the so called **strict verify**. | ||
Compared to "RFC verify", strict verify enforces extra checks against | ||
(certain types of) malleability. | ||
|
||
The Ed25199 precompile currently implements a non-strict verify, so with | ||
Change #1 we'll make it compatible with the way Solana verifies signatures. | ||
|
||
Moreover, the Ed25519 precompile accepts a payload of `[0, 0]` as valid | ||
(for no good reason), so Change #2 will prevent this anomaly. | ||
|
||
Finally, the Ed25519 precompile interleaves retrieving instruction data | ||
and parsing data types such as signatures and public keys. | ||
While this goes against this specification and creates unnecessary complexity | ||
in the return error code, we recommend to NOT change the internal behavior | ||
(as the return error code doesn't really matter). | ||
|
||
**FAQ.** | ||
|
||
- **Q: Why does the Ed25199 precompile currently use `verify` instead of | ||
`strict_verify`?** | ||
A: No good reason, it was built without noticing the difference. | ||
|
||
- **Q: If we switch to `strict_verify`, will some of the existing signatures | ||
break verification?** | ||
A: All signatures created by a "regular" library, i.e. following RFC, | ||
pass both `verify` and `strict_verify`. | ||
Only carefully crafted signatures can pass `verify` and not `strict_verify`. | ||
So this won't break any honest use case. | ||
|
||
- **Q: Why not leaving it as is?** | ||
A: The `verify` is not well specified. In fact, it's behavior is slightly | ||
different in the older version of Dalek that Solana currently uses, versus | ||
the latest version of the same library. | ||
Trying to replicate all the edge cases is different validators is an | ||
unnecessary effort, not worth the risk of exposing different behaviors. | ||
|
||
### Changes to `KeccakSecp256k11111111111111111111111111111` | ||
|
||
**Summary.** | ||
|
||
- Change #3. Implement step 3 above: "If `num_signatures` is 0, return error." | ||
|
||
**Context.** | ||
|
||
The KeccakSecp256k1 precompile currently accepts an input of `[0]`, as | ||
in "verify 0 signatures", which is a useless instruction. | ||
With Change #3 we'll avoid this anomaly. | ||
|
||
We note that the KeccakSecp256k1 precompile has a slightly different | ||
struct for offset, with instruction indexes of a single byte (and, as | ||
a result, no special value of `0xFFFF` to indicate the "current instruction"). | ||
This is for historical reasons, and since modifying it would break | ||
existing users, we recommend to NOT change the existing behavior. | ||
|
||
## Impact | ||
|
||
Reduce the complexity of existing precompiles, to simplify building | ||
different validator clients. | ||
|
||
## Security Considerations | ||
|
||
All 3 changes are straightforward and have no impact on security. | ||
|
||
## Backwards Compatibility | ||
|
||
All 3 changes require a feature gate. |