Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ OOB_OSAKA := oob/osaka

RLP_ADDR := rlpaddr

RLP_AUTH := rlpauth/rlpauth.zkasm

RLP_TXN_LONDON := rlptxn/london
RLP_TXN_CANCUN := rlptxn/cancun
RLP_TXN_PRAGUE := rlptxn/cancun
Expand Down Expand Up @@ -196,6 +198,7 @@ ZKEVM_MODULES_PRAGUE := ${ZKEVM_MODULES_COMMON} \
${MMU_LONDON} \
${MXP_CANCUN} \
${OOB_PRAGUE} \
${RLP_AUTH} \
${RLP_TXN_PRAGUE} \
${RLP_UTILS_CANCUN} \
${TRM_LONDON} \
Expand All @@ -214,6 +217,7 @@ ZKEVM_MODULES_OSAKA := ${ZKEVM_MODULES_COMMON} \
${MMU_OSAKA} \
${MXP_CANCUN} \
${OOB_OSAKA} \
${RLP_AUTH} \
${RLP_TXN_PRAGUE} \
${RLP_UTILS_CANCUN} \
${TRM_OSAKA} \
Expand Down
90 changes: 90 additions & 0 deletions rlpauth/rlpauth.zkasm
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
include "../constants/evm.zkasm"

;; https://eips.ethereum.org/EIPS/eip-7702

;; TODO: do we have an shared notation for constants, function arguments...?
;; TODO: here we can directly have secp256k1n_divided_by_two
const SECP256K1N = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Wrong constant used: field prime instead of curve order

The constant SECP256K1N is set to 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f which is the secp256k1 field prime (p), not the curve order (n). The correct curve order for signature malleability checks is 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141. Using the field prime instead of the curve order makes the s-malleability check ineffective as it allows larger S values than permitted by EIP-7702.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Wrong cryptographic constant used for signature validation

The SECP256K1N constant is set to the secp256k1 field prime p (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F) instead of the curve order n (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141). For EIP-7702 signature validation, the check S <= secp256k1n/2 must use the curve order n. Since p > n, using p makes the check too permissive and would incorrectly accept signatures with invalid S values between n/2 and p/2.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Wrong secp256k1 constant uses field prime instead of curve order

The SECP256K1N constant is set to the secp256k1 field prime p (0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f = 2^256 - 2^32 - 977) instead of the curve order n (0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141). This is a critical cryptographic error because the s-value malleability check in ECDSA signatures requires comparison against the curve order n, not the field prime p. Using the wrong value would allow invalid signatures to pass validation.

Fix in Cursor Fix in Web

const MAGIC = 0x05

fn rlpauth(chain_id u256, nonce u64, address u160, y_parity u8, r u256, s u256) -> (authority u160, error u1)
{
;; The following checks are enforced by the types above:
;; assert auth.chain_id < 2**256
;; assert auth.nonce < 2**64
;; assert len(auth.address) == 20
;; assert auth.y_parity < 2**8
;; assert auth.r < 2**256
;; assert auth.s < 2**256
step_1:
if chain_id == 0 goto step_3
if chain_id == 1 goto step_3 ;; TODO: this should check current chain_id instead
fail
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Hardcoded chain ID validation enables cross-chain replay

The chain ID validation hardcodes acceptance of CHAIN_ID == 1 (mainnet) regardless of which chain the code actually runs on. Per EIP-7702, authorizations with a non-zero chain_id must only be valid when chain_id matches the current execution chain. If this code runs on a non-mainnet chain, it would incorrectly accept mainnet authorizations (enabling cross-chain replay attacks) while rejecting valid authorizations for the actual chain. The function needs the current chain ID as a parameter to properly validate.

Fix in Cursor Fix in Web

;; step_2 is implicitly succeseful due to nonce being declared as u64
step_3:
;; Divide SECP256K1N by 2
var secp256k1n_divided_by_two u256
var b u1
secp256k1n_divided_by_two, b = SECP256K1N
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing division operation for SECP256K1N comparison

The comment says "Divide SECP256K1N by 2" but the code secp256k1n_divided_by_two, b = SECP256K1N just assigns the full constant to the variable without any division. The variable name implies it should contain half the value, but it actually contains the full SECP256K1N constant. This causes the s-malleability check to use the wrong threshold.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing division operation for secp256k1n half calculation

The comment states "Divide SECP256K1N by 2" but the line secp256k1n_divided_by_two, b = SECP256K1N simply assigns the full constant without performing any division or right shift. The variable secp256k1n_divided_by_two will contain the full value, not half of it, causing the subsequent S value check to use an incorrect threshold.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Division by two not actually performed

The comment states "Divide SECP256K1N by 2" but the code secp256k1n_divided_by_two, b = SECP256K1N does not perform division. In zkASM, this syntax performs type decomposition, not arithmetic. To properly compute N/2, a right-shift operation (like bit_shr256) would be needed. The variable secp256k1n_divided_by_two receives an incorrect value, causing the subsequent s <= N/2 malleability check to use the wrong threshold.

Fix in Cursor Fix in Web

;; Check that s <= SECP256K1N / 2
var tmp u256
b, tmp = s - secp256k1n_divided_by_two
if b == 1 goto failure
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Inverted logic for S malleability check comparison

The check if b == 1 goto failure has inverted logic. When computing S - secp256k1n_divided_by_two, a borrow (b=1) indicates S < threshold, which per EIP-7702 should be the valid case (S must be at most N/2). The code incorrectly treats the valid case as failure and allows invalid signatures where S exceeds the threshold.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Inverted borrow check logic for S validation

The condition if b == 1 goto failure fails when the subtraction S - secp256k1n_divided_by_two causes a borrow (underflow), which happens when S < secp256k1n/2. However, the EIP-7702 requirement is that S <= secp256k1n/2, meaning the code should fail when S > secp256k1n/2 (no borrow). The logic is inverted and will reject valid signatures while accepting invalid ones.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: S-value comparison logic is inverted

The s-value malleability check logic is inverted. The code b, tmp = s - secp256k1n_divided_by_two followed by if b == 1 goto failure rejects values where s < N/2 (borrow occurs) and accepts values where s >= N/2 (no borrow). This is backwards - EIP-7702 requires rejecting signatures where s > N/2 to prevent signature malleability. The condition should fail when b == 0 (no borrow, meaning s >= N/2) instead of when b == 1.

Fix in Cursor Fix in Web

;;
var rlp_res u256
rlp_res = compute_rlp(chain_id, address, nonce)
;;
var keccak_input u264
keccak_input = compute_concat_magic_and_rlp(MAGIC, rlp_res)
;;
var msg u256
msg = keccak(keccak_input)
authority = ecrecover(msg, y_parity, r, s)
step_4:
;; Add authority to accessed_addresses, as defined in EIP-2929. Does it have any effect here?
step_5:
;; if authority is emtpy goto step_6
;; if authority is already delegated goto step_6
;; fail
step_6:
;; if authority.nonce == nonce goto step_7
;; fail
step_7:
;; Add PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST gas to the global refund counter if authority is not empty. Does it have any effect here?
step_8:
;; if address == 0x0000000000000000000000000000000000000000 goto "authority.code_hash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
;; authority.code = 0xef0100 || address
step_9:
;; authority.nonce = authority.nonce + 1
exit:
;; dummy values
authority = 0
error = 0
return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Computed authority overwritten before returning

Control flow falls through to exit: unconditionally, where authority is reassigned to 0 just before return. This makes rlpauth always return a zero authority (and error = 0) even when earlier steps computed a nonzero result, breaking RLPAUTH behavior.

Fix in Cursor Fix in Web

failure:
fail
}

;; temporary dummy functions
;; TODO: determine the exact size of RLP
fn compute_rlp(chain_id u256, address u160, nonce u64) -> (rlp_res u256) {
rlp_res = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
return
}

fn compute_concat_magic_and_rlp(magic u8, rlp u256) -> (concat_res u264) {
;; TODO: how to concat?
;; concat_res = magic, rlp?
concat_res = 0x05fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
return
}

fn keccak(keccak_input u264) -> (keccak_output u256) {
keccak_output = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
return
}

fn ecrecover(msg u256, y_parity u8, r u256, s u256) -> (ecrecover_result u160) {
ecrecover_result = 0xfffffffffffffffffffffffffffffffefffffc2f
return
}
Loading