🔑

EVM と比較しながら XRP Ledger を理解する #6

2024/06/09に公開

本記事の目的

Ethereum Virtual Machine(EVM) の開発経験や知識は多少あるが XRP のことは全く知らない人を主な対象とし、EVM と比較しながら XRP Ledger を理解するシリーズです。コンセンサスアルゴリズムなどの理論にはあまり焦点を当てず、アプリ開発の中で必要になりそうな部分を中心に EVM と比較しながらコンパクトにまとめていこうと思います。

ウォレット(アカウント)の種類

EVM XRP Ledger
Externally Owned Account (EOA)
Smart Contract Account (SCA)
マスターキーとレギュラーキー

EVM の場合は EOA が最も基本的なウォレットです。EOA はプロトコルレベルで定義されており、全てのトランザクションは EOA から生成されます。公開鍵と秘密鍵のペアで構成されていてオフチェーンで簡単に生成することができますが、シンプルであるが故に不便な部分も存在し、その問題を解消するために最近研究が進められているのが Smart Contract Account (SCA)です。SCA の設計や機能には様々なものがありますが、以下のような EOA では実現が難しい機能を搭載しています。

  • 独自の柔軟なセキュリティルールを定義できる(例えば1回の最大送金量は0.5 ETH までとするなど)
  • 秘密鍵を失くしてもアカウントを復元できる
  • 信頼しているデバイスや個人間でアカウントのセキュリティを共有できる
  • 他のアカウントの Gas 代を代わりに支払う
  • トランザクションをまとめて処理できる

送金量の制限や Gas 代の肩代わりなどは DApp 側で工夫することで実現も可能ですが、ユーザーの秘密鍵の紛失リスクは DApp 側ではどうすることもできません。そのためユーザー自身でアカウントを復元できる機能は非常に魅力的です。

この秘密鍵を紛失してもアカウントを復元できるなどのアカウントの冗長化の機能が XRP Ledger では既に用意されています。それがマスターキーとレギュラーキーです。

レギュラーキーの作成

マスターキーもレギュラーキーその実態は通常の XRP Ledger のアカウントです。マスターキーに SetRegularKey トランザクションでレギュラーキーを設定することで、この2つのアカウントはそれぞれマスターキー、レギュラーキーとして機能するようになります。
例として rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7 をマスターキーとして、レギュラーキーに rM1p399HpVEghUiQQHgkjQKaPHYg3uixB9 を設定してみます。

import { Client, Wallet } from "xrpl";

const masterSecret = "sEdVFtbyUknJKfeFULoeDpf2s3SZcav";
const regularSecret = "sEdTgr5hR6nztyr3wccXcuWpwvcaVis";

const masterWallet = Wallet.fromSeed(masterSecret);
const regularWallet = Wallet.fromSeed(regularSecret);

const client = new Client("wss://testnet.xrpl-labs.com");
await client.connect();

const response = await client.submitAndWait(
  {
    TransactionType: "SetRegularKey",
    Account: masterWallet.address,  // 割当先のアドレスを指定
    RegularKey: regularWallet.address,  // レギュラーキーとするアドレスを指定
  },
  { wallet: masterWallet }  // 割当先のアドレスでトランザクションを発行
);

console.dir(response, { depth: 10 });

トランザクションが成功すると以下の内容が表示されます。meta.AffectedNodes 内の ModifiedNode の内容から、rM1p399HpVEghUiQQHgkjQKaPHYg3uixB9 をレギュラーキーに設定できたことがわかります。(AffectedNodes の中にこのトランザクションで変更されたオブジェクトの内容が含まれます。)

response
{
  id: 10,
  result: {
    Account: 'rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7',
    Fee: '12',
    Flags: 0,
    LastLedgerSequence: 1363768,
    RegularKey: 'rM1p399HpVEghUiQQHgkjQKaPHYg3uixB9',
    Sequence: 336344,
    SigningPubKey: 'ED7D9EE060A38A968CDF5E091288BE014FDEA79742A71C65C4649088F364F3D1F5',
    TransactionType: 'SetRegularKey',
    TxnSignature: '840E55E483B5426AA5504EEC860D38F47EBBAEAE4737D791D05AC91D1C8BD2E30F6DE07209EFED2501914B687E3D99F349C12B5ACCCA3212922AE78470582904',
    ctid: 'C014CF2600000001',
    date: 771222112,
    hash: 'D660DAB42F1A9217E8CD18E654A8A5BE822C6CA90694A6BC6C1047C1764BD788',
    inLedger: 1363750,
    ledger_index: 1363750,
    meta: {
      AffectedNodes: [
        {
          ModifiedNode: {
            FinalFields: {
              Account: 'rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7',
              Balance: '1033999952',
              Flags: 65536,
              OwnerCount: 0,
              RegularKey: 'rM1p399HpVEghUiQQHgkjQKaPHYg3uixB9',
              Sequence: 336345
            },
            LedgerEntryType: 'AccountRoot',
            LedgerIndex: 'F6D930E6A6BC68403495325F1FE6A60271F817A2D7A6EA2DC64024A7547BADDF',
            PreviousFields: { Balance: '1033999964', Flags: 0, Sequence: 336344 },
            PreviousTxnID: '8BAD6B300CD20B79B4BFA7AADCE58135FA6FD7A6A2828B59C3BB5018F9A1266F',
            PreviousTxnLgrSeq: 381782
          }
        }
      ],
      TransactionIndex: 0,
      TransactionResult: 'tesSUCCESS'
    },
    validated: true
  },
  type: 'response'
}

レギュラーキーの使い方

レギュラーキーはマスターキーのトランザクションを代わりに承認することができます。(一部承認できない種類のトランザクションも存在します。)つまりレギュラーキーさえあればマスターキーの秘密鍵を使わずに操作することが可能です。
この特性を活かして、例えば以下のような運用をすることで資産の漏えいリスクを下げることができます。

  • マスターキーの秘密鍵はオフラインの安全場所に保管し、普段は使用しない
  • トークンなどの資産はマスターキーに所有させる
  • 普段のマスターキーのトランザクションはレギュラーキーで承認する
  • レギュラーキーは定期的にリフレッシュ(変更)する

資産を保有するアカウントと、トランザクションを承認するアカウントを別にできるため、後者を定期的にリフレッシュできることがポイントです。

レギュラーキーで NFT をミント

例としてマスターキーの NFT ミントのトランザクションをレギュラーキーから発行してみます。

import { Client, Wallet, convertStringToHex } from "xrpl";

const masterSecret = "sEdVFtbyUknJKfeFULoeDpf2s3SZcav"
const regularSecret = "sEdTgr5hR6nztyr3wccXcuWpwvcaVis"

const masterWallet = Wallet.fromSeed(masterSecret)
const regularWallet = Wallet.fromSeed(regularSecret)

const client = new Client("wss://testnet.xrpl-labs.com")
await client.connect()

const response = await client.submitAndWait(
  {
    TransactionType: "NFTokenMint",
    Account: masterWallet.address,  // 発行者にマスターキーを指定
    TransferFee: 10 * 1000,
    NFTokenTaxon: 0,
    Flags: 1 + 2 + 8,
    URI: convertStringToHex("https://xrpl.org/ja/")
  },
  { wallet: regularWallet }  // レギュラーキートランザクションを発行
);
console.log(response)

上記を実行すると、レギュラーキーを割り当てる前は以下のように tefBAD_AUTH エラーとなります。この段階では双方ともなんの関係もない2つのアカウントなので当然の結果です。

XrplError: The latest ledger sequence 1363724 is greater than the transaction's LastLedgerSequence (1363723).
Preliminary result: tefBAD_AUTH
    at /Users/hogehoge/xrpl-sandbox/node_modules/xrpl/dist/npm/sugar/submit.js:45:19
    at Generator.next (<anonymous>)
    at fulfilled (/Users/hogehoge/xrpl-sandbox/node_modules/xrpl/dist/npm/sugar/submit.js:5:58) {
  data: undefined
}

先ほど紹介したレギューラーキーの割り当てを実行後に再度同じトランザクションを発行すると、今度はミントのトランザクションに成功します。000B27106453BC8D257444236895925A2B5B76266CD8DED81DC45474000521D9 がミントされました。

{
  id: 10,
  result: {
    Account: 'rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7',
    Fee: '12',
    Flags: 11,
    LastLedgerSequence: 1363773,
    NFTokenTaxon: 0,
    Sequence: 336345,
    SigningPubKey: 'EDFBF38FE259B4693CC712B1CA99ACB870ACEB0FFD5401BF4310E8A3B43C935E16',
    TransactionType: 'NFTokenMint',
    TransferFee: 10000,
    TxnSignature: '470F84E131BFD1805BFDBBE8C58240125730B83B75354F74850328D9BC47B79790836C5246137630D08D952EFBCF627F1BCFE6757B7C43882496C633118E7E0C',
    URI: '68747470733A2F2F7872706C2E6F72672F6A612F',
    ctid: 'C014CF2B00020001',
    date: 771222131,
    hash: 'FFF779B4031CAFF205807E1A667F082E0C7E378217CE4784C2B26A1480D57401',
    inLedger: 1363755,
    ledger_index: 1363755,
    meta: {
      AffectedNodes: [Array],
      TransactionIndex: 2,
      TransactionResult: 'tesSUCCESS',
      nftoken_id: '000B27106453BC8D257444236895925A2B5B76266CD8DED81DC45474000521D9'
    },
    validated: true
  },
  type: 'response'
}

マスターキー (rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7) の保有する NFT を確認してみると、先ほどミントした NFT が表示されます。 Issuer もレギュラーキーではなくマスターキーとなっています。

{
  account: 'rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7',
  account_nfts: [
    {
      Flags: 11,
      Issuer: 'rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7',
      NFTokenID: '000B27106453BC8D257444236895925A2B5B76266CD8DED81DC45474000521D9',
      NFTokenTaxon: 0,
      TransferFee: 10000,
      URI: '68747470733A2F2F7872706C2E6F72672F6A612F',
      nft_serial: 336345
    }
  ],
  ledger_hash: 'F11F1C39B74F6300BD2DFCE084620783A659922628320E90B1729538D03890A8',
  ledger_index: 1364743,
  validated: true
}

レギュラーキーを削除、変更する

変更は作成時と同じトランザクションを発行することで上書き変更することができます。
削除する場合は以下のように RegularKey フィールドを削除して SetRegularKey トランザクションを発行します。

import { Client, Wallet } from "xrpl";

const masterSecret = "sEdVFtbyUknJKfeFULoeDpf2s3SZcav";

const masterWallet = Wallet.fromSeed(masterSecret);

const client = new Client("wss://testnet.xrpl-labs.com");
await client.connect();

const response = await client.submitAndWait(
  {
    TransactionType: "SetRegularKey",
    Account: masterWallet.address,
    //RegularKey: regularWallet.address,  // 削除する場合は RegularKey を指定しない
  },
  { wallet: masterWallet }
);

console.dir(response, { depth: 10 });
response(一部のみ抜粋)
meta: {
      AffectedNodes: [
        {
          ModifiedNode: {
            FinalFields: {
              Account: 'rw97tJMpQ5e1Sceq236M7di4wsS8RJZYx7',
              Balance: '1033999892',
              FirstNFTokenSequence: 336345,
              Flags: 65536,
              MintedNFTokens: 1,
              OwnerCount: 1,
              Sequence: 336350
            },
            LedgerEntryType: 'AccountRoot',
            LedgerIndex: 'F6D930E6A6BC68403495325F1FE6A60271F817A2D7A6EA2DC64024A7547BADDF',
            PreviousFields: {
              Balance: '1033999904',
              RegularKey: 'rM1p399HpVEghUiQQHgkjQKaPHYg3uixB9',
              Sequence: 336349
            },
            PreviousTxnID: '47BBEF8D314FE3D5EF980E47F39E467728D6FBC0194A09303C49C8091B069561',
            PreviousTxnLgrSeq: 1365369
          }
        }
      ],
      TransactionIndex: 0,
      TransactionResult: 'tesSUCCESS'
    },

EVM では後方互換性なども考慮する必要があり苦労をしながら創意工夫を図っているウォレットの復元ですが、XRP Ledger では既に実装されていて、しかも簡単に使える点は非常に魅力的ですね。

Discussion