본문으로 건너뛰기

참조 시뮬레이션

Sui blockchain의 모든 것은 객체이다. Sui network용 Move 패키지를 개발할 때는 보통 Sui API에서 제공하는 기능을 통해 어떤 방식으로든 온체인 객체를 조작하거나 사용한다. 대부분의 API 함수에서는 객체를 reference로 제공한다.

reference는 Move와 Sui에서 프로그래밍할 때 핵심 구성 요소이다. Sui API에서 제공되는 대부분의 기능은 객체를 reference로 받는다.

객체를 사용하는 방법은 2가지가 있다:

  • By value: 객체를 값으로 사용하면 그 객체를 완전히 제어할 수 있다. 기능이 제공된다면 이를 파괴할 수 있고, store ability를 가진다면 wrapping할 수 있으며, address로 transfer할 수도 있다.

  • By reference: 객체를 reference로 사용하면 객체 자체의 ownership을 갖는 대신 그 데이터에 대한 reference를 사용하므로, 그 객체에 대한 연산은 객체를 정의한 모듈이 제공하는 로직에 따라 결정된다. reference의 제한은 asset 주위에 높은 수준의 security와 safety를 가진 스마트 계약을 개발할 수 있게 한다. reference에는 2가지 type이 있다:

    • Mutable reference (&mut): 객체를 변경할 수 있지만(API에 따름), 파괴하거나 transfer할 수는 없다.

    • Immutable reference (&): referenced 객체에 대한 연산 집합과 보장 또는 invariant를 더 제한한다. 객체 데이터에는 read-only access만 가진다.

Programmable 트랜잭션 block(PTB)는 현재 트랜잭션 command 중 하나에서 반환된 객체 reference의 사용을 허용하지 않는다. PTB의 입력 객체, PTB가 생성한 객체(MakeMoveVec 같은 것), 또는 트랜잭션 command가 값으로 반환한 객체를 이후 트랜잭션 command에서 reference로 사용할 수 있다. 하지만 트랜잭션 command가 reference를 반환하면, 그 reference를 어떤 호출에서도 사용할 수 없으므로 Move의 특정 일반 패턴이 크게 제한된다.

The borrow 모듈

Sui framework에는 reference 문제에 대한 해법을 제공하는 borrow module이 포함되어 있다. 이 모듈은 객체를 값으로 접근하게 해 주지만, 가져온 객체를 파괴하거나, transfer하거나, wrapping하는 것이 불가능하도록 만드는 model을 구축한다. borrow 모듈은 다른 객체(reference하려는 객체)를 wrapping하는 Referent 객체를 노출한다. 모듈은 hot potato pattern을(Borrow instance를 통해) 사용해 wrapped 객체를 값으로 가져올 수 있게 한다. 그다음 같은 PTB 안에서 모듈은 객체를 Referent로 다시 반환하도록 강제한다. Borrow instance는 반환되는 객체가 가져온 것과 동일한 객체임을 보장한다.

예시로 객체(Asset)와 그 객체를 사용하는 함수(use_asset)를 노출하는 다음 모듈 stub를 생각해 보자.

module a_module {
struct Asset has key, store {
// some data
}

public fun use_asset(asset: &Asset) {
. // some code
}
}

use_asset 함수는 asset에 대한 immutable reference(&Asset)를 받으며, 이는 API 정의에서 흔한 패턴이다.

이제 이 asset를 사용하는 다른 모듈을 생각해 보자.

module another_module {
struct AssetManager has key {
asset: Asset,
}

public fun get_asset(manager: &AssetManager): &Asset {
&manager.asset
}
}

이 모듈은 이전 모듈(AssetManager)에서 만든 객체(Asset)를 참조하는 객체(a_module)를 만든다.

그런 다음 객체를 reference로 가져와 use_asset 함수에 전달하는 Move 함수를 작성할 수 있다.

fun do_something(manager: &AssetManager) {
let asset = another_module::get_asset(manager);
a_module::use_asset(asset);
}

하지만 do_something의 2개 함수는 PTB 안에서는 유효하지 않은데, PTB가 함수에서 반환된 reference를 다른 함수에 전달하는 것을 지원하지 않기 때문이다.

이 연산을 PTB 안에서 유효하게 만들려면 borrow 모듈의 기능을 포함해야 한다. 따라서 another_module 코드를 다음과 같이 바꿀 수 있다:

module another_module {
struct AssetManager has key {
asset: Referent<Asset>,
}

public fun get_asset(manager: &mut AssetManager): (Asset, Borrow) {
borrow::borrow(&mut manager.asset)
}


public fun return_asset(
manager: &mut AssetManager,
asset: Asset,
b: Borrow) {
borrow::put_back(&mut manager.asset, asset, b)
}
}

이제 PTB는 asset를 가져오고, 이를 use_asset 호출에서 사용하고, asset를 반환할 수 있다.

Considerations

Borrow 객체는 borrow 모듈이 제공하는 보장의 핵심이다. Borrow의 정의는 struct Borrow { ref: address, obj: ID }이며, 이 때문에 그 instance를 drop하거나 어디에도 저장할 수 없고 가져온 같은 트랜잭션 안에서 소비되어야 한다(hot potato). 또한 Borrow struct의 필드는 반환되는 객체가 같은 Referent의 것이며 원래 Referent instance가 보유하고 있던 객체임을 보장한다. 다시 말해, 가져온 객체를 유지하거나 다른 Referent에 있는 다른 객체와 이를 바꿔치기할 방법은 없다.

주의

Referent를 사용하는 것은 매우 명시적이고 침습적인 변경이다. 해법을 설계할 때 이 점을 고려한다.

PTB에서 reference를 지원하는 기능이 계획되어 있으며, 이는 API에 훨씬 더 자연스럽고 올바른 패턴이다.

borrow 모듈 사용의 함의와, 나중에 더 자연스러운 reference 패턴으로 이동할 수 있는 메커니즘이 있는지를 고려해야 한다.

마지막으로 Referent model은 mutable reference의 사용을 강제하고 객체를 값으로 반환한다. 둘 다 API를 설계할 때 중요한 함의를 가진다. 모듈이 어떤 로직을 제공하는지와 객체가 어떻게 노출되는지에 주의해야 한다.

예시

앞선 예시를 확장하면 use_asset를 호출하는 PTB는 다음과 같이 작성된다:

// initialize the PTB
const txb = new TransactionBlock();
// load the assetManager
const assetManager = txb.object(assetManagerId);
// retrieve the asset
const [asset, borrow] = txb.moveCall({
target: "0xaddr1::another_module::get_asset",
arguments: [ assetManager ],
});
// use the asset
txb.moveCall({
target: "0xaddr2::a_module::use_asset",
arguments: [ asset ],
});
// return the asset
txb.moveCall({
target: "0xaddr1::another_module::return_asset",
arguments: [ assetManager, asset, borrow ],
});
...