Precompiles
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
We first propose a specification for precompiles. Then we propose changes to the existing precompiles with respect to the specification.
Precompile Specification
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. From a cost perspective, they're included in the transaction fee (each signature to verify counts as a transaction signature), but don't require any compute units.
If a transaction contains more than 8 precompile signatures, it must fail.
MAX_ALLOWED_PRECOMPILE_SIGNATURES = 8
The precompile instruction verify
accepts the following data:
struct PrecompileVerifyInstruction {
num_signatures: u8, // Number of signatures to verify
padding: u8, // Single byte padding
offsets: PrecompileOffsets[], // Array of `num_signatures` offsets
additionalData?: Bytes, // Optional additional data, e.g.
// signatures included in the same
// instruction
}
struct PrecompileOffsets {
signature_offset: u16 LE, // Offset to signature (offset within
// the specified instruction data)
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:
- If instruction
data
is empty, return error. - The first byte of
data
is the number of signaturesnum_signatures
. - If
num_signatures
is 0, return error. - Expect (enough bytes of
data
for)num_signatures
instances ofPrecompileOffsets
. - For each signature:
a. Read
offsets
: an instance ofPrecompileOffsets
b. Based on theoffsets
, retrievesignature
,public_key
, andmessage
bytes. If any of the three fails, return error. c. Invoke the actualsigverify
function. If it fails, return error.
To retrieve signature
, public_key
, and message
:
- Get the
instruction_index
-thinstruction_data
- The special value
0xFFFF
means "current instruction" - If the index is invalid, return Error
- The special value
- Return
length
bytes starting fromoffset
- If this exceeds the
instruction_data
length, return Error
- If this exceeds the
Note that fields (offsets) can overlap, for example the same public key or
message can be referred to by multiple instances of PrecompileOffsets
.
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:
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 ofstrict_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 bothverify
andstrict_verify
. Only carefully crafted signatures can passverify
and notstrict_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.