EIP-8250 프레임 트랜잭션의 키드 논스 (Keyed Nonces for Frame Transactions)
Hegota 포함 검토 (CFI) · 계정 추상화 · 원문 상태 Draft · 2026-04-16 작성
프레임 트랜잭션의 논스 하나를 논스 키 목록과 순번 하나로 바꿉니다. 키가 겹치지 않는 트랜잭션은 서로 순서를 기다리지 않아 한 발신 주소를 여러 사용자가 나눠 쓰는 프라이버시 앱이 막히지 않습니다.
왜 필요한가
지금 프레임 트랜잭션은 발신자마다 하나의 순차 논스(nonce, 같은 트랜잭션이 두 번 실행되지 않게 붙이는 일련번호)를 씁니다. 번호가 하나로 이어지므로 앞 트랜잭션이 늦어지면 같은 발신자의 뒤 트랜잭션은 모두 기다려야 합니다. 발신 주소 하나를 여러 사용자가 일부러 나눠 쓰는 설계에는 이 제약이 너무 큽니다. 대표 사례가 프라이버시 프로토콜입니다. 온체인 활동이 특정 공개 주소에 묶이지 않도록 사용자들은 하나의 공용 발신 주소로 트랜잭션을 보냅니다. 논스가 하나뿐이면 이 주소가 병목이 됩니다. 한 사용자의 트랜잭션이 블록에 들어가는 순간, 서로 관련 없는 다른 사용자의 대기 중 출금이 전부 무효가 됩니다. 스마트 지갑의 세션 키나 릴레이어 방식 발신자도 같은 문제를 겪습니다. 또 널리파이어처럼 한 번만 써야 하는 키는 썼다는 표시를 원자적으로 남겨야 하는데, 지금 구조에서는 뒤 프레임이 실패하면 이 보장이 깨질 수 있습니다.
무엇이 바뀌나
지금
- 발신자마다 순차 논스 하나뿐이라 앞 트랜잭션이 늦어지면 같은 발신자의 뒤 트랜잭션이 모두 막힙니다.
- 공용 발신 주소를 쓰는 프라이버시 앱에서는 한 사용자의 트랜잭션이 포함되면 다른 사용자의 대기 트랜잭션이 무효가 됩니다.
- 한 번만 쓰는 키를 썼다고 표시하는 일은 컨트랙트가 맡아야 하고 뒤 프레임이 실패하면 표시가 사라질 수 있습니다.
EIP-8250 이후
- 트랜잭션마다 논스 키를 최대 16개 고르고 키가 겹치지 않는 트랜잭션은 서로 순서를 기다리지 않습니다.
- 키 목록을 비우면 지금처럼 계정 논스를 그대로 씁니다. 기존 동작은 바뀌지 않습니다.
- 순번을 최댓값으로 적으면 선택한 키는 한 번만 쓰이고 영구히 소진됩니다. 이 처리는 결제 승인에 묶여 뒤 프레임이 실패해도 유지됩니다.
- 공개 멤풀의 발신자당 대기 트랜잭션 하나 제한은 그대로입니다. 프로토콜 수준의 장애물만 치우고 멤풀 정책은 뒤로 미룹니다.
누구에게 영향이 있나
- 프라이버시 앱 사용자: 공용 발신 주소를 써도 다른 사용자의 출금 때문에 내 트랜잭션이 무효가 되지 않습니다.
- 지갑·앱 개발자: 세션 키, 릴레이어, 널리파이어 기반 설계에서 키마다 독립 논스를 쓸 수 있습니다. 다만 TXPARAM(0x01)을 계정 논스로 가정한 검증 코드는 고쳐야 합니다.
- 클라이언트 팀: 새 페이로드 해석, NONCE_MANAGER 저장소 읽기·쓰기, 블록 접근 목록 기록, 멤풀 재검증 규칙을 구현해야 합니다.
- 노드 운영자·멤풀: 대기 트랜잭션의 식별자가 (sender, nonce_keys, nonce_seq)로 바뀌고 선택한 키의 순번이 바뀌면 재검증해야 합니다.
심층 설명
페이로드: nonce 하나가 nonce_keys와 nonce_seq로
EIP-8141 FRAME_TX_TYPE 페이로드의 nonce 필드를 nonce_keys, nonce_seq 두 필드로 바꿉니다. 결과는 [chain_id, nonce_keys, nonce_seq, sender, frames, signatures, fees, blob_versioned_hashes]이고 다른 필드는 그대로입니다. nonce_keys는 uint256 목록, nonce_seq는 uint64이며 둘 다 최소 길이 RLP 정수로 인코딩해야 합니다. 서명 해시는 nonce 자리에 두 필드를 넣은 뒤 EIP-8141 절차를 그대로 적용합니다. 디코더는 다음 경우 트랜잭션을 거부합니다. 스키마 불일치, nonce_keys가 RLP 목록이 아님, len(nonce_keys) > MAX_NONCE_KEYS(16), 정규 RLP 정수가 아닌 항목, 2**256 이상인 키, nonce_seq > MAX_NONCE_SEQ, nonce_keys == []인데 nonce_seq == MAX_NONCE_SEQ, nonce_keys가 숫자값 기준으로 엄격히 증가하지 않음. 엄격한 증가 조건 덕분에 같은 키 집합은 인코딩과 해시가 하나로 정해집니다. 키가 uint256인 이유는 프라이버시 프로토콜이 32바이트 널리파이어나 커밋먼트에서 키를 만들기 때문입니다. ERC-4337이 24바이트 키와 8바이트 순번을 한 필드에 묶는 것과 달리 키를 자르지 않고 담고 64비트 순번 폭은 그대로 둡니다.
NONCE_MANAGER 시스템 컨트랙트와 슬롯 계산
nonce_keys == []이면 발신자의 기존 계정 논스를 씁니다. nonce_keys != []이면 각 키의 순번은 NONCE_MANAGER(0x8250968C12e01A19d6F667b9B2F3b3A4d0e51cB7)의 저장소에 프로토콜이 직접 기록합니다. 슬롯은 slot(sender, nonce_key) = keccak256(left_pad_32(sender) || uint256_to_bytes32(nonce_key))입니다. 없는 슬롯은 0으로 읽히고 프로토콜은 0을 쓰지 않으므로, 읽은 값이 0이면 처음 쓰는 키입니다. 컨트랙트 코드 NONCE_MANAGER_CODE는 0x60006000fd로 revert(0, 0)과 같아 일반 호출은 모두 되돌아갑니다. 슬롯을 쓰는 주체는 프로토콜뿐입니다. 계정 상태를 확장하지 않고 컨트랙트 저장소를 쓴 이유는 계정 MPT 구조를 바꾸지 않으면서 기존 stateRoot 아래에 키드 논스 상태를 두기 위해서입니다.
상태 유효성 검사와 단일 사용 키
검사는 EIP-8141의 기존 논스 검사와 같은 단계, 즉 어떤 프레임도 실행되기 전에 합니다. nonce_keys == []이면 tx.nonce_seq == state[tx.sender].nonce여야 합니다. nonce_keys != []이면 선택한 키마다 검사합니다. nonce_seq == MAX_NONCE_SEQ(2**64 - 1)이면 current_nonce_seq(sender, key) == 0, 즉 모든 키가 미사용이어야 합니다. 그 외에는 tx.nonce_seq == current_nonce_seq(sender, key)여야 합니다. MAX_NONCE_SEQ는 단일 사용 형태로, 소비하면 키가 소진됩니다. 검사는 블록 실행 순서상 해당 트랜잭션의 실제 pre-state를 기준으로 하므로, 같은 발신자의 키 집합이 겹치는 두 트랜잭션은 각자의 위치에서 모든 키 검사를 통과할 때만 한 블록에 들어갑니다. 이 검사 시점에 읽은 state[tx.sender].nonce를 tx_legacy_nonce라 부르고 TXPARAM(0x0D)로 노출합니다.
결제 승인에 묶인 논스 소비
EIP-8141에서 결제 승인 전이가 발신자 논스를 올리던 것을 consume_nonce_set(sender, nonce_keys, nonce_seq)로 바꿉니다. nonce_keys == []이면 increment_account_nonce(sender)로 현재 계정 논스를 1 올립니다. tx.nonce_seq + 1로 덮어쓰지 않으므로 앞 프레임에서 CREATE, CREATE2, 계정 배포로 논스가 이미 바뀐 경우에도 EIP-8141 동작이 유지됩니다. 올린 값이 MAX_NONCE_SEQ를 넘으면 예외 중단합니다. nonce_keys != []이면 키마다 MAX_NONCE_SEQ 또는 nonce_seq + 1을 씁니다. 이 소비는 payer를 None에서 resolved_target으로 바꾸는 결제 범위 APPROVE(scope 0x1 또는 0x3)에서 트랜잭션당 한 번 일어납니다. 순서는 다음과 같습니다. 1) 빈 키 집합이면 발신자가 없을 때 STATE_BYTES_PER_NEW_ACCOUNT * CPSB, 키 집합이면 KEYED_NONCE_FIRST_USE_STATE_GAS * first_use_count를 nonce_state_gas로 계산, 2) state_gas_left가 부족하면 예외 중단, 3) 상태 가스 부과, 4) consume_nonce_set 실행, 5) 나머지 승인 효과 커밋. 3~5단계는 하나의 전이로 모두 적용되거나 모두 취소됩니다. 감싼 프레임이 revert하면 전이도 되돌리지만 그 프레임이 성공한 뒤에는 뒤 프레임 실패나 원자 배치 롤백으로 되돌리지 않습니다. 트랜잭션 자체가 무효이면 모든 효과를 버립니다.
가스 비용과 블록 접근 목록
nonce_keys 인코딩은 트랜잭션 데이터로 과금합니다. nonce_seq도 과금하되, 키 집합이 비어 있지 않고 nonce_seq == MAX_NONCE_SEQ이면 면제합니다. 단일 사용 형태가 nonce_seq = 0으로 같은 키를 처음 쓰는 것보다 비싸지 않게 하려는 조치이며 서명과 페이로드는 바뀌지 않습니다. 키마다 KEYED_NONCE_ACCESS_COST = COLD_STORAGE_ACCESS + STORAGE_WRITE, 현재 값으로 12100 실행 가스를 냅니다. 이 값과 nonce_calldata_cost를 frame_tx_intrinsic_gas에 더하고 calldata_floor_tokens와 calldata_floor_gas에도 반영합니다. 처음 쓰는 키는 슬롯 하나를 새로 만들므로 KEYED_NONCE_FIRST_USE_STATE_GAS = STATE_BYTES_PER_STORAGE_SET * CPSB, 현재 값으로 97920 상태 가스를 추가로 냅니다. 이 읽기·쓰기는 프로토콜 장부 처리이므로 EIP-2929 accessed 집합에 넣지 않고 EIP-2200 SSTORE 가격을 적용하지 않으며 이후 사용자 접근을 위해 warm 처리하지도 않습니다. 반면 EIP-7928 블록 접근 목록에는 일반 저장소로 기록합니다. nonce_keys != []인 트랜잭션마다 NONCE_MANAGER가 목록에 있어야 하고 consume_nonce_set이 쓴 슬롯은 storage_changes에 new_value와 함께 들어갑니다. 쓴 값은 항상 읽은 값과 다르므로 storage_reads에는 나타나지 않습니다.
TXPARAM 확장과 호환성
TXPARAM(0x01)은 tx.nonce_seq를 돌려줍니다. 새 인덱스 네 개를 더합니다. 0x0D는 pre-state 계정 논스 tx_legacy_nonce, 0x0E는 len(tx.nonce_keys), 0x0F는 nonce_keys_hash(tx), 0x10은 tx.nonce_keys[0](키가 없으면 0)입니다. nonce_keys_hash는 keccak256(uint256_to_bytes32(len) || 각 키의 32바이트 연결)이고 키가 엄격히 증가하므로 키 집합마다 해시가 하나입니다. 0x0D는 트랜잭션 범위 값이라 결제 승인, 키 소비, 계정 배포, CREATE, CREATE2로 바뀌지 않습니다. nonce_keys == []이면 시작 시점에 TXPARAM(0x01) == TXPARAM(0x0D)이지만 키를 쓰면 둘이 다를 수 있습니다. 따라서 TXPARAM(0x01)을 계정 논스로 가정하던 검증 코드는 TXPARAM(0x0E) == 0을 강제하거나 키드 순번을 처리하도록 고쳐야 합니다. 기존 계정 객체, 프레임이 아닌 트랜잭션 타입, eth_getTransactionCount는 바뀌지 않습니다.
배포, 활성화, 멤풀 규칙
NONCE_MANAGER는 포크 때 프로토콜이 심지 않고 키 없는 배포 트랜잭션으로 올립니다. type 0x0, nonce 0x0, gas 0x3d090, gasPrice 0xe8d4a51000, input 0x60058060095f395ff360006000fd, v 0x1b, r 0x539, s 0x825000000000000141de이고 발신자는 0x7A57307E9985b0e26B66A7BB920BcaF0396b8198입니다. 이 발신자의 개인키는 알려져 있지 않아 다른 트랜잭션이 같은 주소에 코드를 둘 수 없습니다. 활성화는 EIP-8141과 같거나 그 뒤여야 하고 활성화 시점에는 NONCE_MANAGER에 0이 아닌 논스와 NONCE_MANAGER_CODE가 이미 있어야 합니다. 포크 전 블록에 배포 트랜잭션을 넣거나 제네시스에 논스 1로 넣으면 됩니다. 클라이언트는 포크 경계에서 이 계정을 검사하지 않고 활성화 자체는 상태를 바꾸지 않습니다. 포크 전 프레임 트랜잭션과 그 서명 해시에 묶인 권한은 경계를 넘지 못하므로 멤풀에서 내보내고 다시 만들어야 합니다. 멤풀에서는 대기 트랜잭션 식별자를 (sender, nonce_keys, nonce_seq)로 바꾸고 발신자 논스 의존성을 선택한 키별 current_nonce_seq로 바꿉니다. 검증 프리픽스가 TXPARAM(0x0D)를 읽으면 계정 논스도 의존성에 들어갑니다. 선택한 순번이 바뀌면 재검증해야 합니다. 결제 범위 APPROVE가 슬롯을 새로 만들 때 VERIFY 프레임은 limits.state에서 상태 가스를 쓸 수 있고 이는 MAX_VERIFY_GAS에 들어가지 않습니다. 발신자당 대기 프레임 트랜잭션 하나 제한은 그대로입니다.
보안 고려 사항과 현재 상태
재생 보호 범위는 (sender, nonce_keys, nonce_seq)입니다. 키가 겹치지 않아도 발신자·payer 잔액, 컨트랙트 저장소, 페이마스터 상태에서는 여전히 충돌할 여지가 있고 nonce_keys는 페이로드에 드러나므로 키 선택은 비밀이 아닙니다. 널리파이어 같은 단일 사용 앱은 VERIFY에서 최소한 (sender, nonce_keys_hash, nonce_seq == MAX_NONCE_SEQ)를 인증해야 하고 TXPARAM(0x08)로 정규 서명 해시를 묶는 것이 좋습니다. nonce_seq == MAX_NONCE_SEQ만 보거나 TXPARAM_NONCE_KEY_0만 확인하면 공격자가 키를 덧붙여 결제 승인에서 소비시킬 수 있어 안전하지 않습니다. 키드 트랜잭션은 계정 논스를 올리지 않으므로 tx.sender에서 CREATE를 하면 주소가 바뀔 수 있고 CREATE2를 쓰거나 TXPARAM(0x0D)로 기대 논스를 인증해야 합니다. 같은 계정 논스로 다른 트랜잭션을 보내는 취소 방식은 키드 트랜잭션을 무효화하지 못합니다. 소비된 슬롯은 삭제되지 않고 영구히 남으며 블록당 새 슬롯은 block_gas_limit // KEYED_NONCE_FIRST_USE_STATE_GAS개를 넘지 못합니다. 이 EIP는 Draft 상태라 내용이 바뀔 수 있습니다. 콜 기록으로는 07.02 ACDE #240에서 헤고타 후보(PFI)로 접수됐고 09.24 ACDE #246에서 헤고타 포함 검토(CFI)로 올라갔으며 10.12 ACDT #100에서 EIP-8141, EIP-8272, EIP-7906과 함께 bogota-devnet-1에 포함하기로 확정됐습니다.