Zum Hauptinhalt springen

Authenticatoren & delegiertes Ausgeben

Authenticator-Lanes (SDK 0.7.0, Chain v3.1.85) erlauben es einem verknüpften externen Schlüssel — einem Phantom-ed25519-Schlüssel oder einem MetaMask-/EVM-secp256k1-Schlüssel — vom EINEN kanonischen PQC-pflichtigen Konto auszugeben: nach dem Prinzip der geringsten Berechtigung, mit Ausgabenlimits und jederzeit widerrufbar, ohne dass der externe Schlüssel jemals eine ML-DSA-Co-Signatur erzeugt.

Dies ist das SDK-Gegenstück zum Account-Abstraction-Modul der Chain.

Das Relayer-Modell

Ein Relayer reicht die Transaktion ein und bezahlt die Gebühren. Die eigene hybride Signatur des Relayers (klassisch + ML-DSA-87) erfüllt den Ante-Handler auf dem Envelope, sodass die PQC-Signatur des kanonischen Kontos on-chain nicht benötigt wird. Die Autorisierung ist stattdessen die Signatur des verknüpften Schlüssels über einen domänen-separierten, replay-gebundenen Sign-Bytes-Digest.

Phantom / MetaMask key Relayer (pays fees) Chain
───────────────────── ─────────────────── ─────
sign(authSignBytes) ──────────▶ wrap in Msg, sign envelope ──▶ verify authenticator sig
check permission + rule
spend FROM canonical account

Der Relayer ist ein anderes Konto als der Eigentümer und erhöht daher die EVM-Nonce des Kontos nicht.

Die drei Lanes

LaneMessageSign-BytesGibt aus
EVMMsgExecuteEVMevmAuthSignBytesnatives QOR / EVM-Call von der 0x-Adresse des Kontos
NativeMsgExecuteCosmoscosmosAuthSignBytesnatives QOR über x/bank vom Konto
SchlüsselrotationMsgRotatePQCKeyrotationSignBytes(rotiert den PQC-Schlüssel des Kontos)

Die Message-Typ-URLs sind /qorechain.abstractaccount.v1.MsgExecuteEVM, /qorechain.abstractaccount.v1.MsgExecuteCosmos und /qorechain.pqc.v1.MsgRotatePQCKey.

Einen Phantom-Authenticator registrieren

Das Verknüpfen eines Schlüssels ist owner-signiert (eine normale hybride Transaktion des kanonischen Kontos): MsgRegisterAuthenticator benennt den Schlüssel (Schema + rohe Pubkey-Bytes), die gewährten permissions und eine expiryUnix-Sitzungsfrist. Ausgabenlimits werden mit einer SpendingRule über MsgUpdateSpendingRules angehängt:

import { msg } from "@qorechain/sdk";

// The Phantom wallet in the browser:
const phantomPubkey = window.solana.publicKey.toBytes(); // 32-byte ed25519

// 1) Link the key: least privilege ("send" only) + a session expiry.
const register = msg.abstractaccount.registerAuthenticator({
owner: canonicalAccount, // the PQC-required account ("qor1…")
accountAddress: canonicalAccount, // the account the key may act for
scheme: "ed25519", // Phantom keys are ed25519
pubkey: phantomPubkey,
permissions: ["send"], // e.g. "send", "evm", "svm" — never "all" for a hot key
expiryUnix: String(Math.floor(Date.now() / 1000) + 7 * 24 * 3600), // 7 days
label: "phantom",
});

// 2) Bound what it can move: per-tx and daily limits, uqor only.
const limits = msg.abstractaccount.updateSpendingRules({
owner: canonicalAccount,
accountAddress: canonicalAccount,
rules: [
{
id: "phantom-hot",
perTxLimit: "1000000", // ≤ 1 QOR per spend
dailyLimit: "10000000", // ≤ 10 QOR per day
allowedDenoms: ["uqor"],
enabled: true,
},
],
});

// Broadcast BOTH owner-signed (hybrid) — e.g. via the hybrid tx path:
// await signAndBroadcastHybrid({ ..., messages: [register, limits] });

Um einen Schlüssel sofort zu deaktivieren, broadcastet der Eigentümer msg.abstractaccount.revokeAuthenticator({ owner, accountAddress, scheme, pubkey }).

Von Phantom ausgeben (Native-Lane, über einen Relayer)

Sobald der Schlüssel verknüpft ist, baut der Browser eine relayer-fertige MsgExecuteCosmos: buildPhantomExecuteCosmos rekonstruiert den domänen-separierten Digest, lässt ihn von Phantom signieren (signMessage) und liefert die { typeUrl, value }-Message zurück.

Browser (der Phantom-Nutzer):

import { buildPhantomExecuteCosmos } from "@qorechain/sdk";

// window.solana is a Phantom-style wallet: { publicKey, signMessage }.
const msgExecute = await buildPhantomExecuteCosmos({
wallet: window.solana,
relayer: relayerAddress, // who will submit + pay fees
chainId: "qorechain-vladi",
account: canonicalAccount, // the PQC-required owner
to: recipient, // "qor1…"
amount: "100uqor", // single-coin amount string
nonce, // the per-authenticator sequence for (account, pubkey)
});

// Ship `msgExecute` to your relayer service (it is already signed by Phantom):
await fetch("/api/relay", {
method: "POST",
body: JSON.stringify({
typeUrl: msgExecute.typeUrl,
value: {
...msgExecute.value,
pubkey: Buffer.from(msgExecute.value.pubkey).toString("base64"),
signature: Buffer.from(msgExecute.value.signature).toString("base64"),
nonce: msgExecute.value.nonce.toString(),
},
}),
});

Server (der Relayer): signiert den Envelope mit seinem eigenen Konto (hybrid, wie auf dem Native-Pfad üblich) und bezahlt die Gebühren. Die Signatur des Authenticators innerhalb der Message ist die Autorisierung, vom Konto des Eigentümers auszugeben.

import {
createClient,
deriveNativeAccount,
directSignerFromPrivateKey,
} from "@qorechain/sdk";

const client = createClient({
network: "mainnet",
endpoints: {
rpc: "https://rpc.qore.host",
rest: "https://api.qore.host",
},
});

// The relayer's OWN account — a different account than the owner.
const relayer = await deriveNativeAccount(process.env.RELAYER_MNEMONIC!);
const signer = await directSignerFromPrivateKey(relayer.privateKey, "qor");
const tx = await client.connectTx(signer);

// Decode the message from the request, then broadcast it (relayer pays fees).
const result = await tx.signAndBroadcast([msgExecute], { fee });
console.log(result.transactionHash);

Eine lauffähige End-to-End-Version (mit einem lokalen ed25519-Schlüssel als Stellvertreter für Phantom) ist das Beispiel authenticator-spend.

Von MetaMask ausgeben (EVM-Lane)

Ein MetaMask-Schlüssel wird über seine 20-Byte-ETH-Adresse (Schema secp256k1) mit registerEthAuthenticatorMsg verknüpft und autorisiert Ausgaben mit einer 65-Byte-EIP-191-personal_sign-Signatur über dieselbe Art von Digest.

1) Der Eigentümer verknüpft die MetaMask-Adresse (owner-signiert, hybrid):

import { registerEthAuthenticatorMsg } from "@qorechain/sdk";

const [ethAddress] = await window.ethereum.request({
method: "eth_requestAccounts",
params: [],
});

const register = registerEthAuthenticatorMsg({
owner: canonicalAccount,
ethAddress, // 0x-hex 20-byte address = the authenticator pubkey
permissions: ["evm"], // EVM lane only
expiryUnix: Math.floor(Date.now() / 1000) + 24 * 3600, // 24 h session
label: "metamask",
});
// broadcast owner-signed (hybrid), like any other message

2) MetaMask autorisiert einen EVM-TransferbuildMetaMaskExecuteEvm baut den Digest, fordert personal_sign (EIP-191) vom Provider an und liefert eine relayer-fertige MsgExecuteEVM zurück:

import { buildMetaMaskExecuteEvm } from "@qorechain/sdk";

const msgExecute = await buildMetaMaskExecuteEvm({
provider: window.ethereum, // any EIP-1193 provider
address: ethAddress, // the linked 20-byte address (0x-hex)
relayer: relayerAddress,
chainId: "qorechain-vladi",
account: canonicalAccount, // the PQC-required owner
to: "0xRecipient…", // 0x-hex recipient
value: "1000000000000000000",// decimal wei string (EVM lane: 18 decimals)
gasLimit: 100000,
nonce: evmNonce, // the account's CURRENT EVM nonce — do NOT +1
});
// hand `msgExecute` to the relayer, exactly as in the Phantom flow

buildMetaMaskExecuteCosmos funktioniert auf dieselbe Weise für die Native-Lane (to: "qor1…", amount: "100uqor", nonce = die Sequenz pro Authenticator). Es gibt passende Low-Level-Composer — executeEvmMsg, executeCosmosMsg, registerEthAuthenticatorMsg, revokeAuthenticatorMsg, rotatePqcKeyMsg — falls Sie Schlüssel und Signaturen selbst verwalten.

Sign-Bytes (byte-exakt)

Zwei Byte-Helfer: BE64(n) ist eine 8-Byte-Big-Endian-Ganzzahl; LP(bytes) ist BE64(len) ‖ bytes (längenpräfixiert).

EVM-LaneevmAuthSignBytes({ chainId, account, pubkey, to, value, data, nonce }) liefert einen 32-Byte-Digest zurück:

sha256( "qorechain-evm-auth-v1"
‖ LP(chainId) ‖ LP(account) ‖ LP(pubkey)
‖ LP(to) ‖ LP(value) ‖ LP(data) ‖ BE64(nonce) )

to ist der 0x-Hex-Empfänger, value der dezimale wei-String, data die rohen Calldata.

Native-LanecosmosAuthSignBytes({ chainId, account, pubkey, to, amount, nonce }) liefert einen 32-Byte-Digest zurück:

sha256( "qorechain-cosmos-auth-v1"
‖ LP(chainId) ‖ LP(account) ‖ LP(pubkey)
‖ LP(to) ‖ LP(amount) ‖ BE64(nonce) )

amount ist der kanonische Single-Coin-String (z. B. 100uqor).

RotationrotationSignBytes(chainId, algorithmId, account, oldPub, newPub) liefert den String zurück, den beide Schlüssel signieren (dessen UTF-8):

qorechain-pqc-rotate-v1|<chainId>|<algorithmId>|<account>|<oldHex>|<newHex>

Nonces

  • MsgExecuteEVM.nonce = die aktuelle EVM-Nonce des Kontos (der Relayer ist ein anderes Konto und erhöht die Nonce des Eigentümers nicht — also nicht 1 addieren).
  • MsgExecuteCosmos.nonce = die Sequenz pro Authenticator für (account, pubkey) — ein Store-Zähler, getrennt von der eigenen Sequenz des Kontos, der bei jeder erfolgreichen Native-Lane-Ausgabe inkrementiert wird.

Eine falsche Nonce führt zu einer Replay-Ablehnung (abstractaccount Code 11, siehe unten).

// EVM lane: the account's current nonce, straight from the EVM JSON-RPC.
const evmNonce = await client.evm.call<string>("eth_getTransactionCount", [
account0x,
"latest",
]);

Das Permission-Schema

Die Chain veröffentlicht die kanonische Authenticator-Permission-Taxonomie, damit Clients Scopes validieren können, ohne Strings zu hartkodieren, und Abweichungen über schema_version erkennen:

// REST (LCD):
const schema = await client.rest.getPermissionSchema();

schema.schema_version; // bumps on any taxonomy/mapping change
schema.permissions; // ["send", "evm", "svm", "all", ...]
schema.msg_permissions; // { "/qorechain.abstractaccount.v1.MsgExecuteEVM": "evm", ... }
schema.key_management_msgs; // typeURLs NEVER delegable to a linked key

Die REST-Route ist GET /qorechain/abstractaccount/v1/permission_schema; der typisierte gRPC-Query-Client stellt dieselben Daten als clients.abstractaccount.permissionSchema() bereit. Das Modul bedient außerdem /config, /accounts und /accounts/{address}.

Fehlercodes

Fehlschläge werden über decodeTxError mit einem verständlichen kind dekodiert:

CodespaceCodeKind
abstractaccount5spending_limit_exceeded
abstractaccount6session_key_expired
abstractaccount10permission_denied
abstractaccount11authenticator_replay
pqc21hybrid_verify_failed
import { decodeTxError } from "@qorechain/sdk";

const decoded = decodeTxError({
code: result.code,
codespace: result.codespace,
rawLog: result.rawLog,
});

switch (decoded.kind) {
case "spending_limit_exceeded": // over the per-tx or daily SpendingRule
break;
case "session_key_expired": // expiryUnix passed — re-register the key
break;
case "permission_denied": // scope missing — check the permission_schema
break;
case "authenticator_replay": // wrong nonce — refetch and re-sign
break;
case "hybrid_verify_failed": // ML-DSA sig did not verify (see note below)
break;
}

hybrid_verify_failed bedeutet meist eine hedged (nicht-deterministische) ML-DSA-87-Signatur — die Chain akzeptiert nur deterministische Signaturen. Derselbe Fehler tritt auch auf, wenn ein SDK vor 0.6.1 die Hybrid-Extension JSON-kodiert hat (bitte upgraden — siehe Accounts & PQC-Signierung).

Schlüsselrotation

Rotieren Sie den ML-DSA-87-Schlüssel eines Kontos zu einem neuen Schlüssel desselben Algorithmus — zum Beispiel bei der Migration eines Legacy-Schlüssels aus der Chain-Bridge-Ableitung (shake256(mnemonic)) zum kanonischen adressgebundenen Schlüssel (shake256("qorechain:pqc:v1|addr|mnemonic")):

import { rotatePqcKeyMsgFromMnemonic, derivePqcLegacy } from "@qorechain/sdk";

const { msg, oldKeypair, newKeypair } = rotatePqcKeyMsgFromMnemonic({
account,
mnemonic,
chainId: "qorechain-vladi",
// oldDerivation: "bridge" (legacy), newDerivation: "adapter" (canonical) by default
});
// broadcast `msg` BY the account, cosigned (hybrid) with the OLD key —
// both keys dual-sign the rotation bytes (old proves ownership, new proves control).

derivePqcLegacy(mnemonic) reproduziert das Legacy-Schlüsselpaar eigenständig, wenn Sie es benötigen (z. B. um bis zum Abschluss der Rotation weiter zu signieren).

Weiter