🦅

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

2024/06/16に公開

本記事の目的

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

エスクロー(Escrow)決済とは

エスクロー決済という言葉を初めて耳にする方も多いかと思いますが、その内容はとても馴染みのあるもので、以下の図のような取引・決済のことを指します。メルカリなど、C2Cサービスでよく目にする決済方法です。

エスクロー決済の実装方法

EVM XRP Ledger
スマートコントラクトを独自に実装
(またはライブラリを使用して実装)
EscrowCreate トランザクションを発行
+ EscrowFinish トランザクションを発行

XRP Ledger ではエスクロー決済用の API が存在しているため、それを利用するだけですが、EVM 系の場合はスマコンを実装する必要があります。まずは XRP Ledger での実装から見ていきます。

XRP Ledger での実装例

先ほどの図に XRP Ledger 上での操作を加えると、以下のようになります。

  1. fulfillment という鍵のようなものを作成し、それを使って escrowCreate で escrow を作成します。receiver がこの escrow を完了させ預かり金を受け取るためには fulfillment が必要です。そのため sender はこの時点ではまだ fulfillment を公開してはいけません。
  2. receiver は sender が escrowCreate を実行してブロックチェーン上に XRP をロックしたことを確認したら、取引を実行します。この取引には様々なものが考えられますが、例えば物を送るなどがこれに当たります。
  3. sender は receiver が取引を実行したことを確認したら fulfillment を receiver に公開します。
  4. receiver は受け取った fulfillment を利用して escrowFinish を実行し、預かり金を受け取ります。

実際にアプリケーションに組み込む場合はキャンセル可能なタイミングや、取引が実行されたことをどう確認するかなど、考慮すべき点は他にも存在しますが、エスクロー取引の大まかな流れは上記のようになります。また、ある時間を過ぎると引き出せるようになるなど、時間で条件を設定することも可能です。

ここからは上記を実現するための XRP Ledger の機能を1つずつ確認していきます。

エスクローの作成(escrowCreate)

エスクローの作成は escrowCreate を実行することでできます。以下はサンプルコードです。

import * as cc from "five-bells-condition";
import crypto from "crypto";
import {
  Client,
  isoTimeToRippleTime,
  Wallet,
  xrpToDrops,
} from "xrpl";


// 1. このエスクロー取引用の鍵を作成
const preimageData = crypto.randomBytes(32);
const myFulfillment = new cc.PreimageSha256();
myFulfillment.setPreimage(preimageData);

const condition = myFulfillment
  .getConditionBinary()
  .toString("hex")
  .toUpperCase();
console.log("Condition:", condition);

const fulfillment = myFulfillment
  .serializeBinary()
  .toString("hex")
  .toUpperCase();
console.log("Fulfillment:", fulfillment);


// 2. エスクロー取引を作成
const client = new Client("wss://testnet.xrpl-labs.com");

const senderWallet = Wallet.fromSeed("sEd7pHD6SSAmAahdcHUq9SN92q85Zos");
const receiverWallet = Wallet.fromSeed("sEdVXmpogg8XzRcdphu2j4ZPTHpQ7nP");

await client.connect();

const responseCreate = await client.submitAndWait(
  {
    TransactionType: "EscrowCreate",
    Account: senderWallet.address,
    Destination: receiverWallet.address,
    Amount: xrpToDrops(1),
		CancelAfter: isoTimeToRippleTime("2024-07-01T00:00:00Z"),
    Condition: condition,
  },
  {
    wallet: senderWallet,
  }
);

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

まず最初にこのエスクロー取引用の"鍵"(fulfillment と condition)を生成します。その後 EscrowCreate トランザクションを発行します。Destination には受け取り手となるアカウント、Amount には送金したい XRP の量、CancelAfter はキャンセル可能になる日付、Condition には最初に生成した鍵の内の condition の方を指定します。
CancelAfter には未来の日付を入れる必要があります。過去の日付を指定した場合は tecNO_PERMISSION となりエスクローの発行に失敗します。
また、fulfillment は escrowCreate の段階では使用しません。

以下は実行結果です。result.hash にある 0FEA8571B14B185054DC066D482BB2261EED4427B8037896078344E46B4CF747 を後ほど使用します。

Condition: A0258020C90C8CB1C51907AEF16B436B5513D38214D57D03FFB4D7083B144B2240AF4D0F810120
Fulfillment: A0228020A526C5FF3329A5B7FC4B97FFEEFF62146AFF4AC470BE7A55F0F1777875194929

{
  id: 8,
  result: {
    Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
    Amount: '1000000',
    CancelAfter: 773107200,
    Condition: 'A0258020C90C8CB1C51907AEF16B436B5513D38214D57D03FFB4D7083B144B2240AF4D0F810120',
    Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
    Fee: '12',
    Flags: 0,
    LastLedgerSequence: 1561409,
    Sequence: 1560655,
    SigningPubKey: 'ED06CF7B5B17F8A764C94DC03EF5463E7080FB1CF15A5DACCE0FD5911190747BE9',
    TransactionType: 'EscrowCreate',
    TxnSignature: 'E0CA1DB9D8F30F0710112EC25917F8B678D8E583FDAE429C3590DA15BA539BD31419EF1794923BAA7637A3673F442DB542FCDAEA74C0E0271C3E543432CA1809',
    ctid: 'C017D32E00000001',
    date: 771850381,
    hash: '0FEA8571B14B185054DC066D482BB2261EED4427B8037896078344E46B4CF747',
    inLedger: 1561390,
    ledger_index: 1561390,
    meta: {
      AffectedNodes: [
        {
          ModifiedNode: {
            LedgerEntryType: 'AccountRoot',
            LedgerIndex: '07B45C2D569402D92738D0F68F5F1C7E475E294F4F31BC52088DBE5C51F08EC2',
            PreviousTxnID: '7C4D1097AD771C53B864F354997C778F25268C8339093FCBC7B50473AB2FA437',
            PreviousTxnLgrSeq: 1560522
          }
        },
        {
          CreatedNode: {
            LedgerEntryType: 'Escrow',
            LedgerIndex: '0D11AC720CE1D02A3344206EF2E6531967B6870F6270D1795458821BD9BC1066',
            NewFields: {
              Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
              Amount: '1000000',
              CancelAfter: 773107200,
              Condition: 'A0258020C90C8CB1C51907AEF16B436B5513D38214D57D03FFB4D7083B144B2240AF4D0F810120',
              Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2'
            }
          }
        },
        {
          CreatedNode: {
            LedgerEntryType: 'DirectoryNode',
            LedgerIndex: '6661EEA291DE4FDF476177D3770B06DFFDE26BDAA18CA228C24EEBBECC692677',
            NewFields: {
              Owner: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
              RootIndex: '6661EEA291DE4FDF476177D3770B06DFFDE26BDAA18CA228C24EEBBECC692677'
            }
          }
        },
        {
          ModifiedNode: {
            FinalFields: {
              Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
              Balance: '98999844',
              Flags: 16777216,
              OwnerCount: 1,
              Sequence: 1560656
            },
            LedgerEntryType: 'AccountRoot',
            LedgerIndex: 'AD94E8B1F9084B4A58F4368F0B0013024CDE8930219E688204F2FB670D2F0394',
            PreviousFields: { Balance: '99999856', OwnerCount: 0, Sequence: 1560655 },
            PreviousTxnID: 'B2FE79BBCDBCFC04CB63F3EB06F96FCFA09658AB369FC5F71A5A4978D983FB6E',
            PreviousTxnLgrSeq: 1561315
          }
        },
        {
          CreatedNode: {
            LedgerEntryType: 'DirectoryNode',
            LedgerIndex: 'E164F167A4FB20C9EBA3A8D3BD70F20F57EE5D44AFBBCB134D046385C9E8F514',
            NewFields: {
              Owner: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
              RootIndex: 'E164F167A4FB20C9EBA3A8D3BD70F20F57EE5D44AFBBCB134D046385C9E8F514'
            }
          }
        }
      ],
      TransactionIndex: 0,
      TransactionResult: 'tesSUCCESS'
    },
    validated: true
  },
  type: 'response'
}

エスクローの確認

エスクローは object の一種のため account_object で確認することができます。

{
  account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
  account_objects: [
    {
      Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
      Amount: '1000000',
      CancelAfter: 773107200,
      Condition: 'A0258020C90C8CB1C51907AEF16B436B5513D38214D57D03FFB4D7083B144B2240AF4D0F810120',
      Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
      DestinationNode: '0',
      Flags: 0,
      LedgerEntryType: 'Escrow',
      OwnerNode: '0',
      PreviousTxnID: '0FEA8571B14B185054DC066D482BB2261EED4427B8037896078344E46B4CF747',
      PreviousTxnLgrSeq: 1561390,
      index: '0D11AC720CE1D02A3344206EF2E6531967B6870F6270D1795458821BD9BC1066'
    }
  ],
  ledger_current_index: 1561451,
  validated: false
}

エスクローの完了(escrowFinish)

エスクローの完了(預かり金の引き出し)は escrowFinish を実行することでできます。以下はサンプルコードです。

import { Client, Wallet } from "xrpl";

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

const senderWallet = Wallet.fromSeed("sEd7pHD6SSAmAahdcHUq9SN92q85Zos");
const receiverWallet = Wallet.fromSeed("sEdVXmpogg8XzRcdphu2j4ZPTHpQ7nP");

await client.connect();

// Sequenceの取得
const txResponse = await client.request({
  command: "tx",
  transaction:
    "0FEA8571B14B185054DC066D482BB2261EED4427B8037896078344E46B4CF747",
});

console.log(txResponse);
console.log(" ");

const responseFinish = await client.submitAndWait(
  {
    TransactionType: "EscrowFinish",
    Account: receiverWallet.address,
    Owner: senderWallet.address,
    OfferSequence: txResponse.result.Sequence,
    Condition:
      "A0258020C90C8CB1C51907AEF16B436B5513D38214D57D03FFB4D7083B144B2240AF4D0F810120",
    Fulfillment:
      "A0228020A526C5FF3329A5B7FC4B97FFEEFF62146AFF4AC470BE7A55F0F1777875194929",
  },
  {
    wallet: receiverWallet,
  }
);

console.log(responseFinish);

escrowFinish のパラメータに escrowCreate を実行したトランザクションの Sequence を指定する必要があります。そのため最初に result.hash である 0FEA8571B14B185054DC066D482BB2261EED4427B8037896078344E46B4CF747 から Sequence を取得しています。
その後、Sequence と condition、fulfillment を指定し escrowFinish を実行します。この時、fulfillment の値が間違っていると tecCRYPTOCONDITION_ERROR となり失敗します。
以下は実行結果です。

{
  id: 1,
  result: {
    Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
    Amount: '1000000',
    CancelAfter: 773107200,
    Condition: 'A0258020C90C8CB1C51907AEF16B436B5513D38214D57D03FFB4D7083B144B2240AF4D0F810120',
    Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
    Fee: '12',
    Flags: 0,
    LastLedgerSequence: 1561409,
    Sequence: 1560655,
    SigningPubKey: 'ED06CF7B5B17F8A764C94DC03EF5463E7080FB1CF15A5DACCE0FD5911190747BE9',
    TransactionType: 'EscrowCreate',
    TxnSignature: 'E0CA1DB9D8F30F0710112EC25917F8B678D8E583FDAE429C3590DA15BA539BD31419EF1794923BAA7637A3673F442DB542FCDAEA74C0E0271C3E543432CA1809',
    ctid: 'C017D32E00000001',
    date: 771850381,
    hash: '0FEA8571B14B185054DC066D482BB2261EED4427B8037896078344E46B4CF747',
    inLedger: 1561390,
    ledger_index: 1561390,
    meta: {
      AffectedNodes: [Array],
      TransactionIndex: 0,
      TransactionResult: 'tesSUCCESS'
    },
    validated: true
  },
  type: 'response'
}
 
{
  id: 9,
  result: {
    Account: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
    Condition: 'A0258020C90C8CB1C51907AEF16B436B5513D38214D57D03FFB4D7083B144B2240AF4D0F810120',
    Fee: '423',
    Flags: 0,
    Fulfillment: 'A0228020A526C5FF3329A5B7FC4B97FFEEFF62146AFF4AC470BE7A55F0F1777875194929',
    LastLedgerSequence: 1561625,
    OfferSequence: 1560655,
    Owner: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
    Sequence: 1560078,
    SigningPubKey: 'ED0B108D383CBA3E7CB0F82AFCBF83FEB6EA92C7E4DC392AFFE7507BFFF9796B61',
    TransactionType: 'EscrowFinish',
    TxnSignature: '6EA7810FDA9A4AE1DDF3EF42FBB7782EE3B91FF700195214928B4090FBDB17113BA51AFB8EFDD310F8A4F3A43B50B4DEED09CE6A3B1342CB1CFB5CD140602A0F',
    ctid: 'C017D40700770001',
    date: 771851052,
    hash: '5324C82E43403C870F3B8F295367B56EC68E8B0F28C2BF9AAAE946BA1D02D52C',
    inLedger: 1561607,
    ledger_index: 1561607,
    meta: {
      AffectedNodes: [Array],
      TransactionIndex: 119,
      TransactionResult: 'tesSUCCESS'
    },
    validated: true
  },
  type: 'response'
}

無事エスクロー取引を完了させることができました。

エスクローのキャンセル

エスクローはもちろんキャンセルすることも可能です。escrowCancel を使用します。

const responseFinish = await client.submitAndWait(
  {
    TransactionType: "EscrowCancel",
    Account: senderWallet.address,
    Owner: senderWallet.address,
    OfferSequence: txResponse.result.Sequence,
  },
  {
    wallet: senderWallet,
  }
);

EVM(Solidity) での実装例

Solidity の場合 OpenZeppelin に Escrow の実装があります。しかし、4系までは存在していたものの、5系では削除されてしまっています。また、上記で紹介したような条件や時間に応じて引き出しを制御する機能の実装は存在しないため、自分で実装する必要があります。(インターフェースのみ実装があります。)
https://docs.openzeppelin.com/contracts/4.x/api/utils#escrow


今回はエスクロー取引を取り上げました。この複雑な取引を API を数回叩くだけで実現できてしまうのは、開発の容易性だけでなくセキュリティ面でもとても魅力的ですね。

Discussion