Workspace IndexAlgorithms › Day 33

Deterministic Execution — Sealing Off Floating Point, Time, and Randomness TODO

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

Concept

Deterministic execution means that the same input and the same initial state always produce the same output and the same state transition. The usual culprits that break it are floating point (operation order, extended precision, FMA, differences between library implementations), wall-clock time and timeouts, randomness, thread scheduling, hash map iteration order, and external I/O such as files or the network. Sealing it off means turning every one of these nondeterminism sources into an injectable input: use integer or fixed-point arithmetic instead of floating point, pass time and the random seed in as arguments and record them, and fix iteration order with an explicit sort. Once that's done, replay verification, reproducible tests, and state machine replication all become possible. In consensus systems, determinism isn't a convenience — it's a safety requirement, because if nodes get different results from the same input, state has already forked.

Many irreproducible, intermittent bugs come from hidden nondeterminism like time, randomness, or iteration order, and replay debugging and replicated execution are only possible on top of determinism.

Code & Formula

# 결정론적 실행 — 부동소수점·시간·난수를 봉인해, 같은 입력이면 언제나 같은 결과가 나오게 만든다.
# float 대신 정수 고정소수점을, wall-clock 대신 주입된 시각을, os 난수 대신 시드 고정 PRNG 를 쓴다.

import random
import hashlib

SCALE = 10_000  # 고정소수점: 정수를 1/10000 단위로 취급 (부동소수점 연산 순서 의존성을 제거)

def fixed_add(a_scaled, b_scaled):
    return a_scaled + b_scaled  # 정수 덧셈은 결합/교환 법칙이 정확히 성립 — float 처럼 순서에 안 흔들림

def deterministic_run(seed, injected_time, events):
    rng = random.Random(seed)          # 벽시계 대신 시드로 재현 가능한 난수
    balance = 0
    log = []
    for ev in events:
        amount_scaled = int(round(ev * SCALE))
        balance = fixed_add(balance, amount_scaled)
        jitter = rng.randint(0, 99)     # 진짜 os.urandom 대신 시드 기반 — 리플레이 가능
        log.append((injected_time, balance, jitter))
        injected_time += 1              # time.time() 대신 명시적으로 흘려보내는 논리 시계
    return balance, log

events = [1.0001, 2.0002, -0.5, 3.3333]

# 같은 입력으로 3번 독립 실행 → 항상 같은 최종 잔고와 로그가 나와야 한다(결정론 검증).
runs = [deterministic_run(seed=42, injected_time=1000, events=events) for _ in range(3)]
digests = [hashlib.sha256(repr(r).encode()).hexdigest() for r in runs]

print("balance (scaled by 1e4):", runs[0][0])
print("balance (real value):", runs[0][0] / SCALE)
print("run 1 == run 2 == run 3 :", runs[0] == runs[1] == runs[2])
print("output hashes identical:", len(set(digests)) == 1, digests[0][:16])

Exercise

Pick a small piece of computation logic that uses time, randomness, and map iteration, separate those three sources behind injectable interfaces, then run it 1000 times with the same seed and check that the output hashes all match.

Practical Connection

The EVM avoids floating point entirely and limits external input to things like block header values, which is exactly how it secures determinism — for the same reason, Verex's off-chain settlement calculations need integer arithmetic and fixed rounding rules to match on-chain results.

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 33 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

부동소수점·시간·난수 봉인하기

개념

결정론적 실행은 같은 입력과 같은 초기 상태에서 언제나 같은 출력과 같은 상태 전이를 얻는 성질이다. 이를 깨뜨리는 대표 요인은 부동소수점(연산 순서, 확장 정밀도, FMA, 라이브러리 구현 차이), 현재 시각과 타임아웃, 난수, 스레드 스케줄링, 해시맵 순회 순서, 그리고 파일·네트워크 같은 외부 입출력이다. 봉인하는 방법은 이 비결정성 원천을 전부 주입 가능한 입력으로 바꾸는 것으로, 부동소수점 대신 정수·고정소수점 산술을 쓰고, 시각과 난수 시드는 인자로 받아 기록하며, 순회 순서는 명시적 정렬로 고정한다. 이렇게 하면 리플레이 검증, 재현 가능한 테스트, 상태 머신 복제가 모두 성립한다. 합의 시스템에서 결정론은 편의가 아니라 안전성 요건인데, 노드마다 같은 입력에서 다른 결과가 나오면 상태가 그대로 분기하기 때문이다.

재현되지 않는 간헐 버그의 상당수가 시각·난수·순회 순서 같은 숨은 비결정성에서 오고, 리플레이 디버깅과 복제 실행은 결정론 위에서만 가능하기 때문이다.

코드 · 수식

# 결정론적 실행 — 부동소수점·시간·난수를 봉인해, 같은 입력이면 언제나 같은 결과가 나오게 만든다.
# float 대신 정수 고정소수점을, wall-clock 대신 주입된 시각을, os 난수 대신 시드 고정 PRNG 를 쓴다.

import random
import hashlib

SCALE = 10_000  # 고정소수점: 정수를 1/10000 단위로 취급 (부동소수점 연산 순서 의존성을 제거)

def fixed_add(a_scaled, b_scaled):
    return a_scaled + b_scaled  # 정수 덧셈은 결합/교환 법칙이 정확히 성립 — float 처럼 순서에 안 흔들림

def deterministic_run(seed, injected_time, events):
    rng = random.Random(seed)          # 벽시계 대신 시드로 재현 가능한 난수
    balance = 0
    log = []
    for ev in events:
        amount_scaled = int(round(ev * SCALE))
        balance = fixed_add(balance, amount_scaled)
        jitter = rng.randint(0, 99)     # 진짜 os.urandom 대신 시드 기반 — 리플레이 가능
        log.append((injected_time, balance, jitter))
        injected_time += 1              # time.time() 대신 명시적으로 흘려보내는 논리 시계
    return balance, log

events = [1.0001, 2.0002, -0.5, 3.3333]

# 같은 입력으로 3번 독립 실행 → 항상 같은 최종 잔고와 로그가 나와야 한다(결정론 검증).
runs = [deterministic_run(seed=42, injected_time=1000, events=events) for _ in range(3)]
digests = [hashlib.sha256(repr(r).encode()).hexdigest() for r in runs]

print("balance (scaled by 1e4):", runs[0][0])
print("balance (real value):", runs[0][0] / SCALE)
print("run 1 == run 2 == run 3 :", runs[0] == runs[1] == runs[2])
print("output hashes identical:", len(set(digests)) == 1, digests[0][:16])

연습

시각·난수·맵 순회를 쓰는 작은 계산 로직을 골라 그 세 원천을 주입 가능한 인터페이스로 분리하고, 같은 시드로 1000회 실행해 출력 해시가 전부 동일한지 확인하기.

실무 · Verex 연결

EVM은 부동소수점을 아예 두지 않고 외부 입력을 블록 헤더 값 정도로 제한해 결정론을 확보하는데, 같은 이유로 Verex의 오프체인 정산 계산도 온체인 결과와 일치하려면 정수 산술과 고정된 반올림 규칙을 써야 한다.

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

← 32. FFI·ABI 경계와 안전성(패닉·정렬·수명)34. 형식 검증 →