트랜잭션 서명 및 전송
Sui의 transaction authentication은 사용자가 제어하는 cryptographic key, 온체인에서 account를 식별하는 address, 소유권을 증명하는 signature라는 세 가지 핵심 개념을 포함한다. Sui는 널리 쓰이는 wallet specification과 여러 signature scheme을 사용해 이 개념들을 구현한다.
key와 address
Sui는 사용자의 key 관리를 돕기 위해 cryptocurrency 업계에서 널리 받아들여진 wallet specification인 BIP-32와 그 변형인 SLIP-0010, BIP-44, BIP-39를 따른다. Sui는 signed transaction에 대해 pure Ed25519, ECDSA Secp256k1, ECDSA Secp256r1, multisig를 지원한다.
key derivation scheme
Ed25519(EdDSA) signing scheme을 지원하는 wallet을 관리할 때 Sui는 SLIP-0010을 따른다. SLIP-0010은 wallet이 hardened key path를 사용해 parent private key에서 child private key를 항상 derive하도록 강제한다.
Sui는 ECDSA Secp256k1과 ECDSA Secp256r1 signing scheme을 지원하는 wallet을 관리할 때 BIP-32를 따른다.
BIP-32는 key 집합을 논리적으로 연결하기 위한 hierarchical deterministic wallet 구조를 정의한다. 이렇게 key를 그룹화하면 많은 private key를 추적해야 하는 부담을 줄일 수 있다. 또한 custodian은 하나의 control source 아래에서 사용자 account마다 별도의 managed address를 발급할 수 있다. BIP-32를 사용하면 private key derivation과 public key derivation이 분리되어 watch-only wallet use case가 가능해진다. 이 경우 public key 체인과 그 address는 derive할 수 있지만, private key는 signing을 위해 offline으로 유지할 수 있다.
key derivation path
BIP-44는 derivation path의 5개 level과 그 정확한 의미를 추가로 정의한다:
이 구조에서 slash는 hierarchy의 level을 나타낸다.
purpose level은 서로 다른 signing scheme을 구분한다:
-
44: Ed25519 -
54: ECDSA Secp256k1 -
74: Secp256r1
예를 들어 BIP-49와 BIP-84는 Bitcoin의 script type을 식별하는 데 사용된다. Sui는 ECDSA Secp256k1을 나타내기 위해 54를 선 택했다. 54 아래에는 기존 BIP가 없어서 Bitcoin standard와 혼동될 가능성을 피할 수 있기 때문이다.
coin_type 값은 다른 cryptocurrency와 함께 repository에서 관리된다. 두 signature scheme 모두 Sui의 등록된 coin_type인 784를 사용한다.
account level은 user account를 논리적으로 분리하고 특정 account category를 만드는 데 사용된다.
Account-based currency는 처음 3개 level만 정의하는 반면, UTXO-based currency는 change와 address level 정의를 추가한다. Sui의 object-oriented data model은 UTXO 기반도 account 기반도 아니며 두 방식을 결합하므로, 최대한의 호환성을 위해 5개 level을 모두 사용한다.
| Scheme | Path | 설명 |
|---|---|---|
| Ed25519 | m/44'/784'/{account}'/{change}'/{address}' | derivation path의 각 level이 hardened이다. |
| ECDSA Secp256k1 | m/54'/784'/{account}'/{change}/{address} | 처음 3개 level이 hardened이다. |
| ECDSA Secp256r1 | m/74'/784'/{account}'/{change}/{address} | 처음 3개 level이 hardened이다. |
mnemonic 지원
Sui가 seed에서 master key를 derive하는 deterministic 방식을 정의한 뒤, BIP-39는 mnemonics를 사용해 seed를 사람이 읽고 기억하기 쉽게 만든다. Sui는 BIP-39 word list에서 checksum이 올바른 12, 15, 18, 21, 24개 단어를 허용하며, 이는 각각 128, 160, 192, 224, 256 bit entropy에 대응한다.
주소 형식
32-byte Sui address를 derive하기 위해 Sui는 signature scheme flag 1-byte와 public key byte를 연결한 값을 BLAKE2b(256-bit output) hashing function으로 hash한다. Sui address는 현재 pure Ed25519, Secp256k1, Secp256r1, multisig를 지원하며, 각각 대응하는 flag byte는 0x00, 0x01, 0x02, 0x03이다.
서명
사용자가 signed transaction을 제출하면 serialized signature와 serialized transaction data가 제출된다. serialized transaction data는 TransactionData struct를 Binary Canonical Serialization으로 serialize한 byte이고, serialized signature는 flag || sig || pk byte를 연결한 값으로 정의된다.
서명 구조
Sui signature는 세 component를 연결한 값이다:
-
flag: signature scheme에 대응하는 1-byte 표현이다. -
sig: signature의 compressed byte 표현이다. DER encoding이 아니다. -
pk: signature에 대응하는 public key의 byte 표현이다.
지원되는 signature scheme
다음 표는 각 signing scheme과 대응하는 flag, signature format, public key format을 나열한다:
| Scheme | Flag | Signature format | Public key format |
|---|---|---|---|
| Pure Ed25519 | 0x00 | Compressed, 64 bytes | Compressed, 32 bytes |
| ECDSA Secp256k1 | 0x01 | Non-recoverable, compressed, 64 bytes | Compressed, 33 bytes |
| ECDSA Secp256r1 | 0x02 | Non-recoverable, compressed, 64 bytes | Compressed, 33 bytes |
| multisig | 0x03 | BCS serialized all signatures, size varies | BCS serialized all participating public keys, size varies |
| zkLogin | 0x05 | BCS serialized zkLogin inputs, max epoch and ephemeral signature, size varies | iss length, iss byte, 32-byte로 padding된 address seed를 연결한 값, size varies |
| passkey | 0x06 | BCS serialized passkey inputs(authenticatorData, clientDataJson, userSignature), size varies | Compressed, 33 bytes |
Signature requirements
signature는 transaction data의 intent message hash에 commit해야 한다. 이 hash는 BCS serialized transaction data 앞에 3-byte intent를 붙여 구성할 수 있다. intent가 무엇인지와 intent message를 구성하는 방법을 자세히 알아보려면 Intent Signing을 참조한다.
signing API를 호출할 때는 먼저 transaction data의 intent message를 Blake2b로 hash하여 32 byte로 만들어야 한다. 이 external hashing은 signing API 내부에서 수행되는 hashing과 별개이다. 기존 standard와 hardware secure module(HSM)과 호환되도록 signing algorithm은 내부적으로 추가 hashing을 수행한다. ECDSA Secp256k1과 Secp256r1의 경우 internal hash function으로 SHA-2 SHA256을 사용해야 한다. Pure Ed25519의 경우 SHA-512를 사용해야 한다.
ECDSA signature requirement
허용되는 ECDSA Secp256k1 및 Secp256r1 signature는 다음 조건을 따라야 한다:
-
ECDSA가 사용하는 internal hash는 transaction data의 SHA256 SHA-2 hash여야 한다. Sui가 SHA256을 사용하는 이유는 Apple, HSM에서 지원되고 Bitcoin에서도 널리 채택되었기 때문이다.
-
signature는 첫 32 byte가
r, 두 번째 32 byte가s인[r, s]형식의 64 byte 길이여야 한 다. -
r값은0x1부터0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364140까지의 범위(포함)에 있어야 한다. -
s값은 curve order의 lower half에 있어야 한다. signature가 너무 높으면 해당 curve order에 대해order - s를 사용해 BIP-0062에 따라 더 낮은s로 변환한다. Secp256k1의 curve order는0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141이다. Secp256r1의 curve order는 Standards for Efficient Cryptography에 정의된0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551이다. -
이상적으로 signature는 RFC6979에 따라 deterministic nonce로 생성되어야 한다.
Ed25519 signature requirement
허용되는 pure Ed25519 signature는 다음 조건을 따라야 한다:
특수 signature scheme
advanced signature scheme에 대한 자세한 내용은 다음을 참조한다:
-
zkLogin: zero-knowledge login signature의 자세한 내용은 zkLogin을 참조한다.
-
Passkey: passkey implementation의 자세한 내용은 SIP-8을 참조한다.
-
Multisig: multi-signature transaction의 자세한 내용은 Multisig를 참조한다.
-
Offline Signing: offline transaction signing의 구체적인 예시는 Offline Signing을 참조한다.
Authority 서명
Sui의 validator collection은 서로 다른 목적을 위해 3개의 distinct key pair를 보유한다.
Protocol key pair
protocol key pair는 user-signed transaction이 검증되면 해당 transaction에 authority signature를 제공한다. user transaction에 signature를 제공하는 authority의 stake가 필요한 2/3 threshold를 넘으면 Sui는 transaction을 실행한다. Sui는 주어진 authority 수에 대해 aggregated signature를 빠르게 검증할 수 있도록 BLS12381 scheme을 사용한다. 특히 Sui는 각 public key가 96 byte이고 signature가 48 byte인 minSig BLS mode를 사용한다. 후자가 중요한 이유는 일반적으로 validator가 각 epoch 시작 시 key를 한 번 등록한 뒤 transaction에 계속 서명하기 때문에, Sui가 최소 signature size에 맞춰 최적화하기 때문이다.
BLS scheme과 마찬가지로 독립 signature를 aggregate해 단일 BLS signature payload로 만들 수 있다. Sui는 aggregate signature와 함께 어떤 validator가 서명했는지를 나타내는 bitmap도 제공한다. 이는 authority signature size를 (2f + 1) × BLS_sig size에서 하나의 BLS_sig payload로 줄인다. 이 방식은 validator set size와 독립적으로 transaction certificate를 압축하여 network cost를 크게 줄인다.
BLS12381 aggregated signature에서 발생할 수 있는 rogue key attack에 대응하기 위해 authority registration 중 secret key에 대한 proof of knowledge(KOSK)를 사용한다. authority가 validator set에 추가되도록 요청하면 proof of possession이 제출되고 검증된다. proof of possession을 만드는 방법은 Intent Signing을 참조한다. 대부분의 standard와 달리 Sui proof of knowledge scheme은 address에도 commit하므로, 악의적인 다른 validator의 validator BLS key를 재사용하는 공격에 대한 추가 보호를 제공한다.
Account key pair
authority가 staking reward payment를 받는 데 사용하는 account는 account key pair로 보호된다. Sui는 signing scheme으로 pure Ed25519를 사용한다.
Network key pair
private key는 consensus networking에 필요한 TLS handshake를 수행하는 데 사용된다. public key는 validator identity에 사용된다. scheme으로는 pure Ed25519가 사용된다.
더 많은 authority key tooling은 Validator Tool을 참조한다.
예시
Sui CLI tool과 Sui SDKs는 여러 signing scheme으로 transaction에 서명할 수 있는 유연한 interface를 제공한다.
- CLI
- TypeScript
$ sui keytool import "TEST_MNEMONIC" ed25519 "m/44'/784'/0'/0'/0'"
$ sui client new-address ed25519 "m/44'/784'/0'/0'/0'"
const keypair = Ed25519Keypair.deriveKeypair(TEST_MNEMONIC, `m/44'/784'/0'/0'/0'`);
const address = keypair.getPublicKey().toSuiAddress();
더 많은 test vector는 pure Ed25519 또는 ECDSA Secp256k1을 참조한다.
워크플로
다음 상위 수준 과정은 온체인 transaction을 구성하고 서명하고 실행하는 전체 흐름을 설명한다:
-
여러 command를 연결한
Transaction을 만들어 transaction data를 구성한다. 자세한 내용은 Programmable Transaction Block 구성하기를 참조한다. -
SDK의 내장 gas estimation과 coin selection이 gas coin을 선택한다.
-
signature를 생성하도록 transaction에 서명한다.
-
Transaction과 그 signature를 제출해 온체인 실행을 요청한다.
address balances feature를 사용하는 경우, 이 feature가 gas coin을 선택하거나 merge할 필요를 완전히 제거한다.
특정 gas coin을 사용하고 싶다면 먼저 gas 지불에 사용할 gas coin object ID를 찾고 이를 PTB에 명시적으로 사용한다. gas coin object가 없다면 splitCoin transaction을 사용해 gas coin object를 만든다. split coin transaction은 PTB의 첫 번째 transaction call이어야 한다.
같은 address의 여러 transaction 처리
Sui SDK는 같은 address에서 여러 transaction을 처리하는 데 도움이 되는 transaction executor를 제공한다.
SerialTransactionExecutor
transaction을 하나씩 순차적으로 처리할 때는 SerialTransactionExecutor를 사용한다.
이 executor는 sender의 모든 coin을 가져와 하나의 coin으로 합친 뒤 모든 transaction에 사용한다.
SerialTransactionExecutor를 사용하면 PTB 전반의 object 입력 versioning을 처리하여 SequenceNumber error를 방지할 수 있다.
ParallelTransactionExecutor
같은 sender의 transaction을 동시에 처리하고 싶다면 ParallelTransactionExecutor를 사용한다.
이 클래스는 병렬 transaction이 해당 coin에 대해 equivocate하지 않도록 관리하는 gas coin pool을 만든다.
이 클래스는 transaction 전반에서 사용된 object를 추적하고, object 입력도 equivocate되지 않도록 그 처리 순서를 정렬한다.
Transaction signing 예시
다음 예시는 Rust, TypeScript, Sui CLI를 사용해 transaction에 서명하고 실행하는 방법을 보여준다.
- TypeScript
- Rust
- Sui CLI
Sui TypeScript SDK를 사용하면 key pair를 인스턴스화하고 그 public key와 Sui address를 파생하는 여러 방법이 있다.
import { fromHex } from '@mysten/bcs';
import { type Keypair } from '@mysten/sui/cryptography';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Secp256k1Keypair } from '@mysten/sui/keypairs/secp256k1';
import { Secp256r1Keypair } from '@mysten/sui/keypairs/secp256r1';
import { Transaction } from '@mysten/sui/transactions';
const kp_rand_0 = new Ed25519Keypair();
const kp_rand_1 = new Secp256k1Keypair();
const kp_rand_2 = new Secp256r1Keypair();
const kp_import_0 = Ed25519Keypair.fromSecretKey(
fromHex('0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82'),
);
const kp_import_1 = Secp256k1Keypair.fromSecretKey(
fromHex('0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82'),
);
const kp_import_2 = Secp256r1Keypair.fromSecretKey(
fromHex('0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82'),
);
// $MNEMONICS는 wordlist에 있는 12/15/18/21/24개 단어를 가리키며, 예를 들어 "retire skin goose will hurry this field stadium drastic label husband venture cruel toe wire" 같은 형태이다. 자세한 내용은 [Keys and addresses](/develop/transactions/transaction-auth/auth-overview)를 참조한다.
const kp_derive_0 = Ed25519Keypair.deriveKeypair('$MNEMONICS');
const kp_derive_1 = Secp256k1Keypair.deriveKeypair('$MNEMONICS');
const kp_derive_2 = Secp256r1Keypair.deriveKeypair('$MNEMONICS');
const kp_derive_with_path_0 = Ed25519Keypair.deriveKeypair('$MNEMONICS', "m/44'/784'/1'/0'/0'");
const kp_derive_with_path_1 = Secp256k1Keypair.deriveKeypair('$MNEMONICS', "m/54'/784'/1'/0/0");
const kp_derive_with_path_2 = Secp256r1Keypair.deriveKeypair('$MNEMONICS', "m/74'/784'/1'/0/0");
// `kp_rand_0` 자리에 위 변수 이름들 중 하나를 사용한다.
const pk = kp_rand_0.getPublicKey();
const sender = pk.toSuiAddress();
// 예시 transaction을 생성한다.
const txb = new Transaction();
txb.setSender(sender);
txb.setGasPrice(5);
txb.setGasBudget(100);
const bytes = await txb.build();
const serializedSignature = (await keypair.signTransaction(bytes)).signature;
// 로컬에서 signature를 검증한다
expect(await keypair.getPublicKey().verifyTransaction(bytes, serializedSignature)).toEqual(true);
// 원하는 network용 sui client를 정의한다.
const client = new SuiGrpcClient({
baseUrl: 'https://fullnode.testnet.sui.io:443',
network: 'testnet',
});
// transaction을 실행한다.
let res = await client.executeTransaction({
transaction: bytes,
signatures: [serializedSignature],
});
console.log(res);
아래 전체 코드 예 시는 crates/sui-sdk에 있다.
Sui Rust SDK를 사용하면 SuiKeyPair를 인스턴스화하고 그 public key와 Sui address를 파생하는 여러 방법이 있다.
// key pair를 결정론적으로 생성한다. test 전용이며 mainnet에서는 사용하지 말고, 무작위 key pair를 생성하려면 다음 섹션을 사용한다.
let skp_determ_0 =
SuiKeyPair::Ed25519(Ed25519KeyPair::generate(&mut StdRng::from_seed([0; 32])));
let _skp_determ_1 =
SuiKeyPair::Secp256k1(Secp256k1KeyPair::generate(&mut StdRng::from_seed([0; 32])));
let _skp_determ_2 =
SuiKeyPair::Secp256r1(Secp256r1KeyPair::generate(&mut StdRng::from_seed([0; 32])));
// 무작위 key pair를 생성한다.
let _skp_rand_0 = SuiKeyPair::Ed25519(get_key_pair_from_rng(&mut rand::rngs::OsRng).1);
let _skp_rand_1 = SuiKeyPair::Secp256k1(get_key_pair_from_rng(&mut rand::rngs::OsRng).1);
let _skp_rand_2 = SuiKeyPair::Secp256r1(get_key_pair_from_rng(&mut rand::rngs::OsRng).1);
// base64로 인코딩된 32-byte `private key`에서 key pair를 import한다.
let _skp_import_no_flag_0 = SuiKeyPair::Ed25519(Ed25519KeyPair::from_bytes(
&Base64::decode("1GPhHHkVlF6GrCty2IuBkM+tj/e0jn64ksJ1pc8KPoI=")
.map_err(|_| anyhow!("Invalid base64"))?,
)?);
let _skp_import_no_flag_1 = SuiKeyPair::Ed25519(Ed25519KeyPair::from_bytes(
&Base64::decode("1GPhHHkVlF6GrCty2IuBkM+tj/e0jn64ksJ1pc8KPoI=")
.map_err(|_| anyhow!("Invalid base64"))?,
)?);
let _skp_import_no_flag_2 = SuiKeyPair::Ed25519(Ed25519KeyPair::from_bytes(
&Base64::decode("1GPhHHkVlF6GrCty2IuBkM+tj/e0jn64ksJ1pc8KPoI=")
.map_err(|_| anyhow!("Invalid base64"))?,
)?);
// base64로 인코딩된 33-byte `flag || private key`에서 key pair를 import한다. signature scheme은 flag가 결정한다.
let _skp_import_with_flag_0 =
SuiKeyPair::decode_base64("ANRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C")
.map_err(|_| anyhow!("Invalid base64"))?;
let _skp_import_with_flag_1 =
SuiKeyPair::decode_base64("AdRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C")
.map_err(|_| anyhow!("Invalid base64"))?;
let _skp_import_with_flag_2 =
SuiKeyPair::decode_base64("AtRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C")
.map_err(|_| anyhow!("Invalid base64"))?;
// `skp_determ_0` 자리에 위 변수 이름들 중 하나를 사용한다
let pk = skp_determ_0.public();
let sender = SuiAddress::from(&pk);
기본 gas coin, gas budget, gas price를 사용한 예시 programmable transaction으로 구성한 transaction data에 다음으로 서명한다. 자세한 내용은 Programmable Transaction Block 구성하기를 참조한다.
// 예시 programmable transaction을 구성한다.
let pt = {
let mut builder = ProgrammableTransactionBuilder::new();
builder.pay_sui(vec![sender], vec![1])?;
builder.finish()
};
let gas_budget = 5_000_000;
let gas_price = sui_client.read_api().get_reference_gas_price().await?;
// network로 전송할 transaction data를 생성한다.
let tx_data = TransactionData::new_programmable(
sender,
vec![gas_coin.object_ref()],
pt,
gas_budget,
gas_price,
);
intent message의 Blake2b hash digest(intent || bcs bytes of tx_data)에 signature를 commit한다.
// key pair가 서명해야 할 digest, 즉 `intent || tx_data`의 blake2b hash를 파생한다.
let intent_msg = IntentMessage::new(Intent::sui_transaction(), tx_data);
let raw_tx = bcs::to_bytes(&intent_msg).expect("bcs should not fail");
let mut hasher = sui_types::crypto::DefaultHash::default();
hasher.update(raw_tx.clone());
let digest = hasher.finalize().digest;
// digest에 서명하기 위해 SuiKeyPair를 사용한다.
let sui_sig = skp_determ_0.sign(&digest);
// 제출 전에 로컬에서 signature를 검증하려면 이 함수를 사용한다. 로컬 검증에 실패하면 transaction은 Sui에서 실행되지 않는다.
let res = sui_sig.verify_secure(
&intent_msg,
sender,
sui_types::crypto::SignatureScheme::ED25519,
);
assert!(res.is_ok());
마지막으로 signature와 함께 transaction을 제출한다.
let transaction_response = sui_client
.quorum_driver_api()
.execute_transaction_block(
sui_types::transaction::Transaction::from_generic_sig_data(
intent_msg.value,
Intent::sui_transaction(),
vec![GenericSignature::Signature(sui_sig)],
),
SuiTransactionBlockResponseOptions::default(),
None,
)
.await?;
Sui CLI를 처음 사용할 때는 머신의 ~/.sui/keystore에 private key 목록(Base64로 인코딩된 flag || 32-byte-private-key)을 담은 로컬 파일을 만든다.
address를 지정하면 어떤 key든 사용해 transaction에 서명할 수 있다.
address 목록을 보려면 sui keytool list를 사용한다.
key를 초기화하는 방법은 3가지가 있다:
# 무작위로 생성한다.
sui client new-address ed25519
sui client new-address secp256k1
sui client new-address secp256r1
# 32-byte private key를 keystore로 import한다.
sui keytool import "0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82" ed25519
sui keytool import "0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82" secp256k1
sui keytool import "0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82" secp256r1
# derivation path가 있는 mnemonics(복구 구문)를 keystore로 import한다.
# $MNEMONICS는 wordlist에 있는 12/15/18/21/24개 단어를 가리키며, 예를 들어 "retire skin goose will hurry this field stadium drastic label husband venture cruel toe wire" 같은 형태이다. 자세한 내용은 [Keys and addresses](/develop/transactions/transaction-auth/auth-overview)를 참조한다.
sui keytool import "$MNEMONICS" ed25519
sui keytool import "$MNEMONICS" secp256k1
sui keytool import "$MNEMONICS" secp256r1
CLI에서 transfer transaction을 생성한다.
$SUI_ADDRESS는 서명에 사용할 key pair에 대응하는 값으로 설정한다.
$GAS_COIN_ID는 gas에 사용할 sender 소유 object ID를 가리킨다.
$GAS_BUDGET은 transaction 실행에 사용할 예산을 가리킨다.
그 다음 sender address에 대응하는 private key로 서명한다.
$MNEMONICS는 wordlist에 있는 12/15/18/21/24개 단어를 가리키며, 예를 들어 "retire skin goose will hurry this field stadium drastic label husband venture cruel toe wire" 같은 형태이다. 자세한 내용은 Keys and addresses를 참조한다.
Beginning with the Sui v1.24.1 release, the --gas-budget option is no longer required for CLI commands.
$ sui client gas
$ sui client transfer-sui --to $SUI_ADDRESS --sui-coin-object-id $GAS_COIN_ID --gas-budget $GAS_BUDGET --serialize-unsigned-transaction
$ sui keytool sign --address $SUI_ADDRESS --data $TX_BYTES
$ sui client execute-signed-tx --tx-bytes $TX_BYTES --signatures $SERIALIZED_SIGNATURE