사용자 정의 업그레이드 정책
단일 키로 온체인 패키지를 업그레이드할 수 있는 권한을 보호하는 것은 여러 보안 위험을 초래할 수 있다.
- The entity owning that key might make changes that are in their own interests but not the interests of the broader community.
- Upgrades might happen without enough time for package users to consult on the change or stop using the package if they disagree.
- The key might get lost.
이러한 단일 키 업그레이드의 보안 위험을 해결하면서도 패키지 업그레이드를 가능하게 하기 위해, Sui는 커스텀 업그레이드 정책을 제공한다. 이러한 정책은 UpgradeCap 액세스를 보호하고 케이스별로 업그레이드를 승인하는 UpgradeTicket object를 발행한다.
compatibility
Sui는 내장된 패키지 호환성 정책 세트를 제공하며, 가장 엄격한 것부터 가장 느슨한 것까지 다음과 같이 나열된다:
| Policy | Description |
|---|---|
| Immutable | 누구도 패키지를 업그레이드할 수 없다. |
| Dependency-only | 패키지의 의존성만 수정할 수 있다. |
| Additive | 패키지에 새로운 기능(예: 새로운 public 함수 또는 구조체)을 추가할 수 있지만 기존 기능(예: 기존 public 함수의 코드)은 변경할 수 없다. |
| Compatible | 가장 느슨한 정책이다. 더 제한적인 정책이 허용하는 것에 더하여, 패키지의 업그레이드 버전에서:
|
나열된 순서대로, 각 정책은 이전 정책들의 허용 기준이 누적되어 통합된 형태다.
패키지를 게시할 때 기본적으로 가장 느슨한 compatible 정책을 채택한다. transaction이 성공적으로 완료되기 전에 정책을 변경할 수 있는 transaction의 일부로 패키지를 게시할 수 있으며, 이를 통해 기본 정책이 아닌 원하는 정책 수준으로 패키지를 체인에 처음 사용 가능하게 만들 수 있다.
패키지의 UpgradeCap에 대해 sui::package의 함수 중 하나(only_additive_upgrades,only_dep_upgrades, make_immutable)를 호출하여 현재 정책을 변경할 수 있으며, 정책은 더 제한적으로만 변경될 수 있다. 예를 들어, sui::package::only_dep_upgrades 를 호출하여 정책을 additive로 제한한 후, 동일한 패키지의 UpgradeCap에 대해 sui::package::only_additive_upgrades를 호출하면 오류가 발생한다.
upgrade 개요
패키지 업그레이드는 단일 transaction 내에서 종단 간으로 발생해야 하며 세 가지 명령으로 구성된다:
- Authorization: 업그레이드를 진행하기 위해
UpgradeCap으로부터 권한을 얻어UpgradeTicket을 생성한다. - Execution:
UpgradeTicket을 소비하고 패키지 바이트코드와 이전 버전과의 호환성을 검증하며, 업그레이드된 패키지를 나타내는 온체인 object를 생성한다. 성공 시 결과로UpgradeReceipt를 반환한다. - Commit: 새로 생성된 패키지에 대한 정보로
UpgradeCap을 업데이트한다.
2단계는 내장 명령이지만, 1단계와 3단계는 Move 함수로 구현된다. the Sui framework는 가장 기본적인 구현을 제공한다:
public fun authorize_upgrade(cap: &mut UpgradeCap, policy: u8, digest: vector<u8>): UpgradeTicket {
let id_zero = @0x0.to_id();
assert!(cap.package != id_zero, EAlreadyAuthorized);
assert!(policy >= cap.policy, ETooPermissive);
let package = cap.package;
cap.package = id_zero;
UpgradeTicket {
cap: object::id(cap),
package,
policy,
digest,
}
}
public fun commit_upgrade(cap: &mut UpgradeCap, receipt: UpgradeReceipt) {
let UpgradeReceipt { cap: cap_id, package } = receipt;
assert!(object::id(cap) == cap_id, EWrongUpgradeCap);
assert!(cap.package.to_address() == @0x0, ENotAuthorized);
cap.package = package;
cap.version = cap.version + 1;
}
이들은 sui client upgrade 가 권한 부여 및 커밋을 위해 호출하는 함수들이다. 커스텀 업그레이드 정책은 패키지 UpgradeCap(따라서 이러한 함수 호출)에 대한 액세스를 해당 정책에 특정한 추가 조건(예: 투표, 거버넌스, 권한 목록, timelock 등) 뒤에 보호함으로써 작동한다.
UpgradeCap으로부터 UpgradeTicket을 생성하고 UpgradeReceipt를 소비하여 UpgradeCap을 업데이트하는 모든 함수 쌍은 커스텀 업그레이드 정책을 구성한다.
UpgradeCap
UpgradeCap은 패키지 업그레이드를 조정하는 중심 타입이다.
public struct UpgradeCap has key, store {
id: UID,
package: ID,
version: u64,
policy: u8,
}
패키지를 게시하면 UpgradeCap object가 생성되고 패키지를 업그레이드하면 해당 object가 업데이트된다. 이 object의 소유자는 다음을 수행할 권한이 있다:
- 향후 업그레이드에 대한 호환성 요구사항을 변경한다.
- 향후 업그레이드를 승인한다.
- 패키지를 불변으로 만든다( 업그레이드 불가능)
그리고 API는 다음 속성을 보장한다:
- 패키지의 최신 버전만 업그레이드될 수 있다(선형 히스토리가 보장됨).
- 한 번에 하나의 업그레이드만 진행 중일 수 있다(여러 동시 업그레이드를 승인할 수 없음).
- 업그레이드는 단일 transaction의 범위 내에서만 승인될 수 있다; 승인을 증명하는
UpgradeTicket을store할 수 없다. - 패키지에 대한 호환성 요구사항은 시간이 지남에 따라 더 제한적으로만 만들어질 수 있다.
UpgradeTicket
public struct UpgradeTicket {
cap: ID,
package: ID,
policy: u8,
digest: vector<u8>,
}
UpgradeTicket 은 업그레이드가 승인되었다는 증거이다. 이 승인은 다음에 특정적이다:
- 업그레이드할 특정
package: ID, 이는cap: ID의UpgradeCap이 식별하는 패밀리의 최신 패키지여야 한다. - 업그레이드가 준수할 것으로 예상되는 호환성 보장 종류를 증명하는 특정
policy: u8. - 업그레이드 후 패키지의 내용을 식별하는 특정
digest: vector<u8>
업그레이드를 실행하려고 시도하면, validator는 수행하려는 업그레이드가 승인된 업그레이드와 모든 측면에서 일치하는지 확인하고, 이러한 기준 중 하나라도 충족되지 않으면 업그레이드를 수행하지 않는다.
UpgradeTicket을 생성한 후에는 해당 transaction 내에서 사용해야 하며(나중을 위해 저장하거나, 삭제하거나, 소각할 수 없음), 그렇지 않으면 transaction이 실패한다.
package digest
UpgradeTicket digest 필드는 호출자가 제공해야 하는 authorize_upgrade의 digest 파라미터에서 가져온다. authorize_upgrade는 digest를 처리하지 않지만, 커스텀 정책은 이를 사용하여 미리 바이트코드 또는 소스 코드를 본 업그레이드만 승인할 수 있다. Sui는 다음과 같이 digest를 계산한다:
- 각 모듈의 바이트코드를 바이트 배열로 표현하여 가져온다.
- 패키지의 전이적 의존성 목록을 추가하며, 각각 바이트 배열로 표현된다.
- 이 바이트 배열 목록을 사전 순으로 정렬한다.
- 정렬된 목록의 각 요소를 순서대로
Blake2Bhasher에 공급한다. - 이 해시 상태로부터 digest를 계산한다.
digest 계산을 위한 구현을 참조할 수 있지만, 대부분의 경우 --dump-bytecode-as-base64 플래그를 전달할 때 빌드의 일부로 Move 툴체인이 digest를 출력하는 것에 의존할 수 있다:
$ sui move build --dump-bytecode-as-base64
FETCHING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test
{"modules":[<MODULE-BYTES-BASE64>],"dependencies":[<DEPENDENCY-IDS>],"digest":[59,43,173,195,216,88,176,182,18,8,24,200,200,192,196,197,248,35,118,184,207,205,33,59,228,109,184,230,50,31,235,201]}
UpgradeReceipt
public struct UpgradeReceipt {
cap: ID,
package: ID,
}
UpgradeReceipt는 Upgrade 명령이 성공적으로 실행되었다는 증거이며, Sui가 transaction에 대해 생성된 object 집합에 새 패키지를 추가했다는 증거이다. 이것은 패밀리의 최신 패키지 ID(package: ID)로 UpgradeCap (cap: ID로 식별됨)을 업데이트하는 데 사용된다.
Sui가 UpgradeReceipt를 생성한 후에는 동일한 transaction 내에서 UpgradeCap을 업데이트하는 데 사용해야 하며(나중을 위해 저장하거나, 삭제하거나, 소각할 수 없음), 그렇지 않으면 transaction이 실패한다.
policy 격리
커스텀 업그레이드 정책을 작성할 때 다음을 선호한다:
- 정책을 자체 패키지로 분리한다(업그레이드 가능성을 관리하는 코드와 함께 위치하지 않음).
- 해당 패키지를 불변으로 만들고(업그레이드 불가능),
UpgradeCap의 정책을 잠궈서 나중에 정책이 덜 제한적으로 만들어질 수 없도록 한다.
이러한 모범 사례는 사용자가 값을 잠그는 순간에 패키지의 업그레이드 정책이 무엇인지 명확히 하고, 패키지 사용자가 인식하지 못하거나 새 조건을 수용하기로 선택하지 않고도 정책이 시간이 지남에 따라 더 허용적으로 변화하지 않도록 보장함으로써 정보에 입각한 사용자 동의와 제한된 위험을 지지하는 데 도움이 된다.