본문으로 건너뛰기

빠른 시작

이 문서는 계정 키를 파생하고, 두 키로 Native 레인 트랜잭션에 서명한 뒤 오프라인으로 검증하고, 선택적으로 qorechain-diana 테스트넷으로 전송하는 과정을 안내합니다. Node.js 20 이상이 필요합니다. QoreSign 0.1.1을 다룹니다. 4단계 이후의 섹션에서는 같은 라이브러리를 Python과 Go에서 사용하는 방법과 커밋된 트랜잭션을 다시 읽어 오는 방법을 보여줍니다.

1. 설치​

mkdir qoresign-quickstart && cd qoresign-quickstart
npm init -y
npm pkg set type=module
npm install @qorechain/qoresign

2. 키 파생​

// keys.mjs
import { addressFromPublicKey, deriveCanonicalPqKey, deriveClassicalKey } from "@qorechain/qoresign";

// The public BIP-39 test phrase. Never use it for real funds.
const mnemonic =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";

const classical = deriveClassicalKey({ mnemonic }); // m/44'/118'/0'/0/0
const { bech32 } = addressFromPublicKey(classical.publicKey, "secp256k1");
const pq = deriveCanonicalPqKey({ mnemonic, address: bech32 });

console.log(bech32, pq.publicKey.length); // qor1..., 2592

deriveCanonicalPqKey는 QoreChain 지갑이 해당 계정에 대해 파생하는 것과 동일한 ML-DSA-87 키를 반환하므로, QoreChain 지갑이 만든 계정에 이미 등록된 키와 일치합니다. 키 파생을 참고하세요.

3. 서명 및 검증​

지갑은 보통 dApp으로부터 bodyBytes, authInfoBytes, 체인 ID, 계정 번호를 받습니다(Cosmos SDK signDirect 요청의 SignDoc). 이 예제에서는 cosmjs-types로 직접 만듭니다.

npm install cosmjs-types @cosmjs/proto-signing @cosmjs/amino
// sign.mjs
import { encodeSecp256k1Pubkey } from "@cosmjs/amino";
import { encodePubkey, makeAuthInfoBytes } from "@cosmjs/proto-signing";
import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx";
import { TxBody } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import {
addressFromPublicKey,
deriveCanonicalPqKey,
deriveClassicalKey,
signHybrid,
verifyHybridTx,
} from "@qorechain/qoresign";

const mnemonic =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
const classical = deriveClassicalKey({ mnemonic });
const address = addressFromPublicKey(classical.publicKey, "secp256k1").bech32;
const pq = deriveCanonicalPqKey({ mnemonic, address });

const bodyBytes = TxBody.encode(
TxBody.fromPartial({
messages: [
{
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: MsgSend.encode({ fromAddress: address, toAddress: address, amount: [{ denom: "uqor", amount: "1" }] }).finish(),
},
],
}),
).finish();
const authInfoBytes = makeAuthInfoBytes(
[{ pubkey: encodePubkey(encodeSecp256k1Pubkey(classical.publicKey)), sequence: 0 }],
[{ denom: "uqor", amount: "20000" }],
200000,
undefined,
undefined,
1, // SIGN_MODE_DIRECT
);

const chainId = "qorechain-diana";
const signed = signHybrid({ chainId, accountNumber: 0, bodyBytes, authInfoBytes, classicalKey: classical, pqKey: pq });

const result = verifyHybridTx({
txBytes: signed.txBytes,
chainId,
accountNumber: 0,
classicalPublicKey: classical.publicKey,
pqPublicKey: pq.publicKey,
});
console.log(result); // { valid: true, classical: true, pq: { valid: true, signBytesVersion: 'v2' } }
node sign.mjs

4. 테스트넷으로 전송(선택 사항)​

본인의 테스트넷 계정을 사용하세요(스크립트에 메인넷 니모닉을 절대 사용하지 마세요). 계정에는 테스트넷 QOR와 등록된 ML-DSA-87 키가 필요합니다. QoreChain 지갑은 계정을 만들 때 키를 등록하며, examples/register-and-send에서 QoreSign으로 키를 등록하는 방법을 볼 수 있습니다.

import { simulateTx, submitWithSignBytesFallback } from "@qorechain/qoresign/network";

const rest = "https://api-testnet.qore.host";
const sim = await simulateTx({ rest, txBytes: signed.txBytes }); // throws if the network would refuse it
console.log(sim.gasUsed, sim.suggestedGasLimit, sim.balanceDeltas);
// Rebuild authInfoBytes with gas limit sim.suggestedGasLimit and fee >= 0.1 uqor per gas, sign again, then:
// await submitWithSignBytesFallback({ rest, chainId, sign: () => signedAgain.txBytes });

계정 번호, 시퀀스, 수수료를 포함한 전체 흐름은 examples/register-and-send에 있습니다.

5. 다른 언어: 메시지 서명 및 검증​

이 예제들은 오프라인으로 실행됩니다. 각각 공개 BIP-39 테스트 문구(주소 qor19rl4cm2hmr8afy4kldpxz3fka4jguq0agt672h)에서 키를 파생하고, 두 키로 메시지에 서명한 뒤 검증하고, qor19rl4cm2hmr8afy4kldpxz3fka4jguq0agt672h verified를 출력합니다. 이 문구에는 절대 자금을 보내지 마세요.

Python​

0.1.1에서 새로 추가되었습니다. Python 3.9 이상, macOS와 Linux용 wheel 제공(아직 Windows wheel 없음).

pip install qorechain-qoresign
import qoresign as qs

# Public BIP-39 test phrase: never send funds to it.
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
classical = qs.derive_classical_key(mnemonic, qs.ClassicalKeyType.SECP256K1) # m/44'/118'/0'/0/0
account = qs.address_from_public_key(classical.public_key, qs.ClassicalKeyType.SECP256K1)
pq_key = qs.derive_canonical_pq_key(mnemonic, account.bech32)

data = b"Hello from QoreSign"
sig = qs.sign_message("qorechain-diana", account.bech32, data, classical, pq_key=pq_key)
# Raises QoreSignError if either signature is invalid.
qs.verify_message("qorechain-diana", account.bech32, data, sig, pq_public_key=pq_key.public_key)
print(account.bech32, "verified")

게시된 0.1.1 wheel로 macOS arm64와 Linux aarch64에서 실행했습니다.

Go​

go get github.com/qorechain/qoresign/go@v0.1.1
package main

import (
"fmt"

qoresign "github.com/qorechain/qoresign/go"
)

func main() {
// Public BIP-39 test phrase: never send funds to it.
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
classical, _ := qoresign.DeriveClassicalKey(mnemonic, qoresign.DeriveClassicalOptions{}) // m/44'/118'/0'/0/0
account, _ := qoresign.AddressFromPublicKey(classical.PublicKey, qoresign.Secp256k1)
pq, _ := qoresign.DeriveCanonicalPqKey(mnemonic, account.Bech32)

data := []byte("Hello from QoreSign")
sig, _ := qoresign.SignMessage(qoresign.SignMessageArgs{ChainID: "qorechain-diana",
Signer: account.Bech32, Data: data, ClassicalKey: classical, PqKey: &pq})
// Returns an error if either signature is invalid.
err := qoresign.VerifyMessage(qoresign.VerifyMessageOptions{ChainID: "qorechain-diana",
Signer: account.Bech32, Data: data, Signature: sig, PqPublicKey: pq.PublicKey})
fmt.Println(account.Bech32, "verified:", err == nil)
}

Go 예제는 짧게 하려고 오류를 무시합니다. 실제 코드에서는 오류를 확인하세요. Rust, Swift, Kotlin도 각 언어의 표기법으로 같은 함수 이름을 사용해 같은 흐름을 따릅니다(개요 참조). Kotlin 및 Android 아티팩트에는 Kotlin 2.3 이상 컴파일러가 필요합니다. Kotlin 2.1을 쓰는 프로젝트는 컴파일에 실패합니다.

6. 커밋된 트랜잭션 읽기(트랜잭션 인스펙터)​

0.1.1에서 새로 추가되었으며 TypeScript용입니다. 읽기만 하고 아무것도 전송하지 않습니다. 커밋된 트랜잭션이 어떻게 보호되었는지 보고합니다. 메시지, 서명자, 수수료, 클래식 서명이 검증되는지 여부, ML-DSA-87 계층의 상태, 보안 등급(pqc, enrollment, shake 또는 classical)이 그 내용입니다. 필드는 트랜잭션 인스펙터를 참고하세요.

// inspect.mjs
import { fetchEvmWindow, fetchPqKeyStatus, inspectTransaction } from "@qorechain/qoresign/network";

const rest = "https://api-testnet.qore.host";
const address = "qor178nscfyp0cq2ac5x7un4u98xrj4ayp6pt0kw08"; // public testnet faucet account

const status = await fetchPqKeyStatus({ rest, address });
console.log("ML-DSA-87 key registered:", status.registered);

const tx = await inspectTransaction({ rest, chainId: "qorechain-diana", txhash: process.argv[2] });
console.log(tx?.tier, tx?.pqLayer, tx?.messages.map((m) => m.typeUrl));

const win = await fetchEvmWindow({ rest, address });
console.log("EVM window:", win.found, win.live);
node inspect.mjs <txhash>

최근 테스트넷 트랜잭션의 해시(예: faucet 전송)를 사용하세요. 그렇게 실행하자 ML-DSA-87 key registered: true, 등급 pqc, pqLayer { present: true, count: 1, algorithmId: 1, verified: true, signBytesVersion: 'v2' }, 메시지 /cosmos.bank.v1beta1.MsgSend, EVM window: false false가 출력되었습니다. inspectTransaction은 네트워크가 모르는 해시에는 undefined를 반환합니다. 테스트넷 노드는 기록을 정리(prune)하므로 오래된 테스트넷 트랜잭션은 없을 수 있습니다.

다음 단계​