This EIP does three things:
0x12
for decoding these newly introduced algorithms.As quantum computers become more advanced, several new post-quantum (PQ) algorithms have been designed. These algorithms all have certain issues, such as large key sizes (>1KiB), large signature sizes, or long verification times. These issues make them more expensive to compute and store than the currently used secp256k1 curve.
This EIP provides a solution to the diversity in algorithms by adding a standardized way to represent alternative algorithms within a transaction.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Unless explicitly noted, integer encoding MUST be in big-endian format.
Constant | Value |
---|---|
MAX_ADDITIONAL_INFO |
255 |
GAS_PER_ADDITIONAL_VERIFICATION_BYTE |
16 |
SIGRECOVER_PRECOMPILE_ADDRESS |
Bytes20(0x12) |
SIGRECOVER_PRECOMPILE_BASE_GAS |
3000 |
SECP256K1_SIGNATURE_SIZE
is defined from EIP-6404.
ExecutionAddress
is defined from EIP-6466.
TransactionPayload
is defined as any type that derives its type from Profile[TransactionPayload]
.
This EIP introduces a new EIP-6404 transaction profile of SSZ type AlgorithmicTransaction
defined below:
class AdditionalSignature(ProgressiveContainer(active_fields=[1,1,1])):
# The selector for which algorithm is to be used.
algorithm_type: uint8,
# The signature of the algorithm specified by `algorithm_type`
signature_info: ProgressiveList[byte],
# The index at which the `AdditionalSignature` is present in the `additional_signatures` list
index: uint8,
class AlgorithmicTransaction(ProgressiveContainer(active_fields=[1,1,1,1])):
# The selector for which algorithm is to be used.
algorithm_type: uint8,
# The signature of the algorithm specified by `algorithm_type`
signature_info: ProgressiveList[byte]
# The payload of the inner transaction
payload: TransactionPayload,
# The list of additional transaction-level signatures
additional_signatures: List[AdditionalSignature, MAX_ADDITIONAL_INFO]
The field algorithm_type
represents the algorithm used to sign the transaction in the payload
field.
This EIP does not define algorithms for use with this transaction type apart from secp256k1
which may only be used under specific circumstances.
The hash of the payload MUST be calculated using the compute_ssz_sig_hash
function below:
def compute_ssz_sig_hash(payload: TransactionPayload) -> Hash32:
return Hash32(ExecutionSigningData(
object_root=payload.hash_tree_root(),
domain=DOMAIN_TX_SSZ,
).hash_tree_root())
The transaction signature MUST be verified using the verify
function for the algorithm denoted via algorithm_type
.
The additional_signatures
field only needs to be populated if additional protocol level signatures are required such as EIP-7702's authorization_list
.
If a signature is being overridden via an additional_signatures
entry, the Secp256k1ExecutionSignature
MUST be set to hash_tree_root(AdditionalSignature)
and right-padded with 33 zero bytes. This MUST be verified during transaction validation and each entry MUST NOT be of type secp256k1
.
The Algorithmic Transaction MUST generate an EIP-6466 receipt of only the payload
, not of the AlgorithmicTransaction
. Implementations MUST NOT be able to differentiate between an unwrapped and wrapped transaction by receipts alone.
When clients receive an Algorithmic Transaction via gossip or RPC they MUST validate both the Algorithmic Transaction and the object in the payload
field, if either the transaction or the payload is invalid, the entire transaction is invalid.
Any discrimination of transactions MUST occur only with the payload
, e.g. gas prices for block ordering.
identify_transaction_profile
modificationsAn additional section is added to the start of the identify_transaction_profile
helper from EIP-6404:
def identify_transaction_profile(tx: Transaction) -> Type[Profile]:
if tx.algorithm_type is not None:
return AlgorithmicTransaction
...
Further algorithms MUST be specified via a distinct EIP.
Each type of algorithm MUST specify the following fields:
| Field Name | Description |
|-|-|
|ALG_TYPE
| The uint8 of the algorithm unique ID |
|MAX_SIZE
| The maximum size of signature_info
field in a transaction |
|GAS_PENALTY
| The additional gas penalty from verification of the signature |
The GAS_PENALTY
field MUST be assumed to be worst-case scenario and MUST only account for verification costs, not storage nor signing GAS_PENALTY
.
New algorithms MUST also specify how to recover and verify a valid address (ExecutionAddress
) from the signature_info
field inside the transaction, the verification function MUST have the following signature:
def verify(signature_info: bytes, payload_hash: Hash32) -> ExecutionAddress
The verify function MUST return either an error or return the address of the signer.
Specifications MUST include some form of security analysis on the provided algorithm and basic benchmarks justifying gas costs.
An example of this specification can be found here.
This EIP uses the Algorithms
object to signify algorithms that have been included within a hard fork.
A living EIP MAY be created on finalization of this EIP to track currently active algorithms across forks, this living EIP MUST contain a formal definition of the Algorithms
object.
Implementations MUST check that transactions pass the following checks:
signature_info
is less than algorithm's defined MAX_SIZE
, including additional_signatures
entries.algorithm_type
field points to a known algorithm.If any checks fail, then the transaction containing them is invalid. If called from the precompile, the precompile MUST return bytes20(0x0)
.
Transactions MUST also follow all rules outlined in the Transaction specification section.
All transactions that use more resources than the secp256k1 curve suffer an additional penalty which MUST be calculated
as specified in the calculate_penalty
function.
def calculate_penalty(signature_info: bytes, algorithm: int) -> int:
gas_penalty_base = max(len(signature_info) - SECP256K1_SIGNATURE_SIZE, 0) * GAS_PER_ADDITIONAL_VERIFICATION_BYTE
total_gas_penalty = gas_penalty_base + ALGORITHMS[algorithm].GAS_PENALTY
return total_gas_penalty
The penalty MUST be added onto the 21000
base gas of each transaction before the transaction is processed. A penalty MUST also be added for every instance of an additional_signatures
object. This MUST be done irrespective of whether the signature is used or not. If the payload's gas_limit
is less than 21000 + penalty
then the transaction MUST be invalidated.
This transaction also MUST inherit the intrinsics of the wrapped transaction's fee structure (e.g. a wrapped EIP-1559 payload would behave as an EIP-1559 transaction).
secp256k1
algorithmField | Value |
---|---|
ALG_TYPE |
0xFF |
MAX_SIZE |
SECP256K1_SIGNATURE_SIZE |
GAS_PENALTY |
0 |
secp256k1_validate
, SECP256K1_SIGNATURE_SIZE
and secp256k1_recover_signer
are defined from EIP-6404.
def verify(signature_info: bytes, payload_hash: Hash32) -> ExecutionAddress
assert len(signature_info) == SECP256K1_SIGNATURE_SIZE
secp256k1_validate(signature_info)
return secp256k1_recover_signer(signature_info, payload_hash)
This algorithm MUST ONLY be used if the transaction's signature is secp256k1 but the additional_signatures
field contains more than (or equal to) one entry. It MUST NOT be present if the transaction does not contain additional signatures. secp256k1
MUST NOT be present in any additional_signatures
entry.
sigrecover
precompileThis EIP also introduces a new precompile located at SIGRECOVER_PRECOMPILE_ADDRESS
.
This precompile MUST cost SIGRECOVER_PRECOMPILE_BASE_GAS
when called regardless of validity, this cost MUST be aggregated with calculate_penalty(signature_info, algorithm)
, this penalty is charged before the sigrecover
precompile executes.
The precompile MUST output the 20-byte address of the signer provided. Callers MUST assume all zero bytes as a failure.
The precompile logic contains the following logic:
def sigrecover_precompile(input: Bytes) -> Bytes:
# Recover signature length and type
assert(len(input) >= 64)
hash: Hash32 = input[:32]
algorithm_type: uint8 = input[32]
sig_length: uint248 = int.from_bytes(input[33:64], "big")
# Ensure the algorithm exists and signature is correct size
if algorithm_type not in Algorithms:
return ExecutionAddress(0x0)
alg = Algorithms[algorithm_type]
# Sig length must be smaller than alg.MAX_SIZE and
# equal to the remaining call data
if sig_length > alg.MAX_SIZE or sig_length != (len(input) - 64):
return ExecutionAddress(0x0)
# Run verify function
try:
alg.verify(input[64:64 + sig_length], hash)
except:
return ExecutionAddress(0x0)
signature_info
typeAs each algorithm has unique properties, e.g. signature recovery and key sizes, the object needs to hold every permutation of every possible signature and additional recovery information. A bytearray of a dynamic size would be able to achieve this goal. However, this leads to a DoS vector which the Gas penalties section solves along with the MAX_SIZE
parameter.
Having multiple different algorithms results in multiple different signature sizes and verification costs. Hence, every signature algorithm that is more expensive than the default ECDSA secp256k1 curve incurs an additional gas penalty. This is to discourage the use of overly expensive algorithms for no specific reason.
The GAS_PER_ADDITIONAL_VERIFICATION_BYTE
value being 16
was taken from the calldata cost of a transaction, as it is a similar datatype and must persist indefinitely to ensure later verification.
Allowing a single account to share multiple keys creates a security risk as it reduces the security of all addresses to the weakest algorithm. This is also out of scope for this EIP and could be implemented via a future EIP.
While adding a new address format for every new algorithm would ensure that collisions never happen and that security would not be bound by the lowest common denominator, it creates an excessively large burden on client and tooling developers.
ecrecover
precompileInitially, modifying the ecrecover
precompile seemed prudent over creating a new precompile. However, after an initial attempt, it proved too hacky for production use.
This EIP allows for EIP‑4337 Bundler
s to settle UserOperation
s on-chain using a different algorithm. sigrecover also complements Account Abstraction well by reducing on-chain verification costs via sigrecover
.
Non-EIP-7932 transactions will still be processed as the default secp256k1 curve. Therefore, there would be no backwards compatibility issues in processing other transactions.
Allowing more ways to potentially sign transactions for a single account may decrease overall security for that specific account, however this is partially mitigated by the increase in processing power required to trial all algorithms. Even still, adding additional algorithms may need further discussion to ensure that the security of the network would not be compromised.
Having signature_info
be of no concrete type creates a chance that an algorithm's decoding logic could be specified or implemented incorrectly, which could lead to, in the best case, invalid blocks or, at worst, enabling anyone to sign a fraudulent transaction for any account. This security consideration is delegated to the algorithm's specification, and so care must be taken when writing these specifications to avoid critical security flaws.
Copyright and related rights waived via CC0.