Workspace IndexAlgorithms › Day 26

Gas Accounting Design TODO

Algorithms · Day 26 / 100 · B. Compilers, Runtimes & VMs (Day 20-35)

Concept

Gas is a mechanism that quantifies compute, state storage, and bandwidth consumption into a single accounting unit and charges for it. Every opcode's gas value needs to be proportional to its actual resource consumption; even one underpriced operation becomes a DoS vector (this is why Ethereum's state-access and storage costs have been repriced several times in the past). Writes that grow state are made expensive, while operations that free state are given a refund, shaping the incentives around state growth. EIP-2929, which charges differently for a first (cold) access versus a repeat (warm) access to the same slot or address, is an example of even cache locality being folded into the pricing model. In the end, the block gas limit is an upper bound that caps a block's worst-case execution time so nodes can keep up.

If the cost model diverges from actual resource consumption, an attacker can stall a node for pennies, and if it's overpriced instead, honest users get priced out.

Code & Formula

# 가스 회계 설계 — opcode별 비용을 합산하고, cold/warm 접근 차등 과금(EIP-2929)과 블록 가스 한도를 시뮬레이션한다.

BASE_COST = {"ADD": 3, "MUL": 5, "SLOAD": 100, "SSTORE_SET": 20000}
COLD_SURCHARGE = 2000     # 슬롯 첫 접근(cold)에 추가로 붙는 과금
BLOCK_GAS_LIMIT = 30000

def run_tx(ops):
    gas_used = 0
    warm_slots = set()     # 트랜잭션 안에서 이미 접근한 슬롯은 이후 warm
    trace = []
    for op, *arg in ops:
        cost = BASE_COST[op]
        if op in ("SLOAD", "SSTORE_SET") and arg:
            slot = arg[0]
            if slot not in warm_slots:
                cost += COLD_SURCHARGE      # cold 접근: 자원 소비가 더 크다고 보고 과금
                warm_slots.add(slot)
        gas_used += cost
        trace.append((op, arg, cost, gas_used))
    return gas_used, trace

# 슬롯 x 를 두 번 읽는 트랜잭션: 첫 접근은 cold, 두 번째는 warm(캐시 지역성을 요금에 반영)
ops = [("SLOAD", "x"), ("ADD",), ("SLOAD", "x"), ("SSTORE_SET", "y"), ("MUL",)]
gas_used, trace = run_tx(ops)

for op, arg, cost, cum in trace:
    print(f"  {op}{arg or ''}: cost={cost:>6}  누적={cum}")

print(f"\n총 가스: {gas_used}, 블록 한도: {BLOCK_GAS_LIMIT}, 한도 내: {gas_used <= BLOCK_GAS_LIMIT}")

# DoS 저항 사고 실험: 이 트랜잭션을 블록 하나에 최대 몇 번 담을 수 있는가
max_repeats = BLOCK_GAS_LIMIT // gas_used
print(f"이 트랜잭션을 블록 하나에 최대 {max_repeats}번 담을 수 있음 (가스가 실행량을 캡핑)")

Exercise

Implement the same logic in two versions — one centered on storage writes, one centered on calldata and events — pull a gas report for each, and break down which opcodes dominate the cost.

Practical Connection

Verex's settlement and order processing need to finish within the block gas limit no matter how many participants there are, so avoiding unbounded array iteration and shifting cost to users via a pull-based claim design is necessary.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

가스 회계 설계 TODO

Algorithms · Day 26 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

비용 모델과 DoS 저항

개념

가스는 연산·상태 저장·대역폭 소비를 하나의 회계 단위로 정량화해 요금을 매기는 장치다. 각 opcode의 가스 값은 실제 자원 소비에 비례해야 하며, 저평가된 연산이 하나라도 있으면 그것이 곧 DoS 벡터가 된다(과거 이더리움에서 상태 접근·스토리지 비용이 여러 차례 재조정된 이유다). 상태를 늘리는 쓰기는 비싸게, 상태를 지우는 연산은 환급을 주는 식으로 상태 증가에 대한 유인을 설계한다. EIP-2929처럼 같은 슬롯·주소의 첫 접근(cold)과 재접근(warm)을 다르게 과금하는 것은 캐시 지역성까지 요금 모델에 반영한 예다. 블록 가스 한도는 결국 한 블록의 최악 실행 시간을 묶어 노드가 따라올 수 있게 하는 상한이다.

비용 모델이 실제 자원 소비와 어긋나면 공격자가 헐값에 노드를 마비시킬 수 있고, 반대로 과다 책정하면 정직한 사용자가 밀려난다.

코드 · 수식

# 가스 회계 설계 — opcode별 비용을 합산하고, cold/warm 접근 차등 과금(EIP-2929)과 블록 가스 한도를 시뮬레이션한다.

BASE_COST = {"ADD": 3, "MUL": 5, "SLOAD": 100, "SSTORE_SET": 20000}
COLD_SURCHARGE = 2000     # 슬롯 첫 접근(cold)에 추가로 붙는 과금
BLOCK_GAS_LIMIT = 30000

def run_tx(ops):
    gas_used = 0
    warm_slots = set()     # 트랜잭션 안에서 이미 접근한 슬롯은 이후 warm
    trace = []
    for op, *arg in ops:
        cost = BASE_COST[op]
        if op in ("SLOAD", "SSTORE_SET") and arg:
            slot = arg[0]
            if slot not in warm_slots:
                cost += COLD_SURCHARGE      # cold 접근: 자원 소비가 더 크다고 보고 과금
                warm_slots.add(slot)
        gas_used += cost
        trace.append((op, arg, cost, gas_used))
    return gas_used, trace

# 슬롯 x 를 두 번 읽는 트랜잭션: 첫 접근은 cold, 두 번째는 warm(캐시 지역성을 요금에 반영)
ops = [("SLOAD", "x"), ("ADD",), ("SLOAD", "x"), ("SSTORE_SET", "y"), ("MUL",)]
gas_used, trace = run_tx(ops)

for op, arg, cost, cum in trace:
    print(f"  {op}{arg or ''}: cost={cost:>6}  누적={cum}")

print(f"\n총 가스: {gas_used}, 블록 한도: {BLOCK_GAS_LIMIT}, 한도 내: {gas_used <= BLOCK_GAS_LIMIT}")

# DoS 저항 사고 실험: 이 트랜잭션을 블록 하나에 최대 몇 번 담을 수 있는가
max_repeats = BLOCK_GAS_LIMIT // gas_used
print(f"이 트랜잭션을 블록 하나에 최대 {max_repeats}번 담을 수 있음 (가스가 실행량을 캡핑)")

연습

같은 로직을 storage 쓰기 중심과 calldata·event 중심 두 버전으로 구현해 gas 리포트를 뽑고, 어떤 opcode가 비용을 지배하는지 분해하라.

실무 · Verex 연결

Verex의 정산과 주문 처리는 참여자 수가 늘어도 블록 가스 한도 안에서 끝나야 하므로, 무제한 배열 순회를 피하고 pull 방식 청구로 비용을 사용자 쪽에 분산시키는 설계가 필요하다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 25. 스택 머신 vs 레지스터 머신27. EVM 인터프리터 내부 →