자산의 규칙을 바꿀 때 기존 승인은 어떻게 될까
A는 자산 100을 갖고 있었고, S에게 그중 30까지 사용할 수 있도록 승인했다. S가 20을 B에게 보낸 뒤에는 A에게 80, B에게 20이 남았다. S가 앞으로 사용할 수 있는 한도는 10이다. Native Program의 검증을 다룬 글에서는 자산의 이전과 한도 감소를 하나의 거래로 검사하는 과정을 살펴봤다.
이번에는 그 거래가 끝난 뒤, 자산의 사용 규칙을 검사하는 프로그램이 바뀐다고 하자. 새 프로그램도 S에게 남은 10을 사용할 수 있게 해줘야 할까? 한동안 다른 프로그램을 쓰다가 처음 프로그램으로 돌아온다면, 그때의 승인도 다시 유효해질까?
현재 nigo-protocol은 같은 프로그램의 실행 버전인 리비전을 바꿀 때 기존 상태를 유지하고, 다른 프로그램으로 교체할 때는 새 상태 영역을 연다. 모든 업그레이드에서 상태를 초기화하는 구조는 아니다. 그러나 기록을 유지했다고 사용자의 승인 의미까지 유지되는 것은 아니고, 기록을 새로 시작한다고 모든 사용 조건이 더 엄격해지는 것도 아니다.
규칙이 바뀔 때 기존 상태의 연속성을 어디까지 보장할 수 있는가? 이 글에서는 원장이 집행하는 상태의 경계와, 프로그램을 바꾸는 쪽이 검토해야 하는 업무 의미를 구분한다.
남은 10은 누구의 약속인가
nigo-protocol에서 자산은 소유자와 수량을 담은 기록으로 남는다. 다른 사람이 대신 사용할 수 있는 한도는 프로그램이 관리하는 별도 기록이다. 한도 10이 남아 있어도 S가 자산 10을 소유한 것은 아니다. A의 자산 80 중에서 조건에 맞게 10까지 사용할 수 있다는 뜻이다.
자산의 거래 조건을 검사하는 Native Program을 Controller라고 부르자. 여기서는 표준적인 사용 한도 규칙을 따르는 Controller P가 A의 승인을 관리한다고 가정한다. 승인 기록에는 누구의 자산을 누가 얼마나 사용할 수 있는지에 관한 의미가 담겨 있다. 이 의미를 해석하고 거래를 허용하는 쪽이 P다.
이 글은 사용자가 발행한 자산인 Custom Native Asset 중 특정 프로그램과 실행 버전을 지정해 사용하는 경우를 다룬다. 정책을 바꾸는 동안 추가 이전·발행·소각은 없고 수수료는 0으로 가정한다. 따라서 아래 예제에서 별도 사용 거래가 나오기 전까지 잔액은 A=80, B=20, 합계 100으로 유지된다.
프로그램의 고유 식별자를 Program ID라고 한다. 변경할 수 있는 대상을 세 가지로 나누면, 무엇을 이어받을지 더 분명해진다.
| 대상 | 달라지는 점 | 승인 기록 |
|---|---|---|
| 운영자 | 운영 권한의 담당자 | 유지 |
| P의 리비전 | 같은 ID의 실행 버전 | 구조 조건 확인 후 유지 |
| P → Q | 다른 ID의 프로그램 | 새 영역에서 시작 |
이 표는 기록을 보존하는 기준이다. 기록이 남는다고 해서 어떤 상황에서도 거래가 허용되는 것은 아니다. 실제 사용 시점에는 자산의 현재 상태와 프로그램의 검증 조건도 통과해야 한다.
운영자를 바꿔도 사용자의 승인이 넘어가지는 않는다
먼저 자산을 운영하는 담당자만 바뀌었다고 하자. 운영 권한을 맡은 키는 발행 같은 관리 업무에 쓰인다. 이 키를 새 담당자에게 넘기는 것과 A가 S에게 자산 사용을 허용하는 것은 서로 다른 권한 변경이다.
운영자 교체는 A의 자산을 새 운영자의 자산으로 만들지 않는다. S에게 남은 한도 10을 새 운영자의 사용 한도로 바꾸거나, 그 기록을 지우지도 않는다. 관리 권한이 바뀌었다는 이유만으로 소유자가 부여한 권한까지 함께 넘겨서는 안 되기 때문이다.
여기에는 규칙 변경을 최종 승인하는 주체도 있다. nigo-protocol에서는 별도로 위임하지 않았다면 현재 운영자의 서명으로 정책을 바꾼다. 정책 승인권을 EVM의 거버넌스 계약에 위임했다면, 지정된 계약의 실행이 변경을 승인해야 한다. 위임한 뒤에는 운영자 서명만으로 정책을 직접 바꿀 수 없다. 거버넌스 계약은 여러 사람의 동의나 대기 시간 같은 자체 조건을 둘 수 있다.
이 승인 경로가 답하는 질문은 “누가 P를 바꿀 수 있는가”다. A의 사용 승인이 답하는 “누가 내 자산 10을 대신 쓸 수 있는가”와는 다르다. 정책 변경의 승인을 받았더라도, 기존 사용자 승인을 어떤 상태로 남길지는 별도로 정해야 한다.
같은 ID의 리비전 변경은 기존 상태를 유지한다
P의 버그를 고치거나 검증 로직을 개선해 새 버전을 만들었다고 하자. nigo-protocol은 프로그램을 식별하는 Program ID와 실행할 리비전을 구분한다. 이 글의 자산은 P의 특정 버전을 지정하고, 그 버전의 코드와 실행 조건을 특정하는 설명자의 해시까지 고정한다. 이렇게 실행 대상을 정확히 지정하는 것을 pin이라고 한다. 새 버전이 게시되고 활성화돼도 자산의 pin은 자동으로 따라가지 않는다. 정책 승인권자가 어느 버전을 사용할지 명시적으로 바꿔야 한다.
같은 Program ID 안에서 실행 버전을 바꿀 때는 기존 프로그램 상태를 보존한다.
현재 Controller 변경 규약은 이 변경을 허용하기 전에 이전·새 버전이 선언한
상태 구조들의 묶음, 즉 schemaSetRoot가 같은지 검사한다. 대상 버전이 설치되어 있고,
실행 시점에 사용할 수 있으며 필요한 권한을 갖췄는지도 확인한다.
하지만 구조가 같다는 사실만으로 의미까지 같아지지는 않는다. 예를 들어 두 버전이 모두 정수 10을 읽더라도, 하나는 “앞으로 쓸 수 있는 양”으로, 다른 하나는 “이미 쓴 양”으로 해석할 수 있다. 같은 기록을 찾는 키의 계산법이 달라져도 문제가 된다. 원장이 구조를 대조하는 것만으로 이런 업무 의미의 변화를 알아낼 수는 없다.
Program ID도 코드의 의미가 같다는 증명은 아니다. 같은 ID의 새 리비전이 한도 검사를 다르게 구현할 수 있고, 반대로 다른 ID에 동일한 코드를 설치할 수도 있다. ID는 상태를 어느 프로그램에 연결할지 정하는 관리 경계다. 기존 키와 상태의 의미가 새 코드에서도 유지되는지는 프로그램과 자산의 변경을 승인하는 쪽이 검토해야 한다.
이를 지킨 P의 새 리비전이라면 남은 한도 10을 이어 사용할 수 있다. 버전 변경 때마다 재승인하지 않아도 되는 대신, 승인 기록을 해석하는 코드의 변경 권한에 대한 신뢰가 필요하다. 사용자가 처음 승인한 코드가 영원히 고정되거나, 새 코드가 같은 승인 범위만 허용한다는 보장은 아니다.
이전 코드로 돌아가는 것과 이전 상태로 돌아가는 것은 다르다
호환되는 리비전 2에서 같은 승인 기록의 한도가 10에서 5로 줄었다고 하자. 사용 가능한 리비전 1로 다시 바꾸더라도 그 프로그램은 현재 기록의 5를 읽어야 한다. 코드를 이전 버전으로 돌리는 롤백은 상태를 과거의 10으로 복구하는 작업이 아니다.
그런데 리비전 사이에 승인 기록을 찾는 키를 바꾸면 이 연속성이 깨질 수 있다. 다음은 남은 한도 10에서 출발하는 별도의 가상 사례다. 업무 호환성 검토가 필요한 이유를 보이는 반례이며, 현재 표준 Controller에서 관측한 오류를 뜻하지 않는다.
- 리비전 1은 A와 S의 승인 기록을 K1이라는 키로 찾는다. K1의 남은 한도는 10이다.
- 리비전 2는 같은 승인을 K2라는 다른 키로 관리하면서 K1을 그대로 남긴다.
- A가 리비전 2에서 S에게 새로 5를 승인했다가 취소한다. K2의 한도는 0이지만 K1의 10은 바뀌지 않는다.
- 다시 리비전 1을 사용하면 K1의 한도 10을 읽을 수 있다. A는 승인을 취소했다고 생각하지만 옛 기록이 다시 쓰인다.
이 사례에서는 자산을 이전하지 않았으므로 잔액은 A=80, B=20이다. 문제는 저장된 두 기록의 형식이 같아도, 새 코드에서 한 취소가 옛 코드의 승인에 반영되지 않는다는 데 있다. 같은 ID의 리비전 변경은 상태 영역을 유지하므로 원장의 영역 검사만으로 이를 막을 수 없다. 리비전 변경을 검토할 때는 앞으로 사용할 코드뿐 아니라, 되돌아갈 코드가 그동안 바뀐 현재 상태를 어떻게 해석하는지도 확인해야 한다.
다른 프로그램으로 교체하면 새 상태 영역을 연다
P 대신 다른 Program ID를 가진 Q를 사용한다면 경계가 달라진다. 현재 nigo-protocol의 Controller 교체는 RESET, 즉 새 프로그램 상태 영역에서 시작하는 방식이다. P의 남은 한도 10을 Q가 자동으로 이어받지 않는다.
이 규칙은 P와 Q의 업무 의미가 반드시 다르기 때문에 적용되는 것은 아니다. 동일한 코드를 쓰더라도 Program ID가 다르면 현재 규약은 RESET을 요구한다. 원장이 두 프로그램의 상태 의미가 동등한지 자동으로 증명하는 대신, 다른 ID에는 옛 상태를 자동으로 넘기지 않는 경계를 택한 것이다.
교체 직후에도 A의 자산 80과 B의 자산 20은 그대로다. 새로 시작하는 것은 Controller가 관리하는 업무 상태다. 잔액과 공급량을 초기화하는 작업이 아니다. 표준적인 승인 기반 사용을 계속하려면 A가 새 Controller 아래에서 S에게 다시 권한을 줘야 한다. 이 글의 한도 외에 계정 허용 목록 같은 상태를 관리하던 Controller라면, 그 상태도 새 영역에 다시 마련해야 한다.
RESET은 이전 기록을 새 형식으로 변환하는 이관 기능도 아니다. 표준 승인 모델에서는 사용자가 다시 승인해야 하고, 운영자는 필요한 정책 상태를 준비해야 한다. 이렇게 재구성하는 비용을 들여 옛 상태의 자동 승계를 끊는다.
상태가 없으면 언제나 권한이 줄어들까
사용 한도는 기록이 없으면 사용할 권한도 없도록 설계할 수 있다. 그러나 프로그램 상태에는 허용뿐 아니라 제한도 담긴다. 가령 기간당 100까지 허용하는 프로그램이 이미 사용한 양 90을 저장하고 있었다고 하자. 교체 뒤 새 프로그램이 기록 부재를 사용량 0으로 해석하면, 같은 기간에 남은 한도가 10에서 100으로 늘어난다. 이것은 RESET의 영향을 설명하기 위한 가상 정책이며 앞의 A·S 잔액 예제와는 별개다.
동결 기록이 없다는 이유로 거래를 허용하는 정책도 비슷한 문제가 생길 수 있다. 현재 nigo-protocol의 Regulated Controller는 계정 정책 기록이 없으면 일반 자산 거래를 거부한다. 따라서 RESET 직후 기록 부재를 동결 해제로 해석하지 않고, 필요한 계정 정책을 다시 등록해야 한다. 이 동작은 Regulated의 업무 규칙이며 모든 새 Controller에 자동으로 적용되는 성질은 아니다.
새 Controller를 검토할 때는 어떤 기록을 다시 만들지와 함께, 초기 상태에서 무엇을 허용하고 어떤 제한이 사라지는지를 확인해야 한다. RESET은 옛 기록이 현재 기록으로 재사용되는 것을 막는다. 새 코드가 사용자의 동의 범위와 기존의 제한을 지키는지까지 보장하려면 별도의 업무 규칙과 변경 승인 절차가 필요하다.
P로 돌아와도 옛 한도가 되살아나면 안 된다
다른 ID로 교체할 때 옛 기록의 사용을 끊기로 했다면, 처음 ID로 돌아올 때도 그 경계가 유지되어야 한다. 상태 영역을 프로그램 ID만으로 구분하면 한 가지 문제가 남는다. P를 Q로 바꿨다가 다시 P로 돌아온 상황을 생각해 보자. P의 옛 승인 기록을 지우지 않았다면, 돌아온 P가 그 기록을 다시 현재 승인으로 사용할 수 있다. 교체 때 끊기로 한 S의 예전 한도 10이 되살아나는 셈이다. 앞의 같은 ID 안에서 리비전 1→2→1로 바꾸는 사례와 달리, 여기서는 다른 ID를 거친 P→Q→P 교체를 다룬다.
이를 막으려면 같은 P라도 어느 시기의 상태인지를 구분해야 한다. nigo-protocol은 Controller 상태에 epoch, 즉 상태의 세대를 붙인다. 프로그램이 승인 기록을 찾는 논리 키를 내놓으면, 원장의 공통 규칙을 집행하는 Core는 현재 자산과 세대 정보를 함께 사용해 실제 상태의 식별자를 계산한다. 프로그램의 ID도 그 식별자에 포함된다.
따라서 “A가 S에게 준 승인”이라는 논리 키가 같아도 자산·프로그램·세대가 다르면 다른 기록이다. Core는 실제 입력과 출력이 현재 영역의 기록인지 대조하므로, 프로그램이 옛 키를 반환한다고 해서 이 경계를 생략할 수는 없다.
다음 표에서는 시작 시 정책 변경 번호와 상태 세대가 모두 0이고, 다른 정책 변경 없이 P→Q→P 교체만 이어진다고 가정한다. 두 번의 교체로 세대가 각각 1과 2가 된다.
| 시점 | 사용하는 상태 영역 | S의 승인 상태 |
|---|---|---|
| 첫 P 사용 | P · 세대 0 | 남은 한도 10 |
| Q로 교체 | Q · 세대 1 | 새 승인 없음 |
| P로 다시 교체 | P · 세대 2 | 새 승인 없음 |
| A가 S에게 5를 새로 승인 | P · 세대 2 | 남은 한도 5 |
마지막 P는 처음과 같은 Program ID여도 새로운 세대의 상태를 사용한다. 세대 0의 한도 10과 세대 2의 한도 5를 합쳐 15로 만들지 않는다. 이제 S가 새 승인으로 5를 B에게 보내면 A=75, B=25가 되고 새 한도는 0이 된다. 자산 합계는 여전히 100이며, 옛 한도 10은 이 거래에 쓰이지 않는다.
현재 규약에서 새 세대 값은 교체 직후의 자산 정책 변경 번호인 policyRevision을 사용한다.
다른 정책 변경이 사이에 있으면 세대 번호는 0, 1, 2처럼 연속하지 않을 수 있다.
중요한 것은 예전 세대의 번호를 재사용하지 않는다는 점이다.
같은 프로그램의 버전 변경이나 운영자 교체는 정책 변경 번호를 증가시키지만 상태 세대는 유지한다.
두 번호가 맡은 일도 다르다. 정책 변경 번호는 과거에 준비한 변경 승인을 다시 실행하지 못하게 한다. 요청이 예상한 번호가 현재 번호와 일치해야 하고, 성공하면 번호가 증가한다. 상태 세대는 현재 프로그램이 사용할 수 있는 업무 기록의 영역을 정한다. 정책 변경 번호는 오래된 변경 요청의 재실행을 막고, 상태 세대는 교체 전 세대의 기록을 현재 기록으로 다시 사용하는 것을 막는다. 세대를 유지하는 리비전 변경의 업무 호환성까지 이 장치가 대신 보장하지는 않는다.
옛 세대의 기록은 RESET만으로 물리 삭제되지 않는다. 저장소에 기록이 남아 있다는 사실과 현재 거래에서 사용할 수 있다는 사실을 구분하는 것이다. 그 기록의 보존 비용과 삭제 시점은 별도의 저장소 정책이 다룬다.
규칙 변경도 원장에서 순서가 필요한 거래다
남은 한도 10에서 출발했지만, 결과는 변경의 종류에 따라 달랐다. 운영자 교체는 사용자가 준 권한을 가져오지 않는다. 같은 ID의 리비전 변경은 기록을 보존하며, 그 의미와 롤백 호환성은 별도로 검토해야 한다. 다른 ID로 교체하면 새 세대를 열어 옛 영역을 격리하지만, 새 영역의 초기 상태에서 어떤 권한과 제한이 적용될지는 새 정책이 결정한다.
기록을 보존하거나 격리하는 규칙과, 그 기록을 해석하는 코드의 정확성이 함께 있어야 상태의 연속성을 판단할 수 있다.
이 구분은 거래 실행 순서에도 영향을 준다. S가 남은 한도를 쓰는 거래와 P를 Q로 교체하는 거래가 함께 들어왔다면, 사용 거래가 어떤 정책과 세대 아래에서 검증되는지 정해져야 한다. 승인 기록이 다르다는 이유만으로 정책 변경까지 독립적인 작업으로 취급할 수는 없다.
동시에 실행해도 같은 원장이 되려면에서는 이 질문을 여러 거래로 넓힌다. 함께 실행할 수 있는 거래를 구분하려면 각 거래의 입력뿐 아니라, 그 거래가 의존하는 정책과 상태의 범위도 알아야 한다.