본문으로 건너뛰기
전체 글

Native Program은 거래의 변경안을 어떻게 검증할까?

· 약 9분
Bankware Global Engineering

A는 자산 100을 갖고 있고, S에게 그중 30까지 사용할 수 있도록 승인했다. S는 이 권한으로 20을 B에게 보내려고 한다. 거래가 끝나면 A에게 80, B에게 20이 남고, S가 앞으로 사용할 수 있는 한도는 10이 되어야 한다.

니고의 Native Program 경로에서는 거래를 구성하는 쪽이 이 변경안을 먼저 준비한다. 프로그램은 전달받은 변경안이 사용 조건을 지키는지 검사한다. 하나의 원장, 세 가지 거래 실행 방식에서는 이 방식을 Direct Cell, EVM과 비교했다. 여기서는 Native Program의 검증 안쪽으로 들어가 보자.

프로그램이 단순히 “허용한다”고 답하면 충분할까? 그 답이 A의 어떤 자산 입력을 쓰도록 허용했는지, 어떤 승인 기록을 무엇으로 바꾸도록 허용했는지가 빠져 있다면, 원장은 그 승인이 어느 입력의 사용과 어떤 상태 변경에 적용되는지 확인할 수 없다. 사용 조건을 통과했다는 판단과 실제 바뀌는 기록을 연결하는 방법이 이 글의 질문이다.

변경안에는 자산과 승인 기록이 함께 들어간다

니고는 원장의 상태를 StateCell이라는 작은 기록 단위에 담는다. 이번 예제에는 자산의 소유자·수량을 담은 기록과, 프로그램이 관리하는 사용 승인 기록이 필요하다. A는 소유자, S는 승인받은 사용자이자 거래의 서명자, B는 자산을 받을 사람이다.

기록현재제안
A의 자산10080
B의 자산020
A가 S에게 허용한 사용 한도3010

여기서 자산은 원장이 종류와 수량을 직접 관리하는 Native Asset이다. 추가 발행·소각은 없고 수수료 금액은 0으로 가정한다. 표는 대상 자산과 승인 기록만 보여준다. 사용 한도 30은 별도로 예치한 자산이 아니므로, 자산 합계는 처음에도 나중에도 100이다. B의 0은 받을 자산이 없다는 뜻이며, 수량이 0인 입력 셀을 준비한다는 뜻은 아니다.

거래에는 소비할 기존 기록과 생성할 새 기록을 담는다. 이 예제에서는 A의 자산 100과 사용 한도 30의 기록을 입력으로 지정하고, A의 자산 80·B의 자산 20·사용 한도 10을 출력으로 제안한다. 서명자 S와 “A의 자산 20을 B에게 이전한다”는 프로그램 호출 정보도 함께 들어간다.

이 변경안을 만들려면 현재 사용할 수 있는 기록을 찾아야 한다. 지갑이나 SDK, 노드의 요청 구성 도구가 도울 수 있지만, 서명할 거래에는 구체적인 입력과 출력이 정해져 있어야 한다. Native Program은 이렇게 준비한 변경안의 타당성을 판단한다.

프로그램에 전달할 자료는 Core가 구성한다

S가 요청에 “A의 자산이 100이고 내 사용 한도가 30이다”라고 적었다고 해서 그 내용이 사실이 되지는 않는다. 어떤 입력을 사용할지 지정하는 것과, 그 입력의 현재 내용을 입증하는 것은 다른 일이다.

니고의 공통 원장 규칙을 집행하는 Core는 거래가 가리킨 입력을 현재 원장 상태에서 가져온다. 입력이 아직 사용할 수 있는 기록인지 확인하고, 서명자와 해당 자산의 정책 등 검증에 필요한 정보를 모은다. 출력은 거래가 제안한 새 기록이므로, 그 형식과 접근 범위를 검사하더라도 아직 원장의 상태가 된 것은 아니다.

프로그램이 받는 자료에는 다음이 함께 들어간다.

  • 현재 입력: A가 소유한 자산 100과, A가 S에게 허용한 한도 30의 실제 기록.
  • 제안된 출력: A의 자산 80, B의 자산 20, 남은 한도 10.
  • 호출과 실행 맥락: 서명자 S, 이전할 상대와 수량, 자산 정책, 체인·블록 정보와 수수료 등.

Core는 자산 정보를 프로그램이 읽기 쉬운 형태로 정리해서 전달할 때도 그 값이 실제 입출력 셀에서 나온 것인지 확인한다. 호출자가 별도로 적은 수량을 원장의 수량처럼 전달하지 않는다.

이 자료 묶음을 정해진 형식으로 표현한 것이 canonical context다. 같은 거래와 같은 실행 상태를 처리하는 노드들이 같은 바이트열을 구성하도록 형식과 순서를 정한다. 프로그램은 그 안에서 무엇이 현재 기록이고 무엇이 제안된 결과인지 구분해 검사할 수 있다.

반환하는 것은 허용한 입력과 상태 변경이다

검증을 맡는 프로그램, 즉 validator는 이 context를 받아 사용 조건을 확인한다. 예제에서는 승인 기록이 A의 자산에 관한 것인지, 승인받은 사람이 서명자 S인지, 사용량 20이 한도 30을 넘지 않는지 확인한다. 자산의 수신자와 수량이 호출 내용에 맞고, 남은 한도가 정확히 10인지도 검사한다.

이 검증의 공통 진입점은 validate다. 하나의 실행 시도에서 프로그램을 한 번 호출하고, 그 결과로 어떤 입력의 사용과 어떤 상태 변경을 허용했는지 돌려받는다. 재실행이 필요한 경우까지 통틀어 거래가 평생 한 번만 검증된다는 뜻은 아니다.

예제의 대상 자산과 승인 기록에만 설명용 번호를 붙여 보자. 실제 거래 전체의 형식은 생략하고, 검증 자료와 반환 결과가 어떻게 연결되는지 보여주는 목록이다.

입력 목록
0: A의 자산 100
1: 기존 사용 한도 30

출력 목록
0: A의 자산 80
1: B의 자산 20
2: 새 사용 한도 10

반환 결과
사용을 승인한 입력: [0, 1]
이 승인 기록의 변경:
입력 1 → 출력 2

실제 반환값은 이미 전달한 입출력 목록의 위치를 가리킨다. 서명자가 직접 소유하지 않은 입력의 사용을 승인할 때는 해당 입력의 위치를 돌려준다. 프로그램 상태를 바꿀 때는 어느 상태인지를 식별하는 논리 키와 소비할 입력·생성할 출력의 위치를 함께 돌려준다. 이 예제의 논리 키는 A와 S 사이의 사용 승인을 식별한다.

위 목록에서 입력 0은 A의 자산이고, 입력 1은 A가 부여한 승인 기록이다. 프로그램은 S가 이 둘을 사용할 수 있도록 허용하면서, 승인 기록은 입력 1에서 출력 2로 바뀐다고 선언한다. 따라서 반환 결과가 새 한도 10을 담은 셀을 따로 만들어 보내는 것은 아니다. 거래가 제안한 출력 중 어느 것이 새 승인 기록인지를 가리킨다. 프로그램은 그 출력의 내용을 검사했고, Core는 반환된 참조가 실제 그 기록에 대응하는지 확인할 수 있다.

성공 경로를 단순화한 그림이다. 검증 자료에 넣은 기록과 반환 결과가 가리킨 기록을 연결한 뒤, 원장 규칙까지 통과한 변경을 반영한다.

프로그램이 요청할 수 있는 권한에도 범위가 있다. 다른 사람의 입력 사용을 승인하거나 프로그램 상태 변경을 선언하려면, 설치된 프로그램에 그 기능을 허용하는 권한이 있어야 한다. 아무 프로그램이나 “A의 자산을 써도 된다”는 결과를 반환해 통과할 수 있는 구조는 아니다. Core는 해당 자산의 정책과 연결된 프로그램인지도 확인한다.

Core는 반환된 참조를 실제 원장 상태와 맞춘다

프로그램이 논리 키와 입출력 위치를 반환해도, 그 선언 자체가 원장 상태를 바꾸지는 않는다. Core는 그 위치에 실제 어떤 셀이 있는지, 해당 상태가 현재 원장에서 무엇을 가리키는지 대조한다.

예를 들어 프로그램이 “A가 S에게 준 승인을 갱신한다”고 반환했다면, 소비 대상으로 지정한 입력은 그 승인의 현재 기록이어야 한다. 출력도 같은 상태의 다음 기록이어야 하며, 호출한 프로그램이 관리하는 상태 영역에 속해야 한다. 다른 승인의 입력이나 거래에 존재하지 않는 출력을 가리키는 결과는 이 대조를 통과할 수 없다. 거래에 들어 있는 프로그램 상태 입출력과 반환한 변경 선언은 서로 빠짐없이 대응해야 한다.

여기서 두 질문을 구분해야 한다. “한도가 10이어야 하는가?”는 업무 규칙의 질문이고, “허용한 그 상태를 그 입력과 출력으로 바꾸는가?”는 원장 반영의 질문이다. Core는 승인 한도의 뺄셈을 모든 프로그램 대신 구현하지 않는다.

세 가지 잘못된 변경안을 보면 이 경계가 더 분명해진다. 다음은 검사 책임을 설명하기 위한 사례이며, 실제로 실행해 관찰한 오류 순서를 나열한 것은 아니다.

한도는 30인데 40을 사용한다면

S가 A의 자산을 60으로 줄이고 B에게 40을 주자고 제안했다고 하자. 자산 합계는 여전히 100이지만, 허용받은 30을 넘는다. 자산의 수량 보존만으로는 이 요청의 문제를 알 수 없다. 승인받은 사용자와 사용 한도의 의미를 아는 프로그램이 이 업무 조건을 검사해야 한다.

20을 보내고도 사용 한도를 30으로 남긴다면

자산은 A=80, B=20으로 맞추면서 한도 출력을 30으로 제안할 수도 있다. 이번에도 자산 합계는 맞는다. 그러나 이 변경을 허용하면 같은 승인을 반복해서 사용할 수 있다. 프로그램은 이전할 수량과 새 한도의 관계를 검사해 이 제안을 거부해야 한다.

한편 프로그램이 승인 기록의 갱신을 반환했는데 그 참조가 다른 상태를 가리키거나, 거래의 실제 승인 입출력과 맞지 않는 문제는 Core가 검사한다. 프로그램이 한도 계산을 잘못 구현해 잘못된 출력까지 허용하고, 반환 참조도 그 출력과 일치한다면, Core의 일치 검사만으로 그 업무 오류를 알아낼 수는 없다. 검증 결과와 실제 변경을 묶는 구조에도 프로그램 자체의 정확성은 필요하다.

A의 80과 B의 25를 만든다면

입력 자산은 100인데 출력 합계가 105가 된다. 이번 예제에는 추가 발행이 없으므로, 이 변경은 Core가 집행하는 자산별 수량 보존 규칙을 위반한다. 프로그램이 잘못 허용하더라도 그 결과만으로 자산 5를 더 만들 수는 없다. 정상 프로그램도 이런 제안을 거부할 수 있지만, 공통 원장 규칙의 책임이 프로그램에 넘어가는 것은 아니다.

이처럼 두 검사는 맡은 질문이 다르다. 프로그램은 그 업무에서 허용할 수 있는 변경인지 판단하고, Core는 그 판단이 실제 기록에 정확히 대응하며 원장의 공통 규칙을 지키는지 확인한다. 검사를 통과한 뒤에야 입력을 소비하고 출력을 반영하므로, 정상 예제에서는 자산의 이전과 한도 감소가 함께 남는다.

실패도 같은 종류로 처리하지 않는다. 프로그램이 실행 중 거절을 뜻하는 revert를 하거나 실행량 한도인 gas를 소진하면 업무 변경은 반영하지 않고, 별도의 수수료 정산과 실패 실행 결과를 남긴다. 반면 반환 형식·허용 권한·실제 상태 변경의 일치 등 프로토콜 규칙을 위반한 결과는 거래 자체를 무효로 처리한다. 프로그램 실행 실패로 정산하는 경로와 구분하는 이유다.

필요한 상태를 전달받는 범위 안에서 검증한다

이 구조에서 프로그램이 검증 도중 임의의 원장 상태를 더 읽거나 다른 컨트랙트를 호출할 수 있다면 어떻게 될까? 거래가 처음 제시한 자료만으로는 실제 판단에 어떤 상태가 쓰였는지 알기 어려워진다. 그 호출이 다른 상태까지 바꿀 수 있다면 입력·출력으로 드러난 변경 범위도 달라진다.

그래서 Native Program은 필요한 원장 상태를 입력·출력과 Core가 구성한 맥락으로 전달받는다. 임의의 컨트랙트 저장소를 읽고 쓰는 명령이나 외부 컨트랙트 호출은 허용하지 않는다. 승인 검증에 새로운 정책 기록이 필요하다면, 그 기록을 검증 자료로 전달하는 구조부터 마련해야 한다.

Native Program은 Solidity와 제한된 EVM 환경을 재사용해 프로그램별 업무 규칙을 작성하고 실행한다. 일반 Ethereum 컨트랙트의 저장소 접근 방식까지 가져오는 것은 아니며, 검증할 상태는 앞의 자료 묶음으로 전달한다.

이 선택에는 거래를 구성하는 쪽의 준비가 필요하다. A의 자산 기록과 S의 승인 기록을 찾고, 사용 후 자산과 한도를 맞춰 출력으로 제안해야 한다. 대신 검증이 의존할 상태와 바꿀 상태를 원장이 사전에 파악할 수 있는 기반을 얻는다.

명시한 변경에서 다음 질문으로

처음의 거래로 돌아가 보자. S가 제안한 것은 자산 100을 80과 20으로 나누고, 사용 한도를 30에서 10으로 줄이는 변경이었다. Core가 실제 현재 기록을 검증 자료로 구성하고, 프로그램은 승인 조건과 새 한도를 검사한다. 반환 결과는 허용한 입력과 승인 기록의 변경을 가리키며, Core는 이 참조를 실제 기록과 맞추고 자산별 수량 보존 같은 공통 규칙을 확인한다.

Native Program의 검증은 이 연결로 완성된다. 무엇을 허용했는지 명시하고, 원장이 그 허용 대상과 실제 반영할 변경을 일치시키는 것이다.

이제 변경 범위를 미리 안다는 성질에서 다음 질문이 나온다. 서로 다른 자산 입력과 승인 기록을 사용하는 거래라면 동시에 검증해도 같은 원장이 될까? 명시적인 접근 범위는 그 판단의 출발점이지만, 시스템 상태를 바꾸는 작업이나 서로 의존하는 거래까지 모두 독립적으로 만들지는 않는다. 다음에는 어떤 변경을 함께 실행할 수 있고, 실행이 끝나는 순서가 달라도 어떻게 같은 결과를 남길 수 있는지 살펴보려 한다.