멈춘 ETH 계열 출금을 대량으로 재전송하기 전에 한 건으로 카나리를 돌리기로 했다. 대상 하나를 고르고 그 출금의 status 를 확인한 뒤 조작 직전에 하나를 더 봤다.
Table of contents
Open Table of contents
제약 — 주소에 토큰별 출금이 붙는 구조
이 시스템은 핫월렛 주소 하나에서 토큰별 출금이 나간다.
핫월렛 주소 X
├─ ETH 출금 nonce 41 (멈춤)
├─ 토큰 A 출금 nonce 42 (대기)
└─ 토큰 B 출금 nonce 43 (대기)
그런데 화면의 재전송 버튼은 pending_withdrawals 행이 아니라 주소의 nonce 단위로 동작한다.
의도한 출금만 나가는 것이 아닐 수 있다는 뜻이다. 같은 주소에서는 nonce 가 순서대로 처리되므로 41 을 수수료를 올려 대체하면 뒤에 막혀 있던 42 와 43 이 함께 풀린다. 한 출금만 보고 안전하다고 판단하면 다른 토큰에서 일어나는 변화를 못 본다.
검증 — 같은 주소의 대기 건을 셌다
스냅샷 쿼리에 서브쿼리를 하나 넣었다.
SELECT
pw.id, pw.coin, pw.status, pw.nonce, ...,
(SELECT COUNT(*) FROM pending_withdrawals p2
WHERE p2.from_address = pw.from_address
AND p2.broadcast_at IS NOT NULL
AND p2.confirmed_at IS NULL) AS pending_same_address
FROM pending_withdrawals pw
WHERE pw.id = ?;
pending_same_address 로 그 주소의 미확정 대기 건 수를 같이 뽑는다.
결과는 이랬다.
pending_same_address 1
confirmed_at 이 비어 있는 행이 이것 하나뿐이라 다른 토큰으로 파급될 행 자체가 없었다.
판단 기준 — 1이면 안전하다
pending_same_address 가 1이면 파급이 구조적으로 불가능하다.
2 이상이면 둘 중 하나를 고른다.
[선택 1] 그 출금을 카나리 대상에서 뺀다
[선택 2] 다른 토큰 출금까지 함께 나가는 것을 사전 합의하고 진행
선택 2 를 버리고 선택 1 을 골랐다. 이유는 어느 쪽을 택하든 모르는 채로 진행하는 것보다는 낫지만 대상을 바꾸는 편이 간단해서다.
주의 — 검증 결과의 오염
pending_same_address 가 2 이상인 출금은 결과 해석을 어렵게 만든다.
카나리 결과가 이상함
↓
내 재전송 때문인가, 함께 풀린 다른 토큰 출금 때문인가
↓
구분이 안 된다
카나리의 목적은 그 재전송 하나의 효과를 보는 것인데 다른 변수가 섞이면 그것을 못 본다.
카나리 대상은 변수가 적어야 한다. 대기 건이 여럿인 주소는 변수가 여럿이고 하나뿐인 주소는 하나라서 대상 선정 자체가 실험 설계였다.
조치 — 스냅샷 쿼리에 상시 포함
이 COUNT(*) 확인을 일회성으로 두지 않았다.
pending_same_address 를 스냅샷 쿼리에 상시 포함해서 카나리를 돌릴 때마다 자동으로 확인되게 했다.
사람의 기억에 의존하는 확인 항목은 급할 때 가장 먼저 빠진다. 절차에 넣어 두면 다음에 누가 하더라도 그 값이 함께 나온다.
사전 준비 — 스냅샷이 되돌림 근거
조작 전 값을 남기는 것까지가 절차다.
조작 후 이상함
↓
원래 값이 뭐였지?
↓
스냅샷에 있다
SELECT 로 떠 둔 스냅샷이 없으면 되돌릴 근거가 없다. 체인에 나간 것은 되돌릴 수 없어도 우리 쪽 상태와 대조할 근거는 남는다.
무엇을 담을지도 정리해 뒀다.
출금 행: ID, 코인, 상태, nonce, txid, gas price, 전파 시각
회원: member_no, 잔액 잠금액
노드: 주소의 확정 nonce, mempool 대기 nonce
파급: 같은 주소의 미확정 대기 건 수
SELECT 하나로 네 가지를 다 뽑는다.
토큰 출금은 결과 확인도 ETH 와 다르다. 컨트랙트의 transfer 호출이라 트랜잭션의 value 는 0 이고, EIP-20 명세가 transfer 는 반드시 Transfer 이벤트를 발생시켜야 한다고 정하므로 그 이벤트 로그에서 수량을 읽어야 한다. 스냅샷에 토큰 여부를 같이 적어 둔 것은 그래서다.
여러 번 나눠 뽑으면 그 사이에 값이 바뀔 수 있고 빠뜨리기도 한다. 조작보다 그 앞에 남기는 기록이 더 중요한 경우가 자주 있었다.
정리
- 핫월렛 주소 하나에서 토큰별 출금이 나가면 재전송이 다른 토큰으로 파급된다
- 한 출금만 보고 판단하면 나머지를 못 본다
- 카나리 전에 같은 주소의 미확정 대기 건 수를 센다
- 그 값이 1이면 파급이 구조적으로 불가능하다
- 1이 아니면 대상에서 빼거나 영향까지 합의한다
- 대기 건이 여럿인 대상은 검증 결과를 오염시킨다
- 카나리 대상 선정이 곧 실험 설계다
- 확인을 스냅샷 쿼리에 상시 포함해 잊지 않게 한다
- 조작 전 값을 남기는 것까지가 절차다
- 스냅샷은 한 쿼리로 다 뽑는다