この記事はIOTA:【技術解説】MAMとは。IOTAのIoT性。で説明した旧MAMではなく、最新版のMAMの仕様を実現したライブラリを使っていくため、少なからず用語が旧MAMと違ったりしているのを留意していただきたい。
MAMとは
IOTAの分散型台帳であるTangleにデータを保管できるようにする。応用される一番の具体例はIOTAのデータマーケットであろう。
IoT機器から収集されうる莫大なデータを保管し売買できるようにする。それを可能にするインフラがIOTAであり、その上に乗っかる技術がMAM。
ただ、MAM自体非常にシンプルな仕組みなので潜在的な利用可能性は多岐に渡りうる。無限大だ。(ただ、IOTAという名前なのだから当分はIoTを主な顧客にしていくだろう。)
先日、IOTAの日本向けファンサイトでデータマーケットに関する面白い議論が交わされた。ビジネスパーソンな方には本記事なんかより、こちらの記事の方が役立つかもしれない。
IOTAの基礎知識
MAM?そもそもIOTAの仕組みが分からんという方のために...
IOTA:【入門】トランザクション大解剖!ウォレットは裏で何をやっているか。
IOTA:【技術解説】送金APIから紐解くBundleの全容。
IOTA【技術解説】署名と承認。 - 改訂版
現行MAMライブラリ
ライブラリはオランダのMeetupでのデモで紹介されたものを使う。
ダウンロードはこちらの下の方のZIP file、"mam.client.js.zip"から。
また、下記のような場所で開発されているようである。
l3wi/mam.client.js
iotawtf/IOTA-MAM.lib.js-PoC
実行環境
- Node.js: バージョンは8以上。
- 自分のフルノードなら
localhost
にリクエスト。自分のがない場合はPoW可能な公開ノードのIPにリクエスト。 - コードの保存ディレクトリは
mam.client.js/example/
内。
Node.jsの一般的なAPIの使い方は別の記事を参照してほしい。
MAMのモード
Public(公開):保管されたメッセージはアドレスを使って復号。(アドレス自身が復号に使われるため、アドレスが分かればメッセージも見れる。)
Private(非公開):アドレス=hash(root)
。rootを使って復号。(アドレスはハッシュ化されており、ハッシュ化前の生root
を知っていないと、例えアドレスにアクセスしてもそのメッセージを復号できない。)
Restricted(限定公開):アドレス=hash(root+key)
。root+keyを使って復号。(rootを教えることはPrivateチャンネルを公開することになるため、あくまでroot+keyを限定公開する。そのroot+keyを知っている人だけがRestrictedチャンネルにアクセスしてメッセージを復号できる。)
本記事では公開モード、限定公開モードを実際に試して見る。
公開モード(Public)
root
(=アドレス)を知っている人が読めるメッセージをTangleに投稿する。
post.js
let Mam = require('../lib/mam.node.js');
let IOTA = require('iota.lib.js');
// 今回はlocalhostからリクエスト。
let iota = new IOTA({ provider: 'http://localhost:14265'});
// 送信したいメッセージ。日本語を含む全角は未対応。
let yourMessage = "O frabjous mam! Calloh! Callay!";
// Seed: 81 chars of A-Z9 //
// MAM用。
let seed = 'ABMUSHI9MAM9TEST...';
// Length: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
let index = 0;
let security = 2;
let mamState = null;
// 送信する前に今までの送信履歴を表示する。
async function fetchStartCount(){
let trytes = iota.utils.toTrytes('START');
let message = Mam.create(mamState, trytes);
console.log('The first root:');
console.log(message.root);
console.log();
// Fetch all the messages upward from the first root.
return await Mam.fetch(message.root, 'public', null, null);
}
// 投稿する。
async function publish(packet){
// Create the message.
let trytes = iota.utils.toTrytes(JSON.stringify(packet))
let message = Mam.create(mamState, trytes);
// Set the mam state so we can keep adding messages.
mamState = message.state;
console.log('Sending message: ', packet);
console.log('Root: ', message.root);
console.log('Address: ', message.address);
console.log();
// Attach the message.
return await Mam.attach(message.payload, message.address);
}
// MAMの初期化
mamState = Mam.init(iota, seed, security, index);
// 送信する前に、
fetchStartCount().then(v => {
// Log the messages.
let startCount = v.messages.length;
if(startCount>0){
console.log('Messages already in the stream:');
}
// 今までの送信履歴を集めて、
for (let i = 0; i < v.messages.length; i++){
let msg = v.messages[i];
console.log(JSON.parse(iota.utils.fromTrytes(msg)));
}
console.log();
// 表示する。
mamState = Mam.init(iota, seed, security, startCount);
// そして、新しいメッセージを作って、
let newMessage = Date.now() + ' ' + yourMessage;
// 投稿する。
publish(newMessage);
}).catch(ex => {
console.log(ex);
});
結果は以下の通りになる。今回このテストの前に筆者はすでに2回メッセージを投稿したので、Message already in the stream
の後に二つのメッセージが表示されている。初めてのメッセージの際は表示されない。最後にRoot: CK9SVMYIMP...
となっているのが、メッセージの保管されているアドレスである。
The first root:
NATZGYPUGDJVGIJOOERUXLWOVLQLEBFUPETMTPTZVUFIWYYXI9BJJJLMTCAOTBVDBHVWSVBYHXVN9YUNH
Messages already in the stream:
1515886694739 O mam frabjous day! Calloh! Callay!
1515888642819 second message. ABmushi
Sending message: 1516086368435 O frabjous mam! Calloh! Callay!
Root: CK9SVMYIMPAPZLNQEGITQUWMIVJVXEKPORBSQZH9PCL9ZLEHIVVWXCLJWEMWZMFSQRWBPZDFR9TOBHPFX
Address: CK9SVMYIMPAPZLNQEGITQUWMIVJVXEKPORBSQZH9PCL9ZLEHIVVWXCLJWEMWZMFSQRWBPZDFR9TOBHPFX
試しにこのアドレスのBundleをtheTangle.orgで見てみると、Bundleが三つ生成されているのが確認できる。残念ながらMAMのBundleの構造についてはまだソースコードを読めていないため詳しくはよく分からない。
話はそれるが、新MAMのソースコードは、行数でいうと軽く数万行に及んでいた!!(もちろん行数がクオリティを正確に測る指標ではないことは承知の上だが。)
今まで、MAMの話題はあまりSlackでも全くと言っていいほど盛り上がっておらず寂しく思っていたが、水面下ではしっかり開発が行われていたのだろう。IOTA開発陣の職人ぶりに改めて驚かされた。
...という、無駄話は置いといて次に今投稿したMAMを見てみよう。
fetch.js
投稿した公開MAMを閲覧するのに必要となるのは、先ほど得たroot
だ。
var Mam = require('../lib/mam.node.js')
var IOTA = require('iota.lib.js')
// 今回はlocalhostにリクエスト。
var iota = new IOTA({'host':'http://localhost','port':14265});
/* 変える必要のあるのは今の所ここから... */
//
// root。メッセージを読み取る際に必要なもの。
let root = 'CK9SVMYIMPAPZLNQEGITQUWMIVJVXEKPORBSQZH9PCL9ZLEHIVVWXCLJWEMWZMFSQRWBPZDFR9TOBHPFX';
//
//
let key = null; // 限定公開モード用。
let mode = 'public'; // 公開モードは'public'。限定公開は'restricted'。
//
/* ...ここまで */
// MAMの初期化
var mamState = Mam.init(iota)
const publish = async packet => {
var trytes = iota.utils.toTrytes(JSON.stringify(packet))
var message = Mam.create(mamState, trytes)
mamState = message.state
await Mam.attach(message.payload, message.address)
return message.root
}
// Callback used to pass data out of the fetch
const logData = data => console.log(JSON.parse(iota.utils.fromTrytes(data)))
// 実行内容
const execute = async () => {
// fetchする関数
// Mam.fetch(root,mode,sideKey,callback);
// root: root。
// mode: (公開)'public'、もしくは(限定公開)'restricted'
// sideKey: null for 'public' mode
// callback: logData
var resp = await Mam.fetch(root, mode, key, logData)
console.log(resp)
}
// 実行開始
execute()
1516086368435 O frabjous mam! Calloh! Callay!
{ nextRoot: 'NFRWJGOCEZZXQKDZALZIASFYNYWLZFYYBZZCEHIOZYNRAEPEYPJMBHHI9NQPK9IKYDVWYZM9JOQXWFBDO' }
しっかりメッセージが得られた。ここでnextRoot
という値だが、これはMAMのメッセージチェーンのつなぎ目である。つまり、もし次の投稿があれば、このnextRoot
をroot
としてfetchすると次の投稿が閲覧できる。
上のpost.jsの際、筆者はすでに2つメッセージを投稿したと述べた。ちなみに最初の投稿を指すroot
はこれだ。
The first root:
NATZGYPUGDJVGIJOOERUXLWOVLQLEBFUPETMTPTZVUFIWYYXI9BJJJLMTCAOTBVDBHVWSVBYHXVN9YUNH
このroot
でfetchすると、
1515886694739 O mam frabjous day! Calloh! Callay!
1515888642819 second message. ABmushi
1516086368435 O frabjous mam! Calloh! Callay!
{ nextRoot: 'NFRWJGOCEZZXQKDZALZIASFYNYWLZFYYBZZCEHIOZYNRAEPEYPJMBHHI9NQPK9IKYDVWYZM9JOQXWFBDO' }
という風に、与えられたroot
以降に投稿された全てのメッセージが表示される。この仕組みを使えばIoT機器から定期的にデータをMAMを使ってTangleに流していくようなシステムも一つのSeedで実現できる。
何度でも言う。Seedは命くらいに大事だ。
限定公開(Restricted Mode)
公開モードではroot
がTangle上のアドレスと一致していたため、メッセージはTangle上でむき出しになっている。そこでMAMでは、限定公開モードも用意している。限定公開モードはroot
とkey
(sideKey)を知っている人のみがメッセージを閲覧できるようにする。
なお、デフォルトのソースコードには限定公開用の関数がなく、しょうがないので自作したため、細かな不具合などがある可能性があるのをご了承いただきたい。
post.js
let Mam = require('../lib/mam.node.js');
let IOTA = require('iota.lib.js');
let iota = new IOTA({ provider: 'http://localhost:14265'});
let yourMessage = 'Restricted message. Only those with key can see this.';
// Please supply a SEED --> 81 chars of A-Z9 //
let seed = 'ABMUSHITEST...';
// Length: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
let mamState = null;
let security = 2;
let key = 'ABMUSHITESTKEY';
let index = 0;
mamState = Mam.init(iota, seed, security, index);
// 限定公開メッセージを全て受信する。
async function fetchRestrictedStartCount(){
let trytes = iota.utils.toTrytes('START');
let message = Mam.create(mamState, trytes);
console.log('The first root:');
console.log(message.root);
console.log();
// Fetch all the messages upward from the first root.
return await Mam.fetch(message.root, 'restricted', key, null);
}
// 限定公開メッセージを投稿する。
async function publishRestricted(packet,key){
// Create the message.
let trytes = iota.utils.toTrytes(JSON.stringify(packet));
// モード切り替え
mamState = Mam.changeMode(mamState,'restricted',key);
let message = Mam.create(mamState, trytes);
mamState = message.state;
console.log('Sending Restricted message:', packet);
console.log('Root: ', message.root);
console.log('Address: ', message.address);
console.log();
// Attach the message.
return await Mam.attach(message.payload, message.address);
}
fetchRestrictedStartCount().then(v => {
// Log the messages.
let startCount = v.messages.length;
if(startCount>0){
console.log('Messages already in the stream:');
}
for (let i = 0; i < v.messages.length; i++){
let msg = v.messages[i];
console.log(JSON.parse(iota.utils.fromTrytes(msg)));
}
console.log();
// To add messages at the end we need to set the startCount for the mam state to the current amount of messages.
mamState = Mam.init(iota, seed, security, startCount);
let newMessage = Date.now() + ' ' + yourMessage;
var now = new Date();
console.log(now.toString());
console.log('attaching... This may take a little while.');
// Now the mam state is set, we can add the message.
publishRestricted(newMessage,key);
}).catch(ex => {
console.log(ex);
});
The first root:
NMCJ9SYUSJDZHDFMRY9YS9ETJTLZ9JTXPKHECGSDPAXVKRAPSLVOZHDTFFALNXF9JEAXBD9V9HOGDBMRD
Tue Jan 16 2018 17:46:40 GMT+0000 (UTC)
attaching... This may take a little while.
Sending Restricted message: 1516124800201 Restricted message. Only those with key can see this.
Root: NMCJ9SYUSJDZHDFMRY9YS9ETJTLZ9JTXPKHECGSDPAXVKRAPSLVOZHDTFFALNXF9JEAXBD9V9HOGDBMRD
Address: BQUZVZGLGNSJMAECHZEMCAPPNVRIHCZRMXGXUTFDOKVJLGFK9MKTUVGROMOABLKEXLNQUHF9BNLXDXEAK
root
が得られた。ちなみにAddressというのはこのMAMのトランザクションのアドレスのことである。このアドレスだけ知っていてもメッセージはfetchできない。必ず、root
とkey
のペアが必要だ。
また、今回はチャンネル初投稿なので履歴は表示されていない。そのためThe first root
とRoot
が同じ値だ。
fetch.js
let key = 'ABMUSHITESTKEY'; // 複合キー
let mode = 'restricted'; // 限定公開=restricted
const execute = async () => {
var resp = await Mam.fetch(root, mode, key, logData)
console.log(resp)
}
root
にさっきのNMCJ9SYUS...
を代入、またkey
も設定して実行すると...
1516124800201 Restricted message. Only those with key can see this.
{ nextRoot: 'IM9VPNBBDVXUICMTINX9PBXBYRZMLQGOZZJCDKIZMDOXQYBZCRTAG9APOVIOPYRDAPVSQUSSZULLHHCU9' }
メッセージが得られた。
3Miゲットだぜ!企画
今回の記事の宿題用に限定公開メッセージを投稿した。
root: A9AWZUHFHKJRTBTXLDLCHGGTGKZGQCYWV9XVQRBQUTYIYHOCCULATTZWYMGD9FNFLB9HOCESQCLYB9OHO
key: ABMUSHI9IOTA9FAN
実はこのメッセージには賞金3Miが入ったSeedが入っている。早い者勝ちだぁ!
(1/28/2018のスナップショットでlocalhost
のデータベースからはMAMメッセージは消去されてしまった。代わりにどこかの公開ノードに接続してみよう。)
質問等ありましたら、コメント欄、Twitter、Discord参加者の方は**#jp-devs**チャンネルまでお願いいたします。