위탁 지갑 연동에서 출금 취소 부분을 붙이고 있었다. 문서를 읽고 만들었는데 실행하면 오류가 계속 났다.
Table of contents
Open Table of contents
증상 — 같은 요청이 갈렸다
돌아오는 것은 이 코드였다.
INVALID_STATE
INVALID_STATE 가 어떤 건에서는 안 나고 어떤 건에서만 나서, 오류 자체보다 그 갈림이 문제였다.
거부된 건들을 모아 보니 우리 쪽 status 가 같았는데 무엇이 그 차이를 만드는지는 문서에 없었다.
원인 — 문답 자료에 있던 상태 규칙
문답 자료를 전수로 훑었다. 출금 관련만 스물 남짓이었다.
created (생성) → withdraw/cancel 가능
signing (서명 대기) → 오류
broadcast (전파됨) → 별도의 "withdraw/bump" 를 써야 함
취소가 created 에서만 허용되고 signing 에서는 withdraw/abort 라는 다른 창구를 불러야 했다. broadcast 뒤에는 취소가 아예 없고 수수료를 올려 대체하는 withdraw/bump 만 있었다.
표준 규격 문서에는 state 규칙이 있다고만 적혀 있었다. 어느 상태에서 어느 창구를 쓰는지도 그때 파라미터가 무엇인지도 없었다.
표준 문서는 정상 경로를 설명하고 문답 자료에는 실패 사례가 쌓인다. 둘을 다 읽어야 만들 수 있다는 것이 여기서도 같았다.
조치 — 분기와 명시적 예외
switch 로 상태별로 부를 창구를 나눴다.
switch ($withdrawal->custodyState) {
case 'created':
return $this->custody->call('withdraw/cancel', ['id' => $withdrawal->custodyId]);
case 'signing':
return $this->custody->call('withdraw/abort', ['id' => $withdrawal->custodyId]);
default:
throw new InvalidStateException($withdrawal->custodyState);
}
default 에서 InvalidStateException 을 던지므로 목록에 없는 상태가 조용히 지나가지 않는다. broadcast 를 취소 경로에 넣지 않은 것도 그래서다.
그러지 않으면 새 상태가 생겼을 때 아무 처리도 안 되고 그것을 모른 채 지나간다. 조용히 넘기는 대신 멈추는 쪽을 골랐다. 이유는 예상 못 한 값에서 멈추면 나중에 조사할 시간이 크게 줄어서다.
분기를 만들 때 나머지 경우를 어떻게 할지가 분기 자체만큼 중요했다.
주의 — 성공이 반영이 아니었다
문답 자료에서 다른 것도 나왔다.
API 성공 → 즉시 반영 아님
withdraw/abort 의 성공을 받아도 상태 조회에 반영되는 것은 다음 서명 라운드가 끝난 뒤라 몇 분이 걸린다.
보내고 나서 바로 확인하면 아직 signing 이라, 안 됐나 싶어 다시 보내면 두 번째 abort 가 INVALID_STATE 로 튕긴다.
확인 시점을 뒤로 미루고 cron 이 몇 번 다시 보게 고쳤는데 성공과 반영 사이의 시차를 전제로 넣어야 했다.
비교 — 이름이 비슷한 필드들
노드 쪽 조회에는 비슷한 이름이 둘 있었다.
walletconflicts 이 트랜잭션과 충돌한(대체한) txid 목록
bip125-replaceable 대체될 수 있는지 여부 (yes / no / unknown)
walletconflicts 는 결과이고 bip125-replaceable 은 가능 여부라 잘못 읽기 쉽다. Bitcoin Core 0.16 의 gettransaction 공식 문서는 bip125-replaceable 이 mempool 에 없는 미확인 트랜잭션에서는 unknown 일 수 있다고 적는데, 그 값을 no 로 읽으면 대체 가능한 건을 고정된 건으로 오판한다.
이름만으로 판단하면 반드시 한 번은 틀리므로, 실제 값을 몇 건 받아 우리 자료와 대조해 의미를 확정하고 표로 적어 뒀다.
수수료 인상 대체에는 부작용도 있었다.
처음 txid A 로 전파
bump 후 txid B 로 대체, A 는 walletconflicts 에 남음
대체된 트랜잭션의 txid 가 바뀌므로 txid A 로 추적하면 못 찾는다.
제약 — 되돌릴 수 없는 처리와 완료 불가
가장 무거운 것은 이것이었다.
일단 broadcast 가 되면 취소로 못 돌아가고, 실수로 서명 라운드를 넘기면 그 건은 나가는 것으로 고정된다.
그래서 그런 처리 앞에는 확인 단계를 두고 대상 목록을 먼저 보게 했다.
내부 주소로 등록 → confirmed 로 완료 처리 불가
↓
온체인 없이 settled 로 종료
위탁 지갑 안의 내부 주소로 보낸 출금은 온체인 트랜잭션이 없어 confirmed 가 영영 안 되는 것도 있었다.
confirmed 는 안 되는데 잔액은 이동하니, 이것을 모르면 완료가 안 된다고 계속 시도하게 되고 그 시도는 영원히 성공하지 않는다.
구현 전에 알아 둔 것을 목록으로 적어 뒀다.
① 상태별로 API 가 다르다 → 분기 필요
② 성공이 즉시 반영이 아니다 → 확인 시점 조정
③ 식별자가 변경된다 → 추적 방식 변경
④ 비가역 처리가 있다 → 사전 확인
⑤ 특정 유형은 완료 불가 → 예외 처리
다섯 다 오류를 만나 가며 알아내려면 며칠이 걸린다. 문답 자료 수백 건 중 출금 섹션 스물 남짓만 읽는 데는 반나절이면 됐다.
정리
- 상태에 따라 부를 창구 자체가 다를 수 있다
- 표준 문서에 없고 문답 자료에만 있는 규칙이 있다
- 표준 문서는 정상 경로이고 문답 자료는 실패 사례다
- 분기에 명시적 예외를 두어 예상 못 한 상태를 바로 안다
- 나머지 경우를 어떻게 할지가 분기 자체만큼 중요하다
- 성공이 즉시 반영을 뜻하지 않는다
- 확인 시점을 안 미루면 중복 전송이 된다
- 비슷한 이름의 필드는 실제 값으로 의미를 확정하고 기록한다
- 대체 처리에 식별자 변경 부작용이 있을 수 있다
- 되돌릴 수 없는 처리에는 확인 단계를 둔다
- 완료가 불가능한 유형을 알아야 시도를 멈춘다
- 필요한 섹션만 골라 읽으면 며칠이 반나절이 된다