본문으로 건너뛰기

참조 시뮬레이션

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

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

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

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

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

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

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

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

The borrow module

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

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

module a_module {
struct Asset has key, store {
// 일부 데이터
}

public fun use_asset(asset: &Asset) {
. // 일부 코드
}
}

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

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

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

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

이 module은 이전 module(a_module)에서 만든 object(Asset)를 참조하는 object(AssetManager)를 만든다.

그런 다음 object를 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 module의 기능을 포함해야 한다. 따라서 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 object는 borrow module이 제공하는 보장의 핵심이다. Borrow의 정의는 struct Borrow { ref: address, obj: ID }이며, 이 때문에 그 instance를 drop하거나 어디에도 저장할 수 없고 가져온 같은 transaction 안에서 소비되어야 한다(hot potato). 또한 Borrow struct의 field는 반환되는 object가 같은 Referent의 것이며 원래 Referent instance가 보유하고 있던 object임을 보장한다. 다시 말해, 가져온 object를 유지하거나 다른 Referent에 있는 다른 object와 이를 바꿔치기할 방법은 없다.

주의

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

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

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

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

Example

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

// PTB를 초기화한다
const txb = new TransactionBlock();
// assetManager를 로드한다
const assetManager = txb.object(assetManagerId);
// asset를 가져온다
const [asset, borrow] = txb.moveCall({
target: "0xaddr1::another_module::get_asset",
arguments: [ assetManager ],
});
// asset를 사용한다
txb.moveCall({
target: "0xaddr2::a_module::use_asset",
arguments: [ asset ],
});
// asset를 반환한다
txb.moveCall({
target: "0xaddr1::another_module::return_asset",
arguments: [ assetManager, asset, borrow ],
});
...