Skip to content
isdnetworks
Go back

다섯으로 수렴한 실패 사유

(수정됨)

오입금 반환 전송을 100건씩 나눠 돌렸다. 웨이브마다 몇 건씩 실패해서 그때마다 노드 응답 로그를 조회해 사유를 분류했다.

Table of contents

Open Table of contents

결과 — 다섯으로 모인 사유

몇 웨이브를 돌리니 사유가 모였다.

반환 주소 무효 (-5)                  가장 빈번
핫월렛 잔액 부족 (-6)
더스트 금액 (-4, amount too small)
mempool 거절 (-4, rejected)
무음 스킵

무음 스킵 까지 다섯 종이 반복해서 나오고 새 사유가 더 나오지 않았다.

오류 코드는 sendtoaddress 가 돌려주는 JSON-RPC 오류의 code 다. 이 코드 표는 공식 문서의 RPC 항목이 아니라 Bitcoin Core 0.15 소스의 rpc/protocol.h 에 있고, 더스트와 mempool 거절이 둘 다 -4 로 오는 것도 거기서 확인했다. 코드만으로는 둘이 안 갈리니 message 까지 봐야 했다.

const rpc = require('./rpc');
try {
  row.txid = await rpc.call('sendtoaddress', [row.refund_address, row.amount]);
} catch (e) {
  if (e.code === -5) reason = 'invalid_address';
  else if (e.code === -6) reason = 'insufficient_funds';
  else if (e.code === -4 && /too small/.test(e.message)) reason = 'dust';
  else if (e.code === -4 && /rejected/.test(e.message)) reason = 'mempool_rejected';
}

새로운 것이 안 나온다는 것 자체가 정보였다. 남은 웨이브에서도 이 다섯 안에서 실패할 것이라고 볼 수 있게 된다.

비율이 안정적이었다

실패율도 세어 봤다.

100건 웨이브 기준 두세 건

100건 짜리 웨이브마다 비슷하게 나왔다.

[비율을 모름]   얼마나 걸릴지 모름
[웨이브당 두세 건]  그만큼의 판정·원복

비율이 정해지면 남은 작업량을 미리 계산할 수 있다.

100건 웨이브를 열 번 돌리면 스무 건에서 서른 건의 후속 작업이 나온다는 뜻이다. 그만큼의 시간을 일정에 미리 넣어 둘 수 있게 됐다.

조치 — 사유별 사전 게이트

사유가 다섯으로 정해지니 미리 거를 수 있는 것이 보였다.

[전송 후 실패]  실패 처리 → 원복 → 재작업
[사전 게이트]   대상에서 미리 제외

각 사유를 전송 전에 확인 가능한 조건으로 바꾸면 된다.

반환 주소 무효   →  validateaddress 로 사전 확인
더스트 금액      →  최소 반환 금액 미만은 대상에서 제외
핫월렛 잔액 부족 →  웨이브 합계와 getbalance 를 비교한 뒤 시작

반환 주소 무효 부터 핫월렛 잔액 부족 까지 셋은 우리 쪽 자료와 노드 조회만으로 판정할 수 있었다. mempool 거절 은 보내 봐야 아는 종류라 게이트에 못 넣었다.

전송 후 실패 처리 대신 사전 게이트 쪽을 골랐다. 이유는 실패 한 건마다 원복과 재작업이 따라오는데 게이트는 조회 한 번이라서다.

미리 거르면 보내는 건수가 줄고 노드 부담도 준다. 다만 그 조건이 틀리면 보낼 수 있는 건까지 빠지므로 거른 건수를 따로 세어 실패했을 수와 맞는지 대조했다.

다섯째가 달랐다

무음 스킵만 성격이 달랐다.

워커는 웨이브를 정상 종료
실패 목록도 비어 있음
그런데 txid 가 안 생김

txid 없이 정상 종료로 끝난다.

[다른 넷]  sendtoaddress 가 오류를 돌려줌 → 실패 로그가 남음
[무음 스킵]  sendtoaddress 를 부르지 않음 → 로그가 없음

워커가 같은 반환 주소로 이미 보낸 건이 있으면 중복으로 보고 조용히 건너뛰고 있었다. 한 사용자가 오입금을 두 번 한 경우가 그 조건에 걸렸다.

fail_reason 을 채우는 조회로는 이 종류가 잡히지 않는다.

앞서 정리한 것과 같은 모양이다. 오류는 목록에 남고 미발생은 아무 데도 안 남는다.

검증 — 건수 대조

없는 것을 찾으려면 다른 방법이 필요했다.

전송한 대상 목록
    vs
txid 가 생긴 목록

보낸 COUNT(*) 와 txid IS NOT NULL 인 COUNT(*) 를 맞춰 보면 그 차이가 무음 스킵 이다.

[로그 기반]  실패한 것을 찾음
[건수 대조]  없어진 것을 찾음

두 방법이 서로 다른 것을 잡으므로 로그만 보면 다섯 중 넷만 보인다.

분류한 사유는 반환 표에 컬럼을 두고 웨이브마다 기록했다.

UPDATE misdeposit_refunds SET fail_reason = ? WHERE id = ?;

fail_reason 을 쌓아 두니 분포를 볼 수 있게 됐다.

SELECT fail_reason, COUNT(*) FROM misdeposit_refunds
 WHERE fail_reason IS NOT NULL GROUP BY 1;

기록하지 않고 웨이브마다 로그만 봤으면 사유가 다섯인지 열인지 몰랐을 것이다. 한 웨이브의 실패는 흩어져 보이고 열 웨이브의 실패는 분포를 만든다.

대응 — 재개 지점을 조건으로

여러 웨이브를 돌리니 어디부터 다시 할지가 필요해졌다.

SELECT r.id FROM misdeposit_refunds r
  JOIN deposits d ON d.id = r.deposit_id
 WHERE r.txid IS NULL
   AND r.reverted_at IS NULL
   AND r.fail_reason IS NULL
   AND d.status = 'misdeposit'
 ORDER BY r.id
 LIMIT 100;

txid 와 reverted_at 과 fail_reason 과 status 네 조건이 남은 대상을 고른다.

[진행 번호로]  어디까지 했는지 기억해야 함
[조건으로]     테이블이 상태를 갖고 있음

조건이 상태를 표현하니 중간에 멈춰도 같은 쿼리로 이어지고 fail_reason 이 채워진 건은 자동으로 빠진다.

ORDER BY 를 넣은 것은 실행마다 순서가 달라지지 않게 하려는 것이었다. 순서가 고정되면 같은 대상이 같은 묶음에 들어가고 문제가 생겼을 때 어디까지 갔는지가 확실해진다.

실패건을 전량 원복할 수 있다는 것이 이 방식의 전제였다. 다섯 사유 전부 브로드캐스트 전에 멈추는 것이라 우리 쪽 상태만 되돌리면 됐고, 체인에 나간 뒤에 되돌릴 일이 없었기에 웨이브를 계속 돌릴 수 있었다.

정리


Share this post on:

Previous Post
초 단위로는 순서를 못 가렸다
Next Post
잔액 복구가 수수료 환불에 매달려 있었다