Symbol(XYM)とほかのチェーンのお金を、取引所に預けずに交換できないか。できればその相手は Monero(XMR)がいい。Monero は送り先も額も隠れるチェーンなので、XYM → XMR → XYM と往復すれば、Symbol の上の「入りと出」のつながりを Monero の大きな匿名性集合の中に消せる。Symbol の利用者だけで CoinJoin をするより、ずっと大きな群れに紛れられる。
ただ、Monero にはスクリプトも時間ロックもない。ハッシュタイムロック契約で交換する、いつものアトミックスワップは組めない。じゃあどうするのかというと、アダプター署名という訳のわからんステキな何かを使う。相手のチェーンには何も要らず、仕掛けは全部 Symbol 側に置ける。
そこで今回は、Symbol 側の仕掛けだけを、自作の heika(Symbol の Rust のツールキット。試験的にノードも動く)で作ってみた。Monero 側は作っていない。その代わりに「Monero の鍵の半分」をただのスカラーにして、交換が終わったときにそれがそろうことを確かめる。完成した署名を Symbol 界隈で人気筆頭(というかほぼ全部)のノードである catapult が受け入れるところまで、プライベートネットで動いた。やったぜぇ🎉
試作なので、メインネットで使うのはおすすめしない。使って大損害ということになっても大目に見て欲しい。だってそういうあれじゃないんだもん💦。って思ったけど AGPL で無保証だから別に言うまでもなかった。
Monero 側は、鍵を半分ずつ持つだけ
Monero は「秘密鍵を知っている人が使える」だけのチェーンだ。そこで、交換する 2 人(XYM を出す Alice と、XMR を出す Bob)が、それぞれ秘密の半分 a、b を作る。公開するのは点 A、B だけ。そして、使う鍵が a + b になるアドレスに XMR を置く。
a と b の両方を知った人だけが、その XMR を動かせる。最初は誰も動かせない。あとは、XYM が動くのと同時に、相手に秘密の半分が渡るようにすればいい。それを Symbol 側でやる。
Symbol も Monero も Ed25519 なので、同じ曲線の上の点をそのまま使える。ビットコインと Monero の交換では、曲線が違うせいで「同じ秘密を使っている」ことの証明が別に要るのだが、Symbol ならそれが要らない。地味にうれしいポイントだ😊
アダプター署名: 「t を足すと完成する署名」
Ed25519 の署名は (R, s) の 2 つでできている。アダプター署名は、ある秘密 t の分だけわざと足りない署名 ŝ だ。
- 受け取った人は、「
tを足せば正しい署名になる」ことを確かめられる -
tを知っている人だけが、正しい署名にできる - 完成した署名がチェーンに出ると、
ŝを持っている人は差を取ればtがわかる
式はこれだけ。
s = ŝ + t
ポイントは、署名する鍵と、仕込む秘密 t が別のものでいいことだ。Alice は Symbol の自分の鍵で連署を作り、そこに Bob の Monero の半分 b を仕込む。Bob がその連署を完成させて XYM を受け取ると、チェーンに出た署名から Alice は b を取り出せる。Bob が XYM を取ることと、Alice が XMR を取れるようになることが、同じ 1 つの署名で結ばれる。
って私のだいすきな Claude Code が言ってる。私も雰囲気で数学しようとして挫折した。もはやバイブコーダー兼バイブマセマティシャンである💦それなんかいやかも……。でもなんか足し算らしい。よいこみんなはちゃんと数学も勉強するんだぞっ✨全部 AI にまかせてるとこうなっちゃうぞっ💦
Symbol 側の流れ
- 2 人が秘密の半分
a、bを作り、点A、Bを交換する -
返金を先に用意する。 Alice が「2-of-2 から Alice へ全額」を作り、Bob が
Aを仕込んだ連署を返す - Alice が、使い捨てのアカウントにちょうどの額を入れて、Alice と Bob の 2-of-2 にする
- (Monero 側で、Bob が
A + Bに XMR を置く) - Bob が「2-of-2 から Bob へ全額」を作り、Alice が
Bを仕込んだ連署を返す - Bob が
bで完成させて出す。Alice はチェーンからbを取り出し、a + bで XMR を動かせる
Bob が受け取りに来なければ、Alice は返金を出す。返金の連署には A が仕込んであるので、出すと Bob に a がばれ、Bob は自分の XMR を取り戻せる。どちらに転んでも、片方だけが得をする途中の状態はない。
2-of-2 にするのは、Alice が XYM を勝手に引き上げられないようにするためだ。Bob が XMR を置いたあとに Alice が XYM を引き上げたら、XMR は A + B に閉じ込められて二度と出てこない😱
Symbol ならではの気をつけどころ
外側の署名が先
アグリゲートの連署は、外側の署名まで含めたハッシュへの署名だった(heika の実装を読み直して気づいた)。なので、外側の署名者(手数料を払う人)が先に署名し、もう一人がそのハッシュにアダプター連署を作る。外側の署名ができた時点で、手数料、期限、受取人、額が全部決まる。
期限の窓で、返金をあとにずらす
Symbol のトランザクションは、期限の前の maxTransactionLifetime(メインネットは 6 時間)のあいだしか入らない。期限は「この時刻までに」であると同時に、「この時刻の 6 時間前から」でもある。これを時間ロックとして使う。
// 返金は、受け取りの窓が閉じて 30 秒たってから入れられるようになる
let refund_deadline = redeem_deadline + GAP.as_millis() as u64 + lifetime;
受け取りの窓が閉じてから返金の窓が開くので、2 つが同時に使える時間はない。
どちらか一方だけにするのは、残高
受け取りと返金は別々の署名なので、署名だけでは排他にならない。そこで、2-of-2 のアカウントにはちょうどの額だけを入れる。先に入ったほうが残高を使い切るので、もう一方は通らない。
heika で組む
アダプター署名
アダプター署名を作るには、秘密鍵を広げたスカラーや、検証と同じハッシュが要る。これを example の中で書き写すと、heika 本体とずれても気づけない。なので heika-core に、curve25519-dalek の hazmat にならって、hazmat という feature の向こうに Ed25519 の中の部品を出すことにした。heika の署名と検証が使っているものを、そのまま使える。ライブラリの依存関係を辿って同じバージョン入れる虚無の作業を撲滅するためである。Rust を扱う時にそういう時間が一番虚無なのでライブラリを作るときには気をつけようと改めて思った。ほんと気をつけよ……。
heika-core = { path = "../../crates/heika-core", features = ["hazmat"] }
pub fn pre_sign(key: &PrivateKey, message: &[u8], adaptor: &Point) -> Option<PreSignature> {
let t = adaptor.decode()?;
let expanded = ExpandedKey::new(key);
let public_key = expanded.public_point().compress();
// ノンスは鍵の後半と、仕込む点と、メッセージから決める(同じノンスを使い回さない)
let r = Zeroizing::new(Scalar::from_bytes_mod_order_wide(&hazmat::sha512([
&expanded.prefix()[..],
&adaptor.0,
message,
])));
let nonce = (EdwardsPoint::mul_base(&r) + t).compress();
let e = hazmat::challenge(nonce.as_bytes(), public_key.as_bytes(), &[message]);
let s = *r + e * expanded.scalar();
Some(PreSignature { nonce: nonce.to_bytes(), s: s.to_bytes() })
}
hazmat の部品で RFC 8032 のふつうの署名を組み直すと、heika の署名とバイト単位で一致することもテストで確かめた。部品が同じなら、作った署名が heika の検証(catapult と同じ検証)を通るかどうかは、その部品の正しさに乗っかれる。
ちなみに catapult の検証は厳しめで、小さい位数の成分が混ざった点を T に仕込むと、完成させた署名が弾かれる。だから受け取った点は最初にちゃんと確かめる。ここが、catapult と付き合うときに一番気をつけるところだ。
完成させて、連署として付ける
完成させた署名は、ふつうの連署と同じ形で付けられる。heika の attach_cosignature は付けるときに署名を確かめるので、ここで通れば catapult でも通る。
let detached = DetachedCosignature {
cosignature: Cosignature {
signer_public_key: alice_public,
signature: adaptor::complete(&pre, &b),
},
parent_hash: hash,
};
redeem.attach_cosignature(&detached, &seed)?;
動かしてみた
catapult 2 台と heika 1 台のプライベートネットで、受け取り、返金、両方を出す、の 3 つを流した。受け取りはこう。
5. Bob が受け取りのアグリゲートを作って外側に署名し、Alice が B を仕込んだ連署を返す
Bob は、b を足せば Alice の連署になることを確かめた
6. Bob が b で Alice の連署を完成させて、受け取りを出す
→ 受け取りが高さ 37 で承認されました
7. Alice が、チェーンに出た自分の連署から b を取り出す
取り出した b の点 = 6E586628…。a + b の点 = 335F36DC…(A + B と同じ)
Alice は a + b をそろえたので、相手のチェーンのお金を動かせる
catapult の 2 台も高さ 37 に同じブロックを持っていた。RFC 8032 の決まりどおりでない作り方の署名でも、検証の式さえ満たせば本家も素直に受け入れてくれる。もちろんそりゃそうなんだけど、こういうのは実際に通るとやっぱりうれしい😆やったぜぇ🎉仮説はごにょごにょしてはじめてアレになる👓という実に面白い話と通じるものを感じる。
返金は、窓が開くまで 3 分半ほど待ってから出して、Bob が a を取り出せた。受け取りのあとに返金も出すと、2-of-2 の残高が空なので通らない。
ついでに見つかった heika のつらい不具合
「両方を出す」を試したら、通らないはずの返金の状態がいつまでも決まらなかった。heika はブロックを作るときに残高などを確かめて外すのだが、外したものを REST で聞くと 404 を返していた。catapult なら failed と答えるところだ。今は heika も、期限まで failed と答える。こういうのが見つかるから、example を作るのは意義深くて大事😇
作っていないもの
- Monero 側(受け取りを確かめるのに要る、見るための鍵の共有、
A + Bへの入金と取り出し)- だって heika の責務じゃないんだもん
- 2 人のあいだの通信路。example では 1 つのプログラムが 2 人を演じている。本当にやるなら、Tor のような匿名の通信路が要る。通信路で IP がつながったら、Monero で消した跡がそこからつながってしまう
- だって heika の以下同文
- 片方が途中で消えたときの罰の経路
- 匿名性を崩さないための作法。Symbol 側の入りと出は、額と時刻で突き合わされる。額をそろえる、待ち時間をばらつかせる、行きと帰りで相手を変える、などが要る
あと、日本では暗号資産どうしの交換も課税の対象になる。往復するたびに申告が要るので、そこはちゃんとやろう。
おわりに
スマートコントラクトがないどころかスクリプトもない相手方の台帳とでも、署名の仕掛けだけで交換を結べる、というのは多分根っから数学が好きな人には面白いトピックなんだと思う。私はそのあたりちょっと一旦挫折した。Symbol 側は、マルチシグと期限の窓とアグリゲートという、もとからある部品だけで組めた。もちろん Symbol の規則には何も手を入れていない。
Monero 側までつないで本当に往復できるかは、またいつか。気が向いたら。maybe somedays。流動性(相手)がいないという一番大きな問題は、まあ、そのときに誰かが考える🙃だって heika の以下同文。