English here
去年の終わりに公開した記事**『IOTA:【旧版】MAMとは。IOTAのIoT性。』**が早くも古くなってしまったので最新版を書くことにした。また、そちらの記事はいろんなアイデアがゴチャゴチャしてMAMの実態を掴みにくい構造になっていたので頑張って分かりやすくなるように努力した。
本記事のMAMを試したこちらの記事『IOTAのMAMを試してみる。』ではライブラリの使い方を説明した。
IOTA(アイオータ )とは
おなじみ初心者向けの解説サイトを念のためここに列挙しておく。
IOTA日本語ファンサイト 実質IOTA日本公式サイト。というのもIOTA公式の情報を日本語訳して掲載しているからである。初心者向けの情報も多く掲載されている。
ホワイトペーパー英語 日本語 Tangleについての概要は序盤までで、それ以降は安全性、想定される攻撃とその耐性について高度な数学で説明している。後半は初心者向きではない。
Redditの初心者向けスレッド(英語) 僕はここから始まった。
IOTA Guide(英語) ホワイトペーパーが理学部なら、これは工学部。
前提知識
IOTA:【入門】トランザクション大解剖!ウォレットは裏で何をやっているか。
IOTA【技術解説】署名と承認。 - 改訂版
MAM とは
MAMはTangle上の情報のストリームである。世の中の情報を発信する色々なメディア、ラジオやテレビ、新聞、天気予報や監視カメラを想像してみる。それらは時々刻々と新しい情報を名前のついた一括りのコンテンツとして発信しているはずだ。
チャンネル
例えばYoutubeのチャンネルのように、MAMもその単位をチャンネルと呼ぶ。MAMを閲覧するときは見たいチャンネルにアクセスし、また投稿するときは自分のチャンネルに投稿する。ここで自分というのは、自分のSeedという意味である。つまり、チャンネルのアイデンティティもまたSeedに由来する。(IOTAにおけるSeedは全てである。プライバシーと個人の所有権の全てがここに詰まっている。)
チャンネルのモード
チャンネルにMAMを発行する際、チャンネル所有者は3つの投稿タイプを選ぶことができる。誰でも見れるPublic(公開)、自分(Seed所有者)しか見れないPrivate(非公開)、特定の人にだけみることのできるRestricted(限定公開)。ここでいう特定の人というのは、チャンネル所有者が設定した**sideKey(チャンネル鍵)**を教えられた人のみという意味である。
Public(公開):保管されたメッセージはアドレスを使って復号。(アドレス自身が復号に使われるため、アドレスが分かればメッセージも見れる。)
Private(非公開):アドレス=hash(root)
。rootを使って復号。(アドレスはハッシュ化されており、ハッシュ化前の生root
を知っていないと、例えアドレスにアクセスしてもそのメッセージを復号できない。)
Restricted(限定公開):アドレス=hash(root)
。keyを使って復号。(rootを教えることはPrivateチャンネルを公開することになるため、あくまでroot+keyを限定公開する。そのroot+keyを知っている人だけがRestrictedチャンネルにアクセスしてメッセージを復号できる。)
メッセージチェーン
IOTA自体に元々メッセージをトランザクションに貼り付ける機能はあった。しかし、そのままだとメッセージの連続性は表現できない。例えば、15分ごとに気温をTangleに記録していくとする。最初にアドレスAにメッセージ12:15 06/02, 18.2
という気温情報を送った。しかし、次のメッセージをまたアドレスAに送るとすると、この気温情報のストリームはアドレスAがずっと保持しなければならず第三者にその情報が筒抜けである。また、誰かからの妨害メッセージ付きトランザクションをアドレスA宛に送金されるなどの恐れも出てくる。
しかしMAMは一つの投稿を別々のアドレスに送っても、紐解くように連続した情報のストリームを取り出すことができるように設計されているためそれらの心配はいらない。
MAMのメッセージチェーンには言うなれば世代の概念がある。ある世代から次の世代へとMAMメッセージは一方通行に鎖に繋がれていく。
基礎
MAMも送金と同じようにBundleに必要情報をまとめてTangleに保管される。MAMのBundleは大きく分けて署名部とMAM部に分かれる。それらはトランザクションのsignatureFragment
として保管される。署名というのはMAMの発行者を見分けるため=MAMメッセージの本人確認に使われる。詳しくは後述する。
MAMを閲覧する際、閲覧者はMAMをTangle上から探すためにroot
やsideKey
と呼ばれる値をMAM発行者から与えられる。公開モードではroot
のみ。限定公開モードではroot
とsideKey
を与えられる。
アドレス生成
アドレスというのはMAMメッセージが保管されるTangle上の場所である。これはroot
を使って計算される。
if (channel.mode !== 'public') {
// 公開モード以外は アドレス=hash(root)
address = Crypto.converter.trytes(Encryption.hash(81, Crypto.converter.trits(mam.root.slice())));
} else {
address = mam.root;
}
おことわり
Paul Handyさんの記事では限定公開のアドレス=hash(root+sideKey)
となっているがソースコード内に認められないので、本記事ではソースコードにのっとり、アドレス=hash(root)
で統一した。これは誤っている可能性があるので、随時確認いたしますが詳しい方にコメント欄等で報告していただけると大変助かります。(追記:実際にAPIで試して比べてみましたが、やはりアドレス=hash(root)
のようです。)
公開モード
root
がメッセージの保管アドレス。
root
でメッセージを復号する。
ここでnextRoot
というのは、MAMのメッセージチェーンのつなぎ目となる値である。一つのメッセージを復号するとそれに含まれるnextRoot
という値によって閲覧者は続きのメッセージにたどり着くことができる。RPG風に例えるなら、最初の鍵で開けた宝箱の中に、次の宝箱を開ける鍵を入れるようなものだ。これを繰り返すことで、最初のジェネシス世代のroot
を知っている人はメッセージの復号によって得られるnextRoot
を使って、チャンネルのメッセージチェーン上の全ての世代を連鎖的に追っていくことができる。
また、チェーン上の途中の世代のメッセージを開けるnextRoot
を所有している人は、それを使って途中からチャンネル閲覧を始めることができる。しかしその場合、過去の世代のメッセージに遡ることはできない。
限定公開モード
hash(root)
がメッセージの保管アドレス。
sideKey
でメッセージを復号する。
メッセージチェーンの仕組みは公開モードと変わらない。ただ、sideKey
という復号鍵を閲覧者は所有していなければメッセージの内容を読むことができない。
チャンネルの所有権問題
チャンネルに初投稿したAさんはBさんにチャンネルを見て欲しいと考えた。そこで、AさんはBさんにroot
を教えた。BさんはAさんにもらったroot
でアドレス=hash(root)
を訪れ、メッセージをsideKey
で復号しAさんの投稿を閲覧した。続きが気になったBさんは復号の際手に入ったnextRoot
で次のアドレス=hash(nextRoot)
を取得し、次のメッセージを見ることにした。しかし、Aさんはまだ次の投稿をしていなかったので、そのアドレス宛てにはまだ何のトランザクションも発行されていなかった。
BさんはAさんに嫌がらせをしたいと突然思いついた。見渡すとBさんの手には、nextRoot
とsideKey
があった。これがあれば、Bさんは次のメッセージをBさんが勝手に決めて、sideKey
で暗号化して次のアドレス=hash(nextRoot)
宛てにAさんより先に投稿すれば、Aさんのチャンネルを乗っ取れると考えた。
これは上手くいくだろうか?今までの説明ではこの盲点をカバーできていない。ここでMAMにも送金と同じように署名という概念が必要になる。
ここでIOTAにおける、アドレスの残高の所有権はどう証明したかを思い出してみよう。アドレスの残高を引き出す際には、Seedから生成されるPrivate Keyで署名したトランザクションが承認されることで残高を引き出すことができた。
MAMではPrivate Keyをroot
生成に使ったMerkle Treeと組み合わせることでチャンネルの所有権を証明するのに利用する。
Merkle tree based signature scheme
MAMにおける**Merkle Tree(マークル木)**を使った独特な署名方法について説明をしたい。Merkle tree based signature schemeと言う技術らしいので興味ある人はリンク先を読んで見ると良い。
投稿 - 基礎
MAMを投稿するにあたってまず必要となるのは投稿する場所=アドレスだ。MAMは上で説明した通り、root
という値がアドレスに直接関係するため、まずroot
の生成方法から説明する。ここで先ほどちらっと出てきた**マークル木(Merkle Tree)**の説明も入る。
/*
SEED: シード
MESSAGE: 送りたいメッセージ
SIDE_KEY: sideKey
CHANNEL: オブジェクト{security,start,count,index}
*indexはbranch_index(後述)
*/
function createMessage(SEED, MESSAGE, SIDE_KEY, CHANNEL) {
if (!SIDE_KEY)
SIDE_KEY = `999999999999999999999999999999999999999999999999999999999999999999999999999999999`
// MAM settings
let SEED_trits = string_to_ctrits_trits(SEED)
let MESSAGE_trits = string_to_ctrits_trits(MESSAGE)
let SIDE_KEY_trits = string_to_ctrits_trits(SIDE_KEY)
const SECURITY = CHANNEL.security
const START = CHANNEL.start
const COUNT = CHANNEL.count
const NEXT_START = START + COUNT
const NEXT_COUNT = CHANNEL.next_count
const INDEX = CHANNEL.index
const HASH_LENGTH = 81
// set up merkle tree
let root_merkle = iota_merkle_create(SEED_trits, START, COUNT, SECURITY)
...
root
の取得
root
とはマークル木のルート(根)を意味する。そのためroot
取得にはまずMerkle Treeを生成する必要がある。
MAMのMerkle TreeはSeedから生成する。(種が木へ!)Merkle Treeはstart
とsize
という引数をとる。これはSeedから生成されうるaddress
の連鎖のどの部分をMerkle Treeの葉として使うかに用いられる。SeedからPrivate Keyを生成し、address
を生成するために、index
という引数をとったことを覚えているだろうか?(忘れてしまった人のための記事はこちら。)下の図のA、B、C、Dはそれぞれindex
が0~3で生成されるPrivate Keyだ。A'、B'、C'、D'は該当Private Keyから生成されるaddress
である。
Merkle Treeの葉A"、B"、C"、D"にはaddress
にハッシュ関数を通したものを用いる。葉からroot
へ枝が根に集まるようにハッシュ関数に通していく。図で言うと下のノードから葉の方向へ逆生成させることはできない。
こうして取得できるroot
をアドレスとして(限定公開モードならroot
にハッシュ関数をかける)使う。
投稿 - MAM部
まず、MAMとして送りたいメッセージ(messsage
)はこのMAM部に含まれる。message
となりうるものは、気温データなどのセンサー情報や文字データである。現在のところ日本語などの全角文字には対応しておらず生データはasciiコードで用意する。ライブラリのasciiToTrytes.js
のtoTrytes(ascii)
で変換してmessage
に格納する。
nextRoot
の取得
一世代のMAMメッセージにつきMerkle Treeは実は二つ生成される。その世代と次の世代の分だ。どちらとも生成方法は同じである。ソースコードでいうとパラメーターはSTART
とCOUNT
が渡される。
const START = CHANNEL.start // 一つ目のMerkle Treeの開始index。
const COUNT = CHANNEL.count // 一つ目のMerkle Treeの終了index(の一個手前)。
const NEXT_START = START + COUNT // 二つ目のMerkle Treeの開始index。
const NEXT_COUNT = CHANNEL.next_count // 二つ目のMerkle Treeの終了index(の一個手前)。
const INDEX = CHANNEL.index // 枝番号(後述)
第0世代のtree0
はstart0 = START
、end0 = COUNT
。第1世代のtree1
はstart1 = START+COUNT
、end1 = CHANNEL.next_count
。言い換えると生成されるPrivate Keyの連鎖で1つ目のMerkle Treeが使っていない次の部分を使う。下図参考。
ちなみに全ての世代のMerkle Treeが同じサイズである必要はない。
枝番号 branch_index
(旧MAMの記事では葉番号と仮に名付けたが後ほど説明するチェーンフォークの枝分かれを考えると**枝番号(branch_index
)**という名前に早くも変更させていただいた。)
枝番号はroot
取得の際に使ったMerkle Treeの葉から任意にindex
を選んで決める。
先ほどの例で言うと、Merkle Treeはstart=0
、count=4
で生成されたのでbranch_index
は0,1,2,3
のどれかの任意の値だ。
ちなみにbranch_index
は整数である。上の図で見るとPrivate Keyを囲んでいるが枝”番号”なので値はあくまでもデータ型はintの0,1,2,3,...
となる。
Siblings
Siblings(兄弟)という概念について説明する。
まず、上のMerkle Tree上の葉A'
が与えられたとする。root
を得るためには全ての葉B'C'D'
が必要になるが全ての葉が分からない場合どうやってroot
を導き出せるか。
Siblingsとは葉A'
とともにroot
を得るために使われるMerkle Tree上の途中の枝の部分である。図を見る方が早いかも知れない。
上の図はbranch_index=0
の例である。枝番号に対応する葉A'
だけが与えられた。その場合はB"
とHash(C"D")
があれば全ての葉を知らなくともroot
を求めることができる。このような働きをする、B"
とHash(C"D")
をA'
のSiblingsと呼ぶ。
また、この際はbranch_index=0
のSiblingsと呼ぶ。branch_index
が変わるとSiblingsの中身も変わる。
MAM部の生成
MAM部にはこれまで説明したmessage
、nextRoot
、branch_index
、Siblings
を公開モードならroot
で、限定公開モードならsideKey
で暗号化する。下の図は限定公開モードの例。sideKey
で暗号化をする。
投稿 - 署名部
発行者はMAM部の内容の正当性を証明するために署名をBundleに載せる必要がある。その署名をsignatureFragment
に保管するのがこの署名部の役割だ。
署名の生成
署名は、key(seed,branch_index,security)
で生成されるPrivate KeyをMAM部のmessageTrytes
で署名して生成される。署名生成の仕組みはこちらの記事を参照。
閲覧
閲覧するのに必要なのは、root
と、限定公開ならsideKey
だ。
まずはroot
からアドレスを計算し、Tangleにそのアドレスで検索をかける。見つかったBundleのMAM部のmessageTryte
を公開モードならroot
で、限定公開モードならsideKey
で復号する。この時点でmessage
、nextRoot
、branch_index
、Siblings
が手に入る。次にこのメッセージの正当性を確認する必要がある。署名部の出番だ。
署名部にある署名を、messageTryte
を署名されるデータとして承認作業をする。承認作業の仕組みはこちらの記事を参照。承認作業で得られるのはアドレスだ。このアドレスをMerkle Treeのbranch_index
に位置する葉として、Siblings
と組み合わせることでRootを計算する(temp_root
と呼ぶ)。temp_root
がチャンネル発行者から教えてもらったroot
と一致すれば今閲覧しているBundleは正当な公式MAMメッセージということになる。
もし、一致しなければ今閲覧しようとしているBundleは悪意を持った発行者以外の人間にアタッチされたMAMメッセージである。
チェーンフォーク
MAMのメッセージチェーンは分裂できる。方法も簡単で、枝番号=branch_index
が違うメッセージを投稿するだけだ。ただし、一つの世代につき、その世代のMerkle Treeのサイズ分までしか分裂できない。というのもbranch_index
が次の世代の方へはみ出てしまうからだ。
また、分裂する際にそれぞれの投稿モード(公開や限定公開)は同じである必要はなく、sideKey
も統一する必要もない。こうすることでサブチェーンごとに役割を住み分けることもできる。
何故こんなにも面倒な署名方法なのか
Merkle Treeの署名方法など使わずに、1つの秘密鍵から生成した1つのアドレスに送りたいだけの数のメッセージを送信することもできる。本人確認はそのメッセージごとに署名をBundleに含めればいいように思える。しかし、IOTAでは1つの秘密鍵ごとに署名は一回しか使えない。
Merkle Treeをわざわざ使ったのは、異なる秘密鍵でそれぞれのメッセージを署名しつつ、送信先のアドレスを一つのアドレス(root
)に集約させるためである。
スナップショット
さてMAMはトランザクションのsignatureFragment
に保管されている。つまり、スナップショット(=残高プラスのアドレス以外削除)が行われると全てのTangle上のデータは削除される。そのため、MAMはスナップショットが行われない**Permanode(パーマノード)**という特殊なフルノードに依存する形となる。
巨大な容量と高速な処理スペックが要求されるPermanodeだが、現在のところ金銭的インセンティブはない。MAMはIOTAのIoT性を左右する要となる機能なだけに今後どうなっていくのか注目される。
参考文献
MAM JS 公式ライブラリ https://github.com/iotaledger/mam.client.js
IOTA github https://github.com/iotaledger/iota.lib.js
Introducing MAM https://blog.iota.org/introducing-masked-authenticated-messaging-e55c1822d50e