XRPLのペイメントチャネル:オフチェーン高速決済ハンズオン

に公開

はじめに

XRP Ledger(XRPL)には ペイメントチャネル という仕組みがあります。

これは「2つのアカウント間で、ブロックチェーンに記録されないやり取りをする」という方法です。ライトニングネットワークのようなオフチェーン決済。

何が嬉しいのか?

  • 超高速 — ブロックチェーン確認を待たない(数ミリ秒)
  • 手数料最小 — 取引ごとの手数料がない(チャネル開設・閉鎖時のみ)
  • スケーラビリティ — 100万回の取引もオンチェーン化しない

この記事で学べること

  • ペイメントチャネルの仕組み(Open → Claim)
  • チャネル内での署名付き支払い
  • チャネルの安全性(署名の使用)
  • 実装コード付きハンズオン
  • ユースケース:マイクロペイメント、ストリーミング決済

ペイメントチャネルの概念

実世界に例えると

Alice と Bob が「共同貯金箱」を作る

1. Alice が 100 XRP を貯金箱に預ける
2. Alice と Bob は何度でも「誰がいくら取る」を更新できる
3. 最終的に合意した配分で、チャネルを閉じる
   → ブロックチェーンに「最終状態」だけ記録

取引ごとに手数料がかかるのは「開設」と「閉鎖」だけ

XRPL の実装

ペイメントチャネルは 3つの状態 を持ちます:

状態 説明 トランザクション
Open チャネル開設 PaymentChannelCreate
Claim 支払い確定 PaymentChannelClaim
Close チャネル閉鎖 PaymentChannelClose

ステップ 1:ペイメントチャネルを開設する

コード例

const xrpl = require("xrpl");

async function createPaymentChannel() {
  const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233");
  await client.connect();

  // Alice(送金者)のウォレット
  const aliceWallet = xrpl.Wallet.fromSeed("sEd7...");

  // Bob(受取人)のアドレス
  const bobAddress = "rU6K7V3Po4snVhBBaU29sesqs2qTQJWDw1";

  // ペイメントチャネル作成トランザクション
  const createChannelTx = {
    TransactionType: "PaymentChannelCreate",
    Account: aliceWallet.address,        // Alice(送金者)
    Destination: bobAddress,               // Bob(受取人)
    Amount: xrpl.xrpToDrops("100"),      // チャネルに預ける額(100 XRP)
    SettleDelay: 86400,                  // 24時間(秒単位)
    PublicKey: aliceWallet.publicKey,    // 署名検証用公開鍵
  };

  // トランザクション送信
  const tx = await client.submitAndWait(createChannelTx, { wallet: aliceWallet });

  console.log("✅ ペイメントチャネル開設成功!");
  console.log("Channel ID:", tx.result.hash);

  // チャネル ID を取得(重要)
  const channelId = getChannelIdFromTx(tx);
  console.log("Channel ID (from ledger):", channelId);

  await client.disconnect();

  return channelId;
}

function getChannelIdFromTx(tx) {
  // トランザクション結果からチャネル ID を抽出
  // 通常、tx.result.CreatedNode から取得
  const createdNode = tx.result.CreatedNodes?.find(
    (node) => node.CreatedNode?.LedgerEntryType === "PayChannel"
  );
  return createdNode?.CreatedNode?.LedgerIndex;
}

createPaymentChannel().catch(console.error);

実行結果

✅ ペイメントチャネル開設成功!
Channel ID: E4F8B8A9C1D2E3F4A5B6C7D8E9F0A1B2C3D4E5F6
Channel ID (from ledger): ABCDEF0123456789ABCDEF0123456789ABCDEF01

重要: チャネル ID は後で使うので保存しておきましょう。


ステップ 2:チャネル内で署名付き支払いを作成

ペイメントチャネルの面白さはここ。ブロックチェーンに記録されない支払い を作ります。

仕組み

Alice が「Bob に 10 XRP 支払う」という署名を作る

その署名を Bob に送る(オフチェーン)

Bob は署名を検証して「本当にAliceからの支払いか」を確認

必要に応じて、最後にチャネルを閉じてブロックチェーンに記録

コード例

const crypto = require("crypto");

async function createSignedClaim(channelId, amount, sequence, alicePublicKey, aliceWallet) {
  // Claim Object を作成(署名対象)
  const claimObject = {
    channel: channelId,
    amount: xrpl.xrpToDrops(amount),  // 支払い額(Drops単位)
    signature: "",                    // ここに署名が入る
    pubkey: alicePublicKey
  };

  // Claim オブジェクトをハッシュ化
  const signedClaim = xrpl.sign(
    {
      channel: channelId,
      amount: xrpl.xrpToDrops(amount),
      signingPubKey: alicePublicKey
    },
    aliceWallet.privateKey
  );

  console.log("✅ 署名付き支払いを作成");
  console.log("Amount:", amount, "XRP");
  console.log("Signature:", signedClaim);

  return signedClaim;
}

// 使用例
const channelId = "ABCDEF0123456789ABCDEF0123456789ABCDEF01";
const aliceWallet = xrpl.Wallet.fromSeed("sEd7...");

createSignedClaim(
  channelId,
  "10",  // 10 XRP を支払い
  1,     // sequence(支払い番号)
  aliceWallet.publicKey,
  aliceWallet
).catch(console.error);

実装上の注意:

  • 署名は XRPL のプロトコルに従う必要があります
  • sequence は増加していく必要があります(再生攻撃防止)
  • 署名検証は Bob が行います

ステップ 3:チャネル内の支払いを確定(Claim)する

Bob が署名を受け取ったら、チャネル上の支払いを確定 します。

コード例

async function claimPayment(channelId, amount, signature, alicePublicKey) {
  const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233");
  await client.connect();

  // Bob(受取人)のウォレット
  const bobWallet = xrpl.Wallet.fromSeed("sEd7...");

  // Claim トランザクション
  const claimTx = {
    TransactionType: "PaymentChannelClaim",
    Account: bobWallet.address,      // Bob が Claim を実行
    Channel: channelId,
    Amount: xrpl.xrpToDrops(amount),
    Signature: signature,             // Alice の署名
    PublicKey: alicePublicKey,       // Alice の公開鍵(検証用)
  };

  // トランザクション送信
  const tx = await client.submitAndWait(claimTx, { wallet: bobWallet });

  console.log("✅ 支払い確定!");
  console.log("Transaction Hash:", tx.result.hash);
  console.log("Claimed Amount:", amount, "XRP");

  await client.disconnect();
}

claimPayment(
  "ABCDEF0123456789ABCDEF0123456789ABCDEF01",
  "10",
  "ABC123...",  // Alice の署名
  "ED5F5F..." // Alice の公開鍵
).catch(console.error);

ステップ 4:チャネルを閉鎖する

チャネルでの取引が終わったら、チャネルを閉鎖 してブロックチェーンに最終状態を記録します。

コード例

async function closePaymentChannel(channelId, finalAmount) {
  const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233");
  await client.connect();

  // Alice または Bob が実行可能
  const aliceWallet = xrpl.Wallet.fromSeed("sEd7...");

  // チャネル閉鎖トランザクション
  const closeTx = {
    TransactionType: "PaymentChannelClose",
    Account: aliceWallet.address,
    Channel: channelId,
    CloseResolution: "Closed",  // 正常に閉鎖
  };

  // または、最終支払い額を明示する場合
  // const finalCloseTx = {
  //   TransactionType: "PaymentChannelClaim",
  //   Account: bobWallet.address,
  //   Channel: channelId,
  //   Amount: xrpl.xrpToDrops(finalAmount),
  //   Close: true,  // チャネル閉鎖と同時に確定
  // };

  const tx = await client.submitAndWait(closeTx, { wallet: aliceWallet });

  console.log("✅ ペイメントチャネル閉鎖!");
  console.log("Transaction Hash:", tx.result.hash);

  await client.disconnect();
}

closePaymentChannel("ABCDEF0123456789ABCDEF0123456789ABCDEF01", "50")
  .catch(console.error);

ユースケース 1:マイクロペイメント

1回 0.001 XRP の支払いを1000回実行したい場合:

通常の支払い:1000回 × 12 drops = 12,000 drops の手数料
ペイメントチャネル:開設と閉鎖の2回分 = 24 drops の手数料

→ 99%以上の手数料削減!

実装例(IoTセンサーの料金徴収)

async function streamingMicroPayments(channelId, aliceWallet) {
  // センサーからのデータを1秒ごとに受信
  // 1回受け取るたびに 0.001 XRP を支払う

  let totalClaimed = 0;

  setInterval(async () => {
    const amount = "0.001";  // 1回の支払い額
    totalClaimed += parseFloat(amount);

    // 署名付き支払いを作成
    const signature = createSignedClaim(channelId, totalClaimed, aliceWallet);

    // センサーに署名を送信(通常の通信でOK)
    console.log(`Payment signature sent: ${totalClaimed} XRP`);

    // 1000回に1回、チャネルをクレーム&再作成
    if (totalClaimed >= 1) {
      // チャネル確定
      await claimPayment(channelId, xrpl.xrpToDrops(totalClaimed), signature, aliceWallet.publicKey);
      totalClaimed = 0;
    }
  }, 1000);
}

ユースケース 2:ゲーム内トランザクション

オンラインゲーム内で 秒単位の課金 を実現:

プレイヤーが敵を倒すたびに小額を支払う
  → 支払い確定はゲーム終了時

実装フロー

async function gamePaymentChannel(playerId, gameSessionId) {
  // ゲーム開始:チャネル作成
  const channelId = await createPaymentChannel();

  // ゲーム実行中:敵を倒すたびに署名を送る
  async function onEnemyDefeated(enemyValue) {
    const amount = (enemyValue / 100).toString();  // XRP に変換
    const signature = createSignedClaim(channelId, amount, aliceWallet);
    
    // サーバーに署名を送信(ほぼ遅延なし)
    await sendSignatureToServer(signature);
  }

  // ゲーム終了:チャネル確定
  async function onGameEnd(totalAmount) {
    await claimPayment(channelId, totalAmount, finalSignature, aliceWallet.publicKey);
    await closePaymentChannel(channelId);
  }
}

セキュリティ考慮事項

1. 署名の検証

Bob は Alice の署名を 必ず検証 してください:

function verifySignature(signedClaim, alicePublicKey) {
  try {
    const verified = xrpl.verify(
      signedClaim.hash,
      signedClaim.signature,
      alicePublicKey
    );
    return verified;
  } catch (e) {
    console.error("Invalid signature!");
    return false;
  }
}

2. Replay Attack(再生攻撃)防止

同じ署名を何度も使われないように sequence を増やす

// ❌ 危険:同じ署名を何度も Claim できる
// ✅ 安全:sequence が増えていく署名のみ Claim 可能

3. SettleDelay の役割

チャネル開設時に設定した SettleDelay

SettleDelay: 86400  // 24時間

= 「チャネルを閉じたい」という意思表示から、
  実際に閉鎖されるまで 24 時間待つ
  (その間、相手がより高い額で Claim できる)

トラブルシューティング

Q: 「Channel Not Found」エラーが出た

A: チャネル ID が間違っているか、チャネルが既に閉鎖されている可能性があります。

// チャネル情報を確認
const channelInfo = await client.request({
  command: "channel_info",
  channel_id: channelId
});
console.log(channelInfo);

Q: 署名検証に失敗する

A: 署名対象の amountchannel が正確に一致していないと失敗します。

// ❌ 間違い:署名時と Claim 時で金額が異なる
createSignedClaim(channel, "10 XRP");
claimPayment(channel, "9.99 XRP");  // 失敗

// ✅ 正しい:額を一致させる
createSignedClaim(channel, xrpl.xrpToDrops("10"));
claimPayment(channel, xrpl.xrpToDrops("10"));

Q: チャネルを途中で閉じたい

A: PaymentChannelClose トランザクション + SettleDelay を待つ。

// ステップ1:閉鎖要求
await closePaymentChannel(channelId);

// ステップ2:SettleDelay 秒待つ(デフォルト 24 時間)

// ステップ3:最終 Claim
await claimPayment(channelId, finalAmount, finalSignature);

実装全体を統合したコード

const xrpl = require("xrpl");
const crypto = require("crypto");

class PaymentChannelManager {
  constructor(senderWallet, receiverAddress) {
    this.senderWallet = senderWallet;
    this.receiverAddress = receiverAddress;
    this.client = new xrpl.Client("wss://s.altnet.rippletest.net:51233");
    this.channelId = null;
    this.totalClaimed = "0";
  }

  async connect() {
    await this.client.connect();
  }

  async disconnect() {
    await this.client.disconnect();
  }

  // チャネル作成
  async createChannel(amountXRP) {
    const tx = {
      TransactionType: "PaymentChannelCreate",
      Account: this.senderWallet.address,
      Destination: this.receiverAddress,
      Amount: xrpl.xrpToDrops(amountXRP),
      SettleDelay: 3600,  // 1時間
      PublicKey: this.senderWallet.publicKey,
    };

    const result = await this.client.submitAndWait(tx, { wallet: this.senderWallet });
    this.channelId = this._getChannelId(result);

    console.log(`✅ Channel created: ${this.channelId}`);
    return this.channelId;
  }

  // 支払い署名を作成
  createPaymentSignature(amountXRP) {
    const amount = xrpl.xrpToDrops(amountXRP);
    // 実装は省略(XRPL の署名プロセスに従う)
    return `SIGNATURE_${amount}`;
  }

  // チャネルを閉鎖
  async closeChannel() {
    const tx = {
      TransactionType: "PaymentChannelClose",
      Account: this.senderWallet.address,
      Channel: this.channelId,
    };

    const result = await this.client.submitAndWait(tx, { wallet: this.senderWallet });
    console.log(`✅ Channel closed: ${result.result.hash}`);
  }

  _getChannelId(tx) {
    // 実装細部は省略
    return "MOCK_CHANNEL_ID";
  }
}

// 使用例
async function main() {
  const aliceWallet = xrpl.Wallet.fromSeed("sEd7...");
  const bobAddress = "rU6K7V3Po4snVhBBaU29sesqs2qTQJWDw1";

  const manager = new PaymentChannelManager(aliceWallet, bobAddress);
  await manager.connect();

  // チャネル作成(100 XRP を預ける)
  await manager.createChannel("100");

  // 10 回の支払いを実行
  for (let i = 0; i < 10; i++) {
    const signature = manager.createPaymentSignature("0.1");
    console.log(`Payment ${i + 1}: ${signature}`);
  }

  // チャネル閉鎖
  await manager.closeChannel();
  await manager.disconnect();
}

main().catch(console.error);

まとめ

ペイメントチャネルは:

超高速(ブロックチェーン確認不要)
手数料最小(開設・閉鎖時のみ)
スケーラブル(サイドチェーンなしで高スループット)
安全(署名による真正性確保)

ライトニングネットワークのようなオフチェーン決済を、XRPLで簡単に実装できます。

次のステップ:

  • Pathfinding(自動ルート探索)で複雑な決済
  • Sidechain で XRP Ledger を拡張
  • DEX(分散取引所)でトークン交換

参考リンク

GitHubで編集を提案

Discussion