트랜잭션 서명 및 전송
Sui의 transaction은 입력을 바탕으로 실행되어 transaction 결과를 정의하는 특정 기능에 대한 호출을 나타낸다.
입력은 owned object, immutable object, shared object를 가리키는 object reference이거나, 예를 들어 Move call의 인자로 쓰이는 byte vector 같은 encoded value일 수 있다. transaction은 보통 PTB 구성하기 가이드에서 설명하는 방식대로 구성되며, 구성된 뒤 사용자가 서명하고 온체인에서 실행되도록 제출한다.
signature는 wallet이 소유한 private key로 제공되며, 그 public key는 transaction sender의 Sui address와 일치해야 한다.
Sui는 intent message의 Blake2b hash digest(intent || bcs bytes of tx_data)에 commit하는 signature 를 생성하기 위해 SuiKeyPair를 사용한다.
현재 지원하는 signature scheme은 다음과 같다:
-
Ed25519 Pure
-
ECDSA Secp256k1
-
ECDSA Secp256r1
-
Multisig
-
zkLogin
Ed25519 Pure, ECDSA Secp256k1, ECDSA Secp256r1은 SuiKeyPair를 사용해 인스턴스화할 수 있으며 transaction 서명에 사용할 수 있다.
이 가이드는 Multisig와 zkLogin에는 적용되지 않으므로, 지침은 각 전용 페이지(다중 서명와 zkLogin 통합)를 참조한다.
signature와 transaction bytes가 있으면 transaction을 제출해 실행시킬 수 있다.
Workflow
다음 상위 수준 과정은 온체인 transaction을 구성하고 서명하고 실행하는 전체 흐름을 설명한다:
-
여러 command를 연결한
Transaction을 만들어 transaction 데이터를 구성한다. 자세한 내용은 PTB 구성하기를 참조한다. -
SDK의 내장 gas 추정과 coin selection이 gas coin을 선택한다.
-
서명을 생성하도록 transaction에 서명한다.
-
Transaction과 그 signature를 제출해 온체인 실행을 요청한다.
특정 gas coin을 사용하고 싶다면 먼저 gas 지불에 사용할 gas coin object ID를 찾고 이를 PTB에 명시적으로 사용한다. gas coin object가 없다면 splitCoin transaction을 사용해 gas coin object를 만든다. split coin transaction은 PTB의 첫 번째 transaction call이어야 한다.
Process multiple transactions from the same address
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되지 않도록 그 처리 순서를 정렬한다.
Examples
다음 예시는 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" 같은 형태이다. 자세한 내용은 [키와 주소](/guides/developer/transactions/transaction-auth/auth-overview#keys-and-addresses)를 참조한다.
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에 다음으로 서명한다. 자세한 내용은 PTB 구성하기를 참조한다.
// 예시 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" 같은 형태이다. 자세한 내용은 [키와 주소](/guides/developer/transactions/transaction-auth/auth-overview#keys-and-addresses)를 참조한다.
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" 같은 형태이다. 자세한 내용은 키와 주소를 참조한다.
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
Notes
- 이 가이드는 단일 private key로 서명하는 방법을 보여준다. 더 복잡한 signing policy를 설정하는 것이 적절할 때는 다중 서명를 참조한다.
- 마찬가지로 native zkLogin은 위 단계를 따르지 않으므로, zkLogin address를 파생하고 ephemeral key pair로 zkLogin signature를 생성하는 방법을 이해하려면 zkLogin 통합을 참조한다.
- 이전 도구 대신 자체 signing mechanism을 구현하기로 했다면, 각 scheme에 대해 허용되는 signature specification은 서명 문서를 참조한다.
- Flag는 signature scheme을 구분하는 1 byte이다. 지원되는 scheme과 해당 flag는 서명에서 확인한다.
execute_transaction_blockendpoint는 signature 목록을 받으므로, sponsored transaction을 사용하지 않는 한 정확히 1개의 사용자 signature만 포함해야 하고, sponsored transaction을 쓰는 경우에는 gas object에 대한 두 번째 signature를 제공할 수 있다. 자세한 내용은 스폰서드 트랜잭션를 참조한다.