Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Feat/#318 precompile ecrecover #495

Merged
merged 15 commits into from
Oct 27, 2023
Merged
33 changes: 33 additions & 0 deletions specs/precompile/01ecRecover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ecRecover precompile

## Procedure

To recover the signer from a signature. It returns signer's address if input signature is valid, otherwise returns 0.

## EVM behavior

### Inputs

The length of inputs is 128 bytes. The first 32 bytes is keccak hash of the message, and the following 96 bytes are v, r, s values. The value v is either 27 or 28.

### Output

The recovered 20-byte address right aligned to 32 byte. If an address can't be recovered or not enough gas was given, then the output is 0.

### Gas cost

A constant gas cost: 3000

## Constraints

1. If gas_left < gas_required, then is_success == false and return data is zero.
1. v, r and s are valid
- v is 27 or 28
- both of r and s are less than `secp256k1N (0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141)`
- both of r and s are greater than `1`
2. `sig_table` lookups
3. recovered address is zero if the signature can't be recovered.

## Code

Please refer to `src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py`.
51 changes: 51 additions & 0 deletions specs/sig-proof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Signature Proof

[Elliptic Curve Digital Signature Algorithm]: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm

According to the [Elliptic Curve Digital Signature Algorithm] (ECDSA), the signatures `(r,s)` are calculated via ECDSA from `msg_hash` and a `public_key` using the formula

`(r,s)=ecdsa(msg_hash, public_key)`

The `public_key` is obtained from `private_key` by mapping the latter to an elliptic curve (EC) point. The `r` is the x-component of an EC point, and the same EC point's y-component will be used to determine the recovery id `v = y%2` (the parity of y). Given the signature `(v, r, s)`, the `public_key` can be recovered from `(v, r, s)` and `msg_hash` using `ecrecover`.


## Circuit behavior

SigTable built inside zkevm-circuits is used to verify signatures. It has the following columns:
- `msg_hash`: Advice Column, the Keccak256 hash of the message that's signed;
- `sig_v`: Advice Column, the recovery id, either 0 or 1, it should be the parity of y;
- `sig_r`: Advice Column, the signature's `r` component;
- `sig_s`: Advice Column, the signature's `s` component;
- `recovered_addr`: Advice Column, the recovered address, i.e. the 20-bytes address that must have signed the message;
- `is_valid`: Advice Column, indicates whether or not the signature is valid or not upon signature verification.

Constraints on the shape of the table is like:

| 0 msg_hash | 1 sig_v | 2 sig_r | 3 sig_s | 4 recovered_addr | 5 is_valid |
| ------------- | ------ | ------------- | ------------- | ---------------- | ---------- |
| $value{Lo,Hi} | 0/1 | $value{Lo,Hi} | $value{Lo,Hi} | $value{Lo,Hi} | bool |


The Sig Circuit aims at proving the correctness of SigTable. This mainly includes the following type of constraints:
- Checking that the signature is obtained correctly. This is done by the ECDSA chip, and the correctness of `v` is checked separately;
- Checking that `msg_hash` is obtained correctly from Keccak hash function. This is done by lookup to Keccak table;


## Constraints

`assign_ecdsa` method takes the signature data and uses ECDSA chip to verify its correctness. The verification result `sig_is_valid` will be returned. The recovery id `v` value will be computed and verified.

`sign_data_decomposition` method takes the signature data and the return values of `assign_ecdsa`, and returns the cells for byte decomposition of the keys and messages in the form of `SignDataDecomposed`. The latter consists of the following contents:
- `SignDataDecomposed`
- `pk_hash_cells`: byte cells for keccak256 hash of public key;
- `msg_hash_cells`: byte cells for `msg_hash`;
- `pk_cells`: byte cells for the EC coordinates of public key;
- `address`: RLC of `pk_hash` last 20 bytes;
- `is_address_zero`: check if address is zero;
- `r_cells`, `s_cells`: byte cells for signatures `r` and `s`.

The decomposed sign data are sent to `assign_sign_verify` method to compute and verify their RLC values and perform Keccak lookup checks.

## Code

Please refer to `src/zkevm-specs/sig_circuit.py`
14 changes: 14 additions & 0 deletions specs/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,17 @@ Row(is_step=1, identifier=rwc, is_last=0, base_limbs=[3, 0, 0, 0], exponent_lo_h
```
Row(is_step=1, identifier=rwc, is_last=1, base_limbs=[3, 0, 0, 0], exponent_lo_hi=[2, 0], exponentiation_lo_hi=[9, 0])
```


## `sig_table`

Provided by the Signature circuit.

The circuit verifies the correctness of signatures.

| 0 msg_hash | 1 sig_v | 2 sig_r | 3 sig_s | 4 recovered_addr | 5 is_valid |
| ------------- | ------ | ------------- | ------------- | ---------------- | ---------- |
| $value{Lo,Hi} | 0/1 | $value{Lo,Hi} | $value{Lo,Hi} | $value{Lo,Hi} | bool |

NOTE:
- `sig_v` is either 0 or 1 so boolean type is used here.
1 change: 1 addition & 0 deletions src/zkevm_specs/evm_circuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .table import *
from .typing import *
from .util import *
from .precompile import *
3 changes: 2 additions & 1 deletion src/zkevm_specs/evm_circuit/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
from .error_oog_static_memory_expansion import *
from .error_oog_sload_sstore import *
from .error_oog_create import *
from .precompiles.ecrecover import *


EXECUTION_STATE_IMPL: Dict[ExecutionState, Callable] = {
Expand Down Expand Up @@ -152,7 +153,7 @@
ExecutionState.ErrorOutOfGasSloadSstore: error_oog_sload_sstore,
ExecutionState.ErrorReturnDataOutOfBound: error_return_data_out_of_bound,
ExecutionState.ErrorOutOfGasCREATE: error_oog_create,
# ExecutionState.ECRECOVER: ,
ExecutionState.ECRECOVER: ecRecover,
# ExecutionState.SHA256: ,
# ExecutionState.RIPEMD160: ,
ExecutionState.DATACOPY: dataCopy,
Expand Down
65 changes: 65 additions & 0 deletions src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from zkevm_specs.evm_circuit.instruction import Instruction
from zkevm_specs.evm_circuit.table import (
CallContextFieldTag,
FixedTableTag,
RW,
)
from zkevm_specs.util import FQ, Word, EcrecoverGas

SECP256K1N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141


def ecRecover(instruction: Instruction):
is_success = instruction.call_context_lookup(CallContextFieldTag.IsSuccess, RW.Read)
address_word = instruction.call_context_lookup_word(CallContextFieldTag.CalleeAddress)
address = instruction.word_to_address(address_word)
instruction.fixed_lookup(
FixedTableTag.PrecompileInfo,
FQ(instruction.curr.execution_state),
address,
FQ(EcrecoverGas),
)

# Get msg_hash, signature and recovered address from aux_data
msg_hash: Word = instruction.curr.aux_data[0]
sig_v: Word = instruction.curr.aux_data[1]
sig_r: Word = instruction.curr.aux_data[2]
sig_s: Word = instruction.curr.aux_data[3]
recovered_addr: FQ = instruction.curr.aux_data[4]

is_recovered = FQ(instruction.is_zero(recovered_addr) != FQ(1))

# is_success is always true
# ref: https://github.com/ethereum/execution-specs/blob/master/src/ethereum/shanghai/vm/precompiled_contracts/ecrecover.py
instruction.constrain_equal(is_success, FQ(1))

# verify r and s
sig_r_upper_bound, _ = instruction.compare_word(sig_r, Word(SECP256K1N))
sig_s_upper_bound, _ = instruction.compare_word(sig_s, Word(SECP256K1N))
sig_r_is_non_zero = FQ(instruction.is_zero_word(sig_r) != FQ(1))
sig_s_is_non_zero = FQ(instruction.is_zero_word(sig_s) != FQ(1))
valid_r_s = instruction.is_equal(
sig_r_upper_bound + sig_s_upper_bound + sig_r_is_non_zero + sig_s_is_non_zero, FQ(4)
)

# verify v
is_equal_27 = instruction.is_equal_word(sig_v, Word(27))
is_equal_28 = instruction.is_equal_word(sig_v, Word(28))
valid_v = instruction.is_equal(is_equal_27 + is_equal_28, FQ(1))

if valid_r_s + valid_v == FQ(2):
# sig table lookups
instruction.sig_lookup(
msg_hash, sig_v.lo.expr() - FQ(27), sig_r, sig_s, recovered_addr, is_recovered
)
else:
instruction.constrain_zero(is_recovered)
instruction.constrain_zero(recovered_addr)

# Restore caller state to next StepState
instruction.step_state_transition_to_restored_context(
rw_counter_delta=instruction.rw_counter_offset,
return_data_offset=FQ.zero(),
return_data_length=FQ(32) if is_recovered == FQ(1) else FQ.zero(),
gas_left=instruction.curr.gas_left - EcrecoverGas,
)
11 changes: 11 additions & 0 deletions src/zkevm_specs/evm_circuit/instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,17 @@ def exp_lookup(
exp_table_row = self.tables.exp_lookup(identifier, is_last, base_limbs, exponent)
return exp_table_row.exponentiation

def sig_lookup(
self,
msg_hash: Word,
sig_v: Expression,
sig_r: Word,
sig_s: Word,
recovered_addr: FQ,
is_valid: Expression,
) -> Word:
return self.tables.sig_lookup(msg_hash, sig_v, sig_r, sig_s, recovered_addr, is_valid)

def constrain_error_state(self, rw_counter_delta: int):
# Current call must fail.
rw_counter_delta += 1
Expand Down
2 changes: 1 addition & 1 deletion src/zkevm_specs/evm_circuit/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class StepState:
# not often used.
log_id: FQ

# Auxilary witness data needed by gadgets
# Auxiliary witness data needed by gadgets
aux_data: Any

def __init__(
Expand Down
33 changes: 33 additions & 0 deletions src/zkevm_specs/evm_circuit/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,16 @@ class ExpTableRow(TableRow):
exponentiation: Word


@dataclass(frozen=True)
class SigTableRow(TableRow):
msg_hash: Word
sig_v: FQ
sig_r: Word
sig_s: Word
recovered_addr: FQ
is_valid: FQ


class Tables:
"""
A collection of lookup tables used in EVM circuit.
Expand All @@ -552,6 +562,7 @@ class Tables:
copy_table: Set[CopyTableRow]
keccak_table: Set[KeccakTableRow]
exp_table: Set[ExpTableRow]
sig_table: Set[SigTableRow]

def __init__(
self,
Expand All @@ -563,6 +574,7 @@ def __init__(
copy_circuit: Optional[Sequence[CopyCircuitRow]] = None,
keccak_table: Optional[Sequence[KeccakTableRow]] = None,
exp_circuit: Optional[Sequence[ExpCircuitRow]] = None,
sig_table: Optional[Sequence[SigTableRow]] = None,
) -> None:
self.block_table = block_table
self.tx_table = tx_table
Expand All @@ -578,6 +590,8 @@ def __init__(
self.keccak_table = set(keccak_table)
if exp_circuit is not None:
self.exp_table = self._convert_exp_circuit_to_table(exp_circuit)
if sig_table is not None:
self.sig_table = set(sig_table)

def _convert_copy_circuit_to_table(self, copy_circuit: Sequence[CopyCircuitRow]):
rows: List[CopyTableRow] = []
Expand Down Expand Up @@ -768,6 +782,25 @@ def exp_lookup(
}
return lookup(ExpTableRow, self.exp_table, query)

def sig_lookup(
self,
msg_hash: Word,
sig_v: Expression,
sig_r: Word,
sig_s: Word,
recovered_addr: FQ,
is_valid: Expression,
) -> SigTableRow:
query = {
"msg_hash": msg_hash,
"sig_v": sig_v,
"sig_r": sig_r,
"sig_s": sig_s,
"recovered_addr": recovered_addr,
"is_valid": is_valid,
}
return lookup(SigTableRow, self.sig_table, query)


T = TypeVar("T", bound=TableRow)

Expand Down
Loading
Loading