Skip to content

Commit

Permalink
SIMD-0152: Precompiles
Browse files Browse the repository at this point in the history
  • Loading branch information
0x0ece committed Jun 4, 2024
1 parent 1890e20 commit 35c5886
Showing 1 changed file with 265 additions and 0 deletions.
265 changes: 265 additions & 0 deletions proposals/0152-precompiles.md
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

View workflow job for this annotation

GitHub Actions / Markdown Linter

Fenced code blocks should be surrounded by blank lines [Context: "```"]

proposals/0152-precompiles.md:63 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```"]

Check failure on line 63 in proposals/0152-precompiles.md

View workflow job for this annotation

GitHub Actions / Markdown Linter

Fenced code blocks should be surrounded by blank lines [Context: "```"]

proposals/0152-precompiles.md:63 MD031/blanks-around-fences Fenced 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

View workflow job for this annotation

GitHub Actions / Markdown Linter

Fenced code blocks should be surrounded by blank lines [Context: "```"]

proposals/0152-precompiles.md:68 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```"]

Check failure on line 68 in proposals/0152-precompiles.md

View workflow job for this annotation

GitHub Actions / Markdown Linter

Fenced code blocks should be surrounded by blank lines [Context: "```"]

proposals/0152-precompiles.md:68 MD031/blanks-around-fences Fenced 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

View workflow job for this annotation

GitHub Actions / Markdown Linter

Lists should be surrounded by blank lines [Context: "1. If instruction `data` is em..."]

proposals/0152-precompiles.md:90 MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "1. If instruction `data` is em..."]

Check failure on line 90 in proposals/0152-precompiles.md

View workflow job for this annotation

GitHub Actions / Markdown Linter

Lists should be surrounded by blank lines [Context: "1. If instruction `data` is em..."]

proposals/0152-precompiles.md:90 MD032/blanks-around-lists Lists 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

View workflow job for this annotation

GitHub Actions / Markdown Linter

Lists should be surrounded by blank lines [Context: "1. Get the `instruction_index`..."]

proposals/0152-precompiles.md:102 MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "1. Get the `instruction_index`..."]

Check failure on line 102 in proposals/0152-precompiles.md

View workflow job for this annotation

GitHub Actions / Markdown Linter

Lists should be surrounded by blank lines [Context: "1. Get the `instruction_index`..."]

proposals/0152-precompiles.md:102 MD032/blanks-around-lists Lists 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

View workflow job for this annotation

GitHub Actions / Markdown Linter

Fenced code blocks should be surrounded by blank lines [Context: "```"]

proposals/0152-precompiles.md:113 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```"]

Check failure on line 113 in proposals/0152-precompiles.md

View workflow job for this annotation

GitHub Actions / Markdown Linter

Fenced code blocks should be surrounded by blank lines [Context: "```"]

proposals/0152-precompiles.md:113 MD031/blanks-around-fences Fenced 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.

0 comments on commit 35c5886

Please sign in to comment.