본문으로 건너뛰기
전체 글

실패한 거래에서 무엇은 취소되고 무엇은 남을까

· 약 7분
Bankware Global Engineering

송금 화면에 “실패”가 표시됐다. 받는 사람의 잔액은 늘지 않았다. 그렇다면 보낸 사람의 수수료도 그대로이고, 같은 서명 거래를 다시 보내면 처음부터 실행할 수 있을까?

실패했다는 사실만으로는 답할 수 없다. 실행할 거래를 준비하지 못한 것과, 원장이 거래를 실행한 뒤 실패로 기록한 것은 다르기 때문이다. 실행 중 실패했더라도 어느 호출까지 취소됐는지에 따라 남는 효과가 달라진다.

잔액은 충분한데 작은 송금도 실패했던 사례에서는 사용할 입력을 준비하는 과정이 문제였다. 이번에는 그다음 질문을 살펴본다. 거래가 실패하면 자산 변경, 이벤트, 수수료와 같은 거래의 재사용을 막는 기록은 각각 어떻게 되는가?

실패한 위치부터 찾기​

A가 자산 X를 100개 가지고 있고 B는 0개라고 하자. A는 B에게 20 X를 보내려 한다. X는 수수료용 NIGO와 다른 자산이며, 추가 발행과 소각은 없다. 성공하면 A는 80 X, B는 20 X가 된다.

앞 글과 같은 이름을 사용하지만 각 실패 사례는 A=100, B=0인 새 초기 상태에서 시작한다. 앞서 실행한 거래에 이어서 잔액을 계산하지 않는다.

지갑이 적절한 입력을 찾지 못해 서명할 거래를 만들지 못했다면, 원장은 아직 이 송금을 실행하지 않았다. 제출한 거래라도 서명이 잘못됐거나 이미 소비된 입력을 참조해 유효성 검사를 통과하지 못하면 거절된다. 이 거절 자체로 자산을 옮기거나 수수료를 정산하지 않는다.

반면 기본 검사를 통과해 실행 대상이 된 뒤, 프로그램이 업무 조건을 이유로 거절할 수도 있다. 이때의 실패는 처리 결과로 원장에 남길 수 있는 실패다. 아래에서 수수료나 재사용 방지 효과가 남는다고 할 때는 이런 결과가 블록에 포함되어 확정된 경우를 말한다. 임시 실행을 한 번 시도했다는 이유만으로 비용이 확정되는 것은 아니다.

마지막 조건이 거절하면, 앞의 성공도 확정되지 않는다​

이번 송금은 Native 프로그램의 정책 검증을 거쳐야 한다고 하자. 자산을 직접 보내는 조건과 정책을 거치는 조건은 통합 자산 모델을 설명한 글에서 구분했다. 여기서는 정책이 거절한 송금을 다른 경로로 우회하지 않는다.

정책은 송금 업무를 확인하는 부분과 추가 조건을 검사하는 부분으로 나뉠 수 있다. 조합형 정책에서는 각각 ACTION과 GUARD라고 부른다. ACTION이 20 X 전송을 허용하는 결과를 반환하고 전송 이벤트까지 만들었는데, 마지막 GUARD가 수신 조건을 이유로 거절하는 상황을 생각해 보자.

이벤트가 만들어졌다는 이유로 B에게 먼저 20 X를 주어서는 안 된다. Native 정책의 실행 결과는 필요한 검증을 모두 통과하기 전까지 최종 자산 변경이 아니다. 마지막 검사가 실패하면 앞서 나온 허용 결과와 이벤트도 채택하지 않는다.

따라서 최종 X 잔액은 A=100, B=0이다. 전송 성공 이벤트도 남지 않는다. 대신 이 실행을 실패로 기록한 receipt, 즉 거래 처리 결과는 남을 수 있다. “성공한 송금이 없다”와 “거래 처리 기록이 없다”는 서로 다른 말이다.

실제 회귀 테스트도 이 차이를 겨냥한다. ACTION이 로그를 실제로 만든 뒤 마지막 GUARD가 거절하거나 실행 예산을 소진하는 상황에서, 앞선 로그와 일부 업무 효과가 결과에 섞여 남지 않는지 검사한다. 테스트의 업무 대상은 별도의 상태 변경이며, 위의 100 X 송금은 경계를 설명하기 위한 예제다.

수수료가 0이어도 같은 서명은 다시 쓸 수 없다​

X 잔액이 원래대로라고 해서 거래 처리에 필요한 모든 상태가 원래대로인 것은 아니다.

Native 거래는 업무에 사용할 입력과 별도로, 수수료 정산과 재사용 방지를 위한 소유자의 NIGO 입력을 지정한다. 재사용 방지란 이미 처리한 동일 서명 거래를 다시 적용하지 못하게 하는 것이다. 정책 실행이 실패한 경우에도 이 입력을 소비하고, 정해진 수수료를 뺀 잔돈을 새 입력으로 쓸 수 있는 Cell에 돌려준다.

수수료 정책이 ZERO인 경우를 보자. A가 이 용도로 10 NIGO를 담은 Cell 하나를 지정했다고 가정한다. 금액은 설명용이며 수수료는 0이다.

  • X는 전송되지 않으므로 A=100 X, B=0 X다.
  • 기존 10 NIGO Cell은 소비되고, A에게 10 NIGO를 담은 새 잔돈 Cell이 생긴다.
  • NIGO 총액은 그대로지만, 이미 소비된 예전 Cell을 가리키는 동일 서명 거래는 다시 적용할 수 없다.

유료 정책에서는 정해진 수수료만큼 잔돈이 줄어든다. 현재 Native 경로의 금전 수수료는 서명된 거래 데이터인 envelope의 크기 등을 반영한 비용을 기준으로 한다. receipt에는 프로그램의 실행량도 gas로 기록하지만, 그 전체 값에 단가를 곱해 수수료라고 해석하면 맞지 않는다.

이 구분에는 두 가지 이유가 있다. 업무 실패가 이미 사용한 실행 자원을 없애지는 않으며, 수수료를 받지 않는 체인에서도 같은 서명을 반복 적용하는 일은 막아야 한다. 업무의 취소와 거래 처리의 정산은 서로 다른 책임이다.

EVM에서는 내부 호출의 실패와 전체 실패를 나눈다​

Native에서 승인한 자산을 EVM에서 사용하는 예제를 짧게 이어 보자. 다시 A=100 X, B=0 X에서 시작한다. A가 업무 계약 P에 30 X의 사용을 승인한 별도 Native 거래는 이미 확정돼 있다. P는 이번 EVM 거래에서 그중 20 X를 B에게 보내려 한다.

여기에는 두 가지 실패 경계가 있다.

첫째, 자산을 다루는 내부 호출이 실패했지만 P가 그 실패를 처리하는 경우다. 사용 한도는 충분하지만 추가 지급 조건을 충족하지 못해 자산 호출이 거절됐다고 하자. P는 그 실패를 확인하고, 다른 자산 변경 없이 “지급 보류”를 기록한 뒤 정상 종료한다. 실패한 자산 호출의 변경과 이벤트는 사라진다. X 잔액은 100/0, 남은 승인은 30이다. 하지만 P의 지급 보류 기록은 전체 거래가 성공하면 남을 수 있다. 내부 실패가 언제나 바깥 거래 전체의 실패인 것은 아니다.

둘째, 자산 호출은 성공했지만 이후 P의 업무 전체가 되돌아가는 경우다. 내부에서는 잠시 80/20과 남은 승인 10이 계산됐더라도, P가 이후 조건을 충족하지 못해 REVERT하면 그 호출에 속한 변경과 이벤트를 함께 버린다. 최종 결과는 다시 100/0과 승인 30이다. 앞서 별도 거래로 확정했던 승인 30 자체를 취소하는 것은 아니다.

EVM의 실행 중 변경은 격리된 임시 상태에 쌓이고, 상위 호출이 성공해야 그 범위의 변경이 살아남는다. 실패를 처리한 상위 호출도 나중에 실패하면, 앞서 남기려던 지급 보류 기록까지 취소된다. 반대로 내부 호출이 실패했다는 이유만으로 상위 호출의 정상적인 후속 업무까지 무조건 취소하지는 않는다.

이 안에서 Native 자산 정책을 호출했다고 Native 거래 한 건이 별도로 확정되는 것도 아니다. 별도의 Native 수수료나 재사용 방지 입력을 추가하지 않으며, 바깥 EVM 거래가 비용과 처리 결과를 책임진다.

자산이 돌아와도 바깥 거래의 기록은 남는다​

EVM 거래 전체가 REVERT하거나 gas 한도를 소진해도, 업무 상태를 버리는 것과 바깥 거래의 처리 효과를 버리는 것은 다르다.

Ethereum 형식으로 서명한 거래는 계정의 nonce, 즉 다음 거래에 사용할 순번으로 재사용을 막는다. 유효한 거래가 실행된 뒤 실패로 확정됐다면 해당 순번의 소비와 적용되는 수수료는 남는다. NIGO 형식으로 서명한 EVM 거래는 같은 ETH 순번을 소비하는 대신 확정된 거래의 식별 정보로 재사용을 막는다. 모든 EVM 거래가 동일한 nonce 규칙을 쓰는 것은 아니다.

계정에 실행 코드를 위임하는 Type-4 거래에는 한 가지 경계가 더 있다. 유효하게 적용된 코드 위임 승인과 그에 따른 순번 변경은 뒤의 업무 호출이 실패해도 남을 수 있다. 여기서 말하는 승인은 X의 사용 한도 30과 다른, 계정 코드 위임에 관한 승인이다.

앞의 설명용 송금을 기준으로 보면 다음과 같다. EVM 두 행에는 별도 거래로 확정한 승인 30이 있다는 가정을 적용한다.

실패한 위치X의 최종 결과함께 살펴볼 기록
거래 준비 실패·실행 전 유효성 거절A=100, B=0이 거절 자체의 실행 수수료·성공 이벤트 없음
포함된 Native 거래의 정책 거절A=100, B=0전송 이벤트 없음. 실패 receipt와 NIGO 수수료·재사용 방지 효과
EVM 내부 자산 호출만 실패하고 상위는 성공A=100, B=0, 승인=30실패한 호출의 이벤트 없음. 상위의 다른 업무와 바깥 거래 처리 효과는 남을 수 있음
EVM 업무 전체가 되돌아감A=100, B=0, 승인=30업무 상태·이벤트 취소. 실패 receipt와 바깥 거래의 수수료·재사용 방지 효과

수수료가 있다는 표현이 항상 양수 금액을 뜻하지는 않는다. ZERO에서는 금전 수수료가 0이다. 유료 정책에서 정산하는 금액은 거래 형식과 실패 종류에 따라 달라진다. gas 한도를 소진한 경우까지 하나의 계산식으로 뭉뚱그리지 않고, 실행량과 금전 비용의 관계는 다음 글에서 더 살펴본다.

모든 오류를 정상적인 거래 실패로 기록하지는 않는다​

지금까지의 설명은 정해진 규칙으로 판정할 수 있는 업무 거절과 실행 한도 소진에 관한 것이다. 저장소 읽기나 내부 실행 기반에 문제가 생겼다면 이를 “사용자의 송금 실패”로 바꾸어 정상적인 실패 receipt를 만들면 안 된다. 그런 오류는 블록 실행의 문제로 전달하고, 확정되지 않은 임시 변경은 버려야 한다.

시뮬레이션도 구분해야 한다. 성공·실패와 예상 gas를 확인하더라도, 그 결과만으로 실제 잔액이나 재사용 방지 기록이 바뀌지는 않는다. 병렬 실행 중 검증을 통과하지 못해 버린 임시 시도 역시 원장에 포함된 실패 거래와 같지 않다.

이 글은 2026년 10월 19일 확인한 구현과 회귀 테스트의 경계를 설명한다. 상태 변경과 receipt의 영속 반영은 상위 블록 처리의 commit 경계에서 이뤄진다. 정책 검증 함수가 끝났다는 사실만으로 영속 저장까지 끝났다고 해석해서는 안 된다.

실패를 이해하려면 성공 여부 하나 대신 실패한 위치, 취소된 업무 범위, 확정 후 남은 거래 처리 효과를 함께 봐야 한다. 그래야 잔액이 돌아왔는데 순번이 바뀐 이유나, 수수료가 0인데도 같은 서명을 재사용할 수 없는 이유를 설명할 수 있다.

여기서 다음 질문으로 이어진다. 금전 수수료를 받지 않아도 실행량은 왜 계산하고 제한해야 할까? 다음 글에서는 gas로 측정하는 실행 자원과 NIGO로 정산하는 비용을 나누어 살펴본다.