Waves - payment channel的なコントラクト

内容が古い可能性があります。
参考にする場合は、公式サイト、ドキュメントも合わせて確認してください。
このブログでの最新内容は、ページ末尾の「Waves」タグから辿ってください。

Stellarのstate channel(リンク)を読んで、 RIDEでやれそうな気がしたので、ちょっと考えてみる

何ができれば良いか

  • 資金をロックしてチャンネルオープン。オープンできなかったら資金は回収できる
  • (オフチェーンで)二人だけが共有する状態を二人の合意のもと更新していき、最後の状態(になるトランザクション)のみをオンチェーン(ブロックチェーン)に発行する。
  • 合意してチャンネルクローズした場合は、即座にオンチェーンへ反映される。
  • クローズ前に相手との連絡が途絶えたら、最後に合意した状態がオンチェーンへ反映される。
  • 相手が最新ではない都合の良い状態を勝手に反映させようとしたら、より新しい状態で更新できる。(裏切りに対するタイムロック)
  • オンチェーンへ反映される状態は一つのみ

RIDEで実現できるか

  • チャンネルオープン失敗したら、資金回収できる
    -> 発行済トランザクションは特定できる
  • 複数アカウントへの残高反映
    -> MassTransferTransaction
  • 二人の合意
    -> マルチシグできる
  • 一つのみ反映
    -> データトランザクションによって、特定のトランザクションのみ許可できる
  • 最新のみ反映
    -> データトランザクションでシーケンス制御できる
  • タイムロック
    -> ブロック長で制限できる

ということで、 チャンネルアカウントを作成し、以下コントラクトを付与すれば実現できそうだ

# payment channelする二つのアカウントの公開鍵
let pk1 = base58'<アカウント1の公開鍵>'
let pk2 = base58'<アカウント2の公開鍵>'
# payment channel アカウント
let ac = Address(base58'<ペイメントチャンネルアカウントのアドレス>')

# 特定トランザクションのみ許可を制御する用のパラメータ
let keyTxid = "txid"
let keyLockHeight = "lockHeight"
let keySequenceNo = "sequenceNo"

let txid = match getBinary(ac, keyTxid) {
  case v: ByteVector => v
  case _ => base58''
}

let lockHieght = match getInteger(ac, keyLockHeight) {
  case v: Int => v
  case _ => 9223372036854775807
}

let sequenceNo = match getInteger(ac, keySequenceNo) {
  case v: Int => v
  case _ => 0
}

# 常に二人でマルチシグ
let multisig = sigVerify(tx.bodyBytes, tx.proofs[0], pk1) && sigVerify(tx.bodyBytes, tx.proofs[1], pk2)
multisig || match tx {
  case tx: MassTransferTransaction =>
    # タイムロック解除されたら指定のトランザクションを発行できる
    let agreement = if txid == tx.id then true else false
    let afterLock = height > lockHieght
    agreement && afterLock
  case tx: DataTransaction =>
    # 特定の key-value ペアを要求する
    if size(tx.data) >= 3 then {
      let d1 = if tx.data[0].key == keyTxid then {
        match tx.data[0].value {
          case v: ByteVector => true
          case _ => false
        } } else false
      let d2 = if tx.data[1].key == keyLockHeight then {
        match tx.data[1].value {
          case v: Int => true
          case _ => false
        } } else false
      let d3 = if tx.data[2].key == keySequenceNo then {
        match tx.data[2].value {
          # シーケンス番号は増えていること
          case v: Int => if sequenceNo < v then true else false
          case _ => false
        } }  else false
      
      d1 && d2 && d3
    } else false
  case tx: TransferTransaction => 
    # 入金予定の残高
    let fundingBalance = 1000
    # このブロック高までに入金する
    let fundingLockHeight = 1115984
    # 入金期限までに予定の金額集まらなかったら返金できる
    if (wavesBalance(ac) < fundingBalance) && (height > fundingLockHeight) then {
       #入金分だけ取り戻せる
        let fundedTxid  = tx.proofs[2]
        let sendTo = addressFromRecipient(tx.recipient)
        match extract(transactionById(fundedTxid)) {
          case fundedTx: TransferTransaction => 
             let fundedTo = addressFromRecipient(fundedTx.recipient)
             let fundedFrom = addressFromPublicKey(fundedTx.senderPublicKey)
             let cond1 = if fundedTo == ac then true else false
             let cond2 = if fundedFrom == sendTo then true else false
             let cond3 = if fundedTx.amount == tx.amount then true else false
             cond1 && cond2 && cond3
          case _ => false
         }
    } else false
  case _ => false
}

シナリオ

チャンネルオープン

  1. ペイメントチャンネルするアカウントa1, a2で入金金額(Wa1, Wa2)と期日を決め、ペイメントチャンネルアカウントapcを作成(SetScriptTransaction(前述のスクリプト))する
  2. 三つのトランザクションを用意し、a1, a2で署名する
    1. TransferTransaction(apc -> a1, Wa1)
    2. MassTransferTransaction(apc -> a1: Wa1, apc -> a2: Wa2)
    3. DataTransaction(“txid”=上記2のTxID, “lockHieght”=所望のタイムロック, “sequenceNo”=1)
  3. a1入金する(TransferTransaction: a1 -> apc: Wa1)
  4. a2入金する(TransferTransaction: a2 -> apc, Wa2)

発行されるトランザクションは1, 3, 4。全部行われたらチャンネルオープン。
2-1が払い戻し用で、4がなかったらタイムロック解除後に2-1を発行して3の入金分を回収する。
3, 4の入金額が申告より少なかったら、回収不可になる。これは自業自得。
2-2, 2-3はチャンネル上での支払いの最初の状態になる。

チャンネル上での支払い

  1. 支払いのやりとり都度、以下二つを用意しa1, a2で署名する。
    支払いnのとき、a1, a2それぞれの残高Wa1n, Wa2nとして
    1. MassTransferTransaction(apc -> a1: Wa1n, apc -> a2: Wa2n)
    2. DataTransaction(“txid”=上記2のTxID, “lockHieght”=所望のタイムロック, “sequenceNo”=1+n)

これらのトランザクションは、正常であれば発行されない。
相手と連絡が取れなくなって支払いnのトランザクションセットを得られなかったら、 支払いn-1の1-2 -> タイムロック解除待ち -> 1-1 と処理する。
n-1以下のトランザクションセットを相手が勝手に発行しようとしたら、
タイムロック解除前、1-1を発行される前に、支払いnの1-2を発行し上書きする

チャンネルクローズ

ほぼ前述のチャンネル上での支払いと同じ。
ただし、1-2 lockHeightを直近の値にする。
1-2 -> 1-1 とトランザクション発行してクローズとなる

留意点

この例では考慮していないが、実際にはトランザクション手数料がかかる。

タイムスタンプについて

署名対象(およびトランザクションID算出元)バイトはタイムスタンプを含むので、トランザクション生成時、タイムロックのブロック長に合わせていい感じに設定しておく必要がある。
または、RIDEではバイト列に対してdrop(), take()で一部分を取得したり、+で連結したりることができるので、タイムスタンプを除いた部分に対して署名やID算出することができる。(タイムスタンプは発行時に付与する)
ただしこの場合は、attachmentなりで乱数を含んでおかないと同じ値が生成されるので注意

例えばMassTransferTransactionの場合、
末尾にtimestamp(8) + fee(8) + attachmentLength(2) + attachmentBytes(n)と付いているので
以下の様にすれば、タイムスタンプ除外した署名検証ができる

match tx {
  case tx: MassTransferTransaction =>
    let bytesForSign = dropRight(tx.bodyBytes, size(tx.attachment) + 18) + tx.attachment
    sigVerify(bytesForSign, tx.proofs[0], tx.senderPublicKey)
  case _ => false
}

所感

試してはいない。動きそうではある。
ほとんどの用途には対応できる、という触れ込みだったが
その実感を得た気がしなくも無い。

あと、ちょくちょくと更新続けられていて、ドキュメントと実装が一致していないし、昔のが動かなくなったりで安定していない。
現時点でやる場合は、都度フルノードのコードを参照しなくてはならない。

以上


See also