mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 02:42:27 +00:00
EIPs: add eip-2929.md
This commit is contained in:
parent
fff6498ed6
commit
2e7cc76071
1 changed files with 229 additions and 0 deletions
229
EIPs/eip-2929.md
Normal file
229
EIPs/eip-2929.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# EIP-2929: Gas cost increases for state access opcodes
|
||||
|
||||
|
||||
## Abstract
|
||||
EIP-2929는 `베를린 하드포크`에서 시행되었습니다. 이 EIP는 이더리움의 가스 비용 메커니즘을 공정하게 개선하여 DoS공격에 대한 네트워크 보안을 강화하는 것을 목표로 합니다. 주요 내용은 다음과 같습니다.
|
||||
|
||||
1. `SLOAD`(0x54) 연산의 가스비용을 2100으로 늘리고 `*CALL` opcode 집합(0xf1, f2, f4, fA)과, `BALANCE`(0x31), `EXT*` opcode 집합(0x3b, 0x3c, 0x3f)의 가스비용을 2600으로 증가시킵니다.
|
||||
2. precompiles와 이미 동일한 트랜잭션에서 액세스된 주소 및, 스토리지 슬롯은 가스 비용 감면 대상이 되어 불필요한 가스비용을 줄입니다.
|
||||
3. 그리고 `SSTORE`의 계량을 개선하고, `SELFDESTRUCT`와 관련된 “de-facto storage loads”(실제 스토리지 로드)에 대한 가스 비용 책정을 수정합니다.
|
||||
|
||||
이러한 변경사항은 이더리움 네트워크의 가스비용을 더욱 공정하고 안전하게 만드는 것을 목표로 합니다. 프리컴파일과 이미 엑세스된 주소 및 스토리지 슬롯에 대한 가스 비용을 감면함으로써, 불필요한 가스 비용을 줄이고, `STORE`와 `SELFDESTRUCT` opcode의 가스비용을 조정하여 스토리지를 줄이는 것에 유인을 제공합니다.
|
||||
|
||||
## Motivation
|
||||
EIP-2929는 이더리움의 가스비용 매커니즘을 개선하여 네트워크 보안과 성능을 향상시키고자 하는 목표가 있습니다.
|
||||
과거에는 storage access (`SLOAD`, `CALL`, `BALANCE`, `EXT`)등의 opcode의 가스 비용이 실제 처리 시간에 비해 낮게 책정되어 있었습니다. 이로 인해 `2016년 Shanghai DoS attack`에서 공격자가 다수의 계정에 액세스하거나, 호출하는 트랜잭션을 보내는 형태의 DoS 공격이 발생할 수 있었습니다.
|
||||
이 문제를 완화하기 위해 가스 비용이 인상되었으나, 충분하지 않았습니다.
|
||||
EIP-2929에서는 이러한 opcode들의 비용을 대략 3배 정도 올림으로써, 최대 처리 시간을 7~27초로 단축시키고자 합니다. 이는 이더리움 state machine 데이터베이스의 안정성 개선과 성능을 향상시킬 수 있습니다.
|
||||
EIP-2929는 이더리움 확장성 로드맵 중 하나인 stateless witness를 실현할 수 있는 작업의 기반작업이 될 수 있습니다. BALANCE opcode의 가스비용이 700에서 2600으로 증가하여, (12500000 gas limit) / (700 gas per BALANCE) * (800 witness bytes per BALANCE) ~= 14.3M bytes to 12500000 / 2600 * 800 ~= 3.85M bytes로 코드 크기를 제외한 이론적인 witness의 이론적인 최대 크기가 14.3MB에서 3.85MB로 줄어들수 있기 때문입니다.
|
||||
또한 SNARK/STARK의 witness 개선에도 기여할 수 있습니다. 이러한 영지식 증명기술에서도 머클패트리샤 트리를 사용하는데, 각 브랜치에 25개의 해시가 있고 블록이 state access로 가득 차 있다고 가정할 경우, 현재는 witness를 생성하는데 약 44.64초가 소요됩니다. 그러나 EIP-2929를 적용한 후에는 이 시간이 12.5초로 단축됩니다.
|
||||
|
||||
## Change
|
||||
### Parameters
|
||||
|
||||
| Parameter | Value |
|
||||
|---------------------------|-------------------------|
|
||||
| `FORK_BLOCK` | 12244000 |
|
||||
| `COLD_SLOAD_COST` | 2100 |
|
||||
| `COLD_ACCOUNT_ACCESS_COST`| 2600 |
|
||||
| `WARM_STORAGE_READ_COST` | 100 |
|
||||
| `SLOAD_GAS` | WARM_STORAGE_READ_COST |
|
||||
| `SSTORE_RESET_GAS` | 5000 - COLD_SLOAD_COST |
|
||||
|
||||
12244000 블록 이후 EIP-2929가 적용됩니다.
|
||||
트랜잭션을 실행하면
|
||||
- accessed_address: Set[Address]
|
||||
- accessed_storage_keys: Set[Tuple[Address, Bytes32]]
|
||||
|
||||
두 집합을 transaction-context-wide로 유지합니다.
|
||||
|
||||
|
||||
### Access list에 address와 slot이 속해 있는지 확인
|
||||
ContainsAddress는 address가 access list에 있는지 여부를 리턴합니다.
|
||||
```go
|
||||
// ContainsAddress returns true if the address is in the access list.
|
||||
func (al *accessList) ContainsAddress(address common.Address) bool {
|
||||
_, ok := al.addresses[address]
|
||||
return ok
|
||||
}
|
||||
```
|
||||
|
||||
Contains는 slot이 access list에 있는 account에 속해 있는지 확인하고 account와 slot각각에 대해 분리된 flag를 리턴합니다.
|
||||
|
||||
```go
|
||||
// Contains checks if a slot within an account is present in the access list, returning
|
||||
// separate flags for the presence of the account and the slot respectively.
|
||||
func (al *accessList) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) {
|
||||
idx, ok := al.addresses[address]
|
||||
if !ok {
|
||||
// no such address (and hence zero slots)
|
||||
return false, false
|
||||
}
|
||||
if idx == -1 {
|
||||
// address yes, but no slots
|
||||
return true, false
|
||||
}
|
||||
_, slotPresent = al.slots[idx][slot]
|
||||
return true, slotPresent
|
||||
}
|
||||
```
|
||||
|
||||
### `*EXT`계열과 `CALL`계열 opcode 가스비용 변경
|
||||
Address가 `EXTCODESIZE` (0x3B), `EXTCODECOPY` (0x3C), `EXTCODEHASH` (0x3F) or `BALANCE` (0x31)) 혹은
|
||||
(`CALL` (0xF1), `CALLCODE` (0xF2), `DELEGATECALL` (0xF4), `STATICCALL` (0xFA) opcode를 사용하면 accessed address인지 확인 후 accessed라면 `WARM_STORAGE_READ_COST`에 해당하는 가스비를, 다른 경우에는 `COLD_ACCOUNT_ACCESS_COST`를 청구합니다. 그리고 해당 address를 accessed_address Set에 추가합니다.
|
||||
|
||||
```go
|
||||
// gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929
|
||||
// EIP spec:
|
||||
// > If the target is not in accessed_addresses,
|
||||
// > charge COLD_ACCOUNT_ACCESS_COST gas, and add the address to accessed_addresses.
|
||||
// > Otherwise, charge WARM_STORAGE_READ_COST gas.
|
||||
func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
|
||||
// memory expansion first (dynamic part of pre-2929 implementation)
|
||||
gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
addr := common.Address(stack.peek().Bytes20())
|
||||
// Check slot presence in the access list
|
||||
if !evm.StateDB.AddressInAccessList(addr) {
|
||||
evm.StateDB.AddAddressToAccessList(addr)
|
||||
var overflow bool
|
||||
// We charge (cold-warm), since 'warm' is already charged as constantGas
|
||||
if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow {
|
||||
return 0, ErrGasUintOverflow
|
||||
}
|
||||
return gas, nil
|
||||
}
|
||||
return gas, nil
|
||||
}
|
||||
```
|
||||
|
||||
### `SLOAD`, `SSTORE` opcode의 가스비용 변경
|
||||
`SLOAD`와 `SSTORE`의 경우에도 accessed address인지 확인하고 access 여부에 따라 명시된 gas값을 청구합니다.
|
||||
|
||||
```go
|
||||
// gasSLoadEIP2929 calculates dynamic gas for SLOAD according to EIP-2929
|
||||
// For SLOAD, if the (address, storage_key) pair (where address is the address of the contract
|
||||
// whose storage is being read) is not yet in accessed_storage_keys,
|
||||
// charge 2100 gas and add the pair to accessed_storage_keys.
|
||||
// If the pair is already in accessed_storage_keys, charge 100 gas.
|
||||
func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
|
||||
loc := stack.peek()
|
||||
// slot에 접근 데이터 hash를 저장
|
||||
slot := common.Hash(loc.Bytes32())
|
||||
// Check slot presence in the access list
|
||||
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
|
||||
// If the caller cannot afford the cost, this change will be rolled back
|
||||
// If he does afford it, we can skip checking the same thing later on, during execution
|
||||
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
|
||||
return params.ColdSloadCostEIP2929, nil
|
||||
}
|
||||
return params.WarmStorageReadCostEIP2929, nil
|
||||
}
|
||||
```
|
||||
|
||||
### `SELFDESTRUCT` opcode 가스비용 변경
|
||||
`SELFDESTRUCT`는 accessed_address가 아닐경우 데이터의 양과 관계없이 `COLD_ACCOUNT_ACCESS_COST`를 청구하고, accessed_address의 경우 두번이상 작동하지 않기 때문에 추가적인 gas를 청구하지 않습니다.
|
||||
|
||||
makeSelfdestructGasFn은 selfdestruct의 dynamic gas func를 생성합니다.
|
||||
|
||||
```go
|
||||
// makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-3529
|
||||
func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
|
||||
gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
|
||||
var (
|
||||
gas uint64
|
||||
address = common.Address(stack.peek().Bytes20())
|
||||
)
|
||||
if !evm.StateDB.AddressInAccessList(address) {
|
||||
// If the caller cannot afford the cost, this change will be rolled back
|
||||
evm.StateDB.AddAddressToAccessList(address)
|
||||
gas = params.ColdAccountAccessCostEIP2929
|
||||
}
|
||||
// if empty and transfers value
|
||||
if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
|
||||
gas += params.CreateBySelfdestructGas
|
||||
}
|
||||
if refundsEnabled && !evm.StateDB.HasSelfDestructed(contract.Address()) {
|
||||
evm.StateDB.AddRefund(params.SelfdestructRefundGas)
|
||||
}
|
||||
return gas, nil
|
||||
}
|
||||
return gasFunc
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Access list에 address와 slot추가
|
||||
|
||||
AddAddress는 access list에 address를 추가하고, 이 연산이 변화를 만들었다면 true를 리턴합니다.
|
||||
|
||||
```go
|
||||
// AddAddress adds an address to the access list, and returns 'true' if the operation
|
||||
// caused a change (addr was not previously in the list).
|
||||
func (al *accessList) AddAddress(address common.Address) bool {
|
||||
if _, present := al.addresses[address]; present {
|
||||
return false
|
||||
}
|
||||
al.addresses[address] = -1
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
AddSlot은 (addr, slot)을 access list에 추가합니다.
|
||||
|
||||
```go
|
||||
// AddSlot adds the specified (addr, slot) combo to the access list.
|
||||
// Return values are:
|
||||
// - address added
|
||||
// - slot added
|
||||
// For any 'true' value returned, a corresponding journal entry must be made.
|
||||
func (al *accessList) AddSlot(address common.Address, slot common.Hash) (addrChange bool, slotChange bool) {
|
||||
idx, addrPresent := al.addresses[address]
|
||||
if !addrPresent || idx == -1 {
|
||||
// Address not present, or addr present but no slots there
|
||||
al.addresses[address] = len(al.slots)
|
||||
slotmap := map[common.Hash]struct{}{slot: {}}
|
||||
al.slots = append(al.slots, slotmap)
|
||||
return !addrPresent, true
|
||||
}
|
||||
// There is already an (address,slot) mapping
|
||||
slotmap := al.slots[idx]
|
||||
if _, ok := slotmap[slot]; !ok {
|
||||
slotmap[slot] = struct{}{}
|
||||
// Journal add slot change
|
||||
return false, true
|
||||
}
|
||||
// No changes required
|
||||
return false, false
|
||||
}
|
||||
```
|
||||
|
||||
*geth의 Pull Request #21509에서 더 자세한 구현을 확인할 수 있습니다.
|
||||
https://github.com/ethereum/go-ethereum/pull/21509/files
|
||||
|
||||
|
||||
|
||||
## Reasoning
|
||||
### opcode의 가스비용을 조정
|
||||
Witness 데이터의 크기를 gas비에 반영하기 위해서는 opcode의 가스비용을 조정하는 방식과 witness데이터의 바이트당 gas 비용을 측정하여 청구하는 방법이 있습니다. 그러나 단기적인 보안 해결이 필요했던 베를린 하드포크에서는 빠르게 업데이트할 수 있는 전자의방식을 택했습니다.
|
||||
또한 바이트당 비용을 청구하는 방식을 사용할 경우 컨트랙트 코드에 접근하는 트랜잭션의 가스 비용이 지나치게 높아질 수 있습니다. 이는 해당 트랜잭션은 컨트랙트 코드의 모든 바이트를 측정해야 하고, 이 접근에 대한 비용을 청구해야하기 때문입니다. 이는 개발환경의 부담을 만들어내므로 적합하지 않습니다. 단기적인 DoS공격 예방 측면에서는 24kB를 읽어오는 것과 32Byte를 읽어오는 것에 비용차이가 크지 않으므로 코드 크기에 지나친 신경을 쓸 필요가 없습니다.
|
||||
|
||||
### accessed_addresses와 accessed_storage_keys 세트의 도입
|
||||
EIP-2929는 이미 액세스한 계정과 스토리지 슬롯의 세트를 추적하기 위해 accessed_addresses와 accessed_storage_keys를 도입했습니다. 이는 캐시될 수 있는 데이터에 대해 불필요한 비용 청구를 피하기 위한 것입니다. 대부분의 효율적인 구현에서는 이미 이러한 데이터를 캐시하고 있습니다.
|
||||
또한, 이를 통해 자기 호출(self-call)이나 프리컴파일 호출 시 불필요하게 높은 비용이 발생하는 문제를 해결할 수 있습니다. 나아가 향후 실행에서 예상된 양의 가스를 사용할 수 있도록 일부 스토리지 키를 미리 가져오는 등의 계약 중단 완화 방안을 가능하게 합니다.
|
||||
|
||||
## Conclusion
|
||||
결론적으로, EIP-2929는 이더리움 네트워크의 상태 액세스 비용을 조정하여 DoS 공격에 대한 취약점을 해결하고, 성능을 개선하기 위해 도입되었습니다. EIP-2929는 cold access에 대한 비용을 증가시키고, warm access에 대한 비용을 감소시킴으로써 state access 패턴에 따라 가스 비용을 차등 부과하는 방식을 채택했습니다.
|
||||
|
||||
EIP-2929의 주요 수정 사항으로는 SLOAD 연산의 가스 비용을 2100에서 100으로 감소시키고, `*CALL` 계열 연산과 `SSTORE` 연산에서 cold access에 대한 추가 비용을 부과하는 것이 있습니다. 이를 통해 state access 패턴에 따른 가스 비용의 불균형을 해소하고, `2016년 Shanghai DoS attack`과 같은 형태의 공격을 방지할 수 있게 되었습니다.
|
||||
|
||||
EIP-2929의 도입으로 인해 이더리움 네트워크의 전반적인 성능이 향상되었습니다. warm access에 대한 비용 감소로 인해 일반적인 트랜잭션 처리 속도가 빨라졌으며, cold access에 대한 비용 증가로 인해 악의적인 행위자의 공격이 억제되었습니다. 또한, 가스 비용의 예측 가능성이 향상되어 사용자와 개발자들이 트랜잭션 비용을 보다 정확하게 예측할 수 있게 되었습니다.
|
||||
|
||||
## Reference
|
||||
공식문서: https://eips.ethereum.org/EIPS/eip-2929#implementation
|
||||
|
||||
Pull Request: https://github.com/ethereum/go-ethereum/pull/21509/files
|
||||
|
||||
|
||||
Loading…
Reference in a new issue