はじめに
本記事は、過去にコンセンサス・ベイスが主宰していたオンラインサロンの記事です。記事は2017年~2018年にかけて執筆されたため、一部は、既に古くなっている可能性があります。あらかじめご了承ください。
本記事ではシャミアの秘密分散というアルゴリズムを用いたマルチシグの実装と解説を行います。シャミアの秘密分散にはnpmパッケージライブラリの”shamirs-secret-sharing”を用います。また、マルチシグは以前「1から始めるP2Pとブロックチェーンプログラミング」という連載記事で作成した、ブロックチェーンのプログラムにtypescriptで型を与えたプログラムをベースに実装します。
完成したプログラムはhttps://github.com/shinyoshiaki/blockchain-tsにアップロードされています。
動作環境
Node v10
Yarn v1.3.2
シャミアの秘密分散について
シャミアの秘密分散では秘密のデータであるシークレットを複数のシェアに分割します。分散する際にあらかじめしきい値を設定します。しきい値はシークレットを復元する際に必要な最低必要なシェアの数です。しきい値以上のシェアが揃うと元のシークレットを復元することができます。
シャミアの秘密分散自体の仕組みは本記事の趣旨からは外れるのでここで軽く説明するにとどめます。
シャミアの秘密分散ではシークレットとしきい値を元に多項式を生成し、シェアを変数として多項式補完によってシークレットの復元を実現しています。
実装のイメージ
シャミアの秘密分散によるマルチシグ実装の処理の流れを以下に示します。
実装の解説
ソースコードのファイル構成
本プログラムのソースコードのファイル構成を以下に示します。
.
├── blockchain
│ ├── blockchainApp.ts
│ ├── blockchain.ts
│ ├── cypher.test.ts
│ ├── cypher.ts
│ ├── format.ts
│ ├── index.ts
│ ├── interface.ts
│ ├── multisig.ts
│ ├── responder.ts
│ └── type.ts
└── test
└── blockchain
├── blockchain.test.js
└── multisig.test.js
blockchainディレクトリ以下にあるblockchain.ts,blockchainApp.tsがブロックチェーンの基本的なコードでmultisig.tsがシャミアの秘密分散を用いたマルチシグのコードです。
testディレクトリ以下のmultisig.test.tsがマルチシグの動作確認を行うプログラムです。
プログラムのコード解説
ブロックチェーンのマルチシグ拡張であるmultisig.tsについての解説を行っていきます。
「multisig.ts」の基本的な構成としては、普通の関数と「responder」という通信などにより、受け取った情報を元に処理を行う関数の2つによって成り立っています。
makeNewMultiSigAddress
まず、マルチシグのアドレスとシェアを生成する関数について説明します。ソースコードを以下に示します。
/src/blockchain/multisig.ts
//マルチシグのアドレスを生成
makeNewMultiSigAddress(
friendsPubKeyAes: Array, //共有者の情報
vote: number, //しきい値
amount: number //金額
) {
console.log(this.makeNewMultiSigAddress);
//秘密鍵と公開鍵を生成
const cypher = new Cypher();
//次に使うaeskeyを生成
const aesKey = sha1(Math.random().toString()).toString();
console.log({ aesKey });
//aeskeyで秘密鍵を暗号化
const encryptSecKey: string = aes256.encrypt(aesKey, cypher.secKey);
//シャミアの秘密分散ライブラリでaeskeyをシェア化
const shareKeys: Array = sss.split(Buffer.from(aesKey), {
shares: friendsPubKeyAes.length + 1,
threshold: vote
});
console.log({ shareKeys });
//マルチシグアドレスを導出
const address = sha256(cypher.pubKey);
const shares: { [key: string]: string } = {};
//シェアの共有者にシェアを配布
friendsPubKeyAes.forEach((aes, i) => {
const pubKey = aes256.decrypt("format", aes);
const id = sha256(pubKey);
console.log("makeNewMultiSigAddress sharekey", shareKeys[i]);
//共有者の公開鍵でシェアを暗号化
shares[id] = crypto
.publicEncrypt(pubKey, Buffer.from(shareKeys[i]))
.toString("base64");
});
console.log({ shares });
//自身にシェアを一つ割当
const myShare = shareKeys[shareKeys.length - 1];
//マルチシグの情報を保管
this.multiSig[address] = {
myShare,
threshold: vote,
isOwner: false,
pubKey: cypher.pubKey,
encryptSecKey,
shares: []
};
this.multiSig[address].shares.push(myShare);
//ブロックチェーンに載せるマルチシグ情報
const info: multisigInfo = {
multisigPubKey: cypher.pubKey,
multisigAddress: address,
encryptSecKey,
threshold: vote
};
//トランザクションを生成
const tran = this.b.newTransaction(this.b.address, address, amount, {
type: type.MULTISIG,
opt: type.MAKE,
shares,
info
});
console.log("makeNewMultiSigAddress done", { tran });
return tran;
}
ブロックチェーン上に秘密鍵に関わる情報を公開することになるので、やや複雑な暗号化の処理が行われています。
またシャミアの秘密分散では、分散後のシェアのデータサイズが元のシークレットの数倍程度になります。
その際、秘密鍵のように比較的データサイズが大きなデータをシャミアで分散させた上で、ブロックチェーンに載せてしまうと、チェーンが肥大化する恐れがあります。
これを回避するために、まず秘密鍵をaes暗号で暗号化して、ブロックチェーンに公開し、比較的サイズの小さいaes暗号鍵をシャミアで分散するという手法を取りました。
また、分散したシェアはブロックチェーンに載せることになるので、共有者本人にしか見れないようにするために共有者の公開鍵を使って暗号化しています。流れをまとめると以下のようになります。
秘密鍵→AES→シャミア→公開鍵暗号→ブロックチェーン
private getMultiSigKey
この関数は「responder」から呼び出される関数で、トランザクションからマルチシグの情報を取得を行います。先程の「makeMultiSigTransaction」のトランザクションからマルチシグの情報を抜き出します。ソースコードを以下に示します。
//トランザクションからマルチシグの情報を取得
private getMultiSigKey(
shares: { [key: string]: string },
info: multisigInfo
) {
console.log("getMultiSigKey");
if (info.encryptSecKey && Object.keys(shares).includes(this.address)) {
console.log("getMultiSigKey start");
//シェアキーの公開鍵暗号を秘密鍵で解除
const key = crypto.privateDecrypt(
this.b.cypher.secKey,
Buffer.from(shares[this.address], "base64")
);
console.log("getMultiSigKey get my key", key);
//マルチシグ情報を保存
this.multiSig[info.multisigAddress] = {
myShare: key.toString("base64"),
isOwner: false,
threshold: info.threshold,
pubKey: info.multisigPubKey,
encryptSecKey: info.encryptSecKey,
shares: []
};
}
}
makeMultiSigTransaction
この関数はマルチシグアカウントからトークンを取り出すトランザクションを作成する関数です。ソースコードを以下に示します。
//マルチシグのトランザクションを生成
makeMultiSigTransaction(multisigAddress: string) {
console.log("makeMultiSigTransaction start");
//マルチシグアドレスの情報を自分が持っているのか
const data = this.multiSig[multisigAddress];
if (!data) return;
const multisigPubKey = data.pubKey;
//自分の持っているシェアキーを公開鍵で暗号化
const shareKeyRsa = crypto
.publicEncrypt(this.b.cypher.pubKey, Buffer.from(data.myShare, "base64"))
.toString("base64");
//ブロックチェーンに載せる情報
const info: multisigInfo = {
ownerPubKey: this.b.cypher.pubKey,
multisigPubKey,
multisigAddress,
sharePubKeyRsa: shareKeyRsa,
threshold: data.threshold
};
//マルチシグ情報にトランザクション実行者フラグを立てる
data.isOwner = true;
//マルチシグアドレスの残高を取得
const amount = this.b.nowAmount(multisigAddress);
console.log("multisig tran", { amount });
//トランザクションを生成
const tran = this.b.newTransaction(this.b.address, multisigAddress, 0, {
type: type.MULTISIG,
opt: type.TRAN,
amount,
info
});
console.log("makeMultiSigTransaction done", { tran });
return tran;
}
makeMultiSigTransaction
この関数はマルチシグアカウントからトークンを取り出すトランザクションを作成する関数です。ソースコードを以下に示します。
//マルチシグのトランザクションを生成
makeMultiSigTransaction(multisigAddress: string) {
console.log("makeMultiSigTransaction start");
//マルチシグアドレスの情報を自分が持っているのか
const data = this.multiSig[multisigAddress];
if (!data) return;
const multisigPubKey = data.pubKey;
//自分の持っているシェアキーを公開鍵で暗号化
const shareKeyRsa = crypto
.publicEncrypt(this.b.cypher.pubKey, Buffer.from(data.myShare, "base64"))
.toString("base64");
//ブロックチェーンに載せる情報
const info: multisigInfo = {
ownerPubKey: this.b.cypher.pubKey,
multisigPubKey,
multisigAddress,
sharePubKeyRsa: shareKeyRsa,
threshold: data.threshold
};
//マルチシグ情報にトランザクション実行者フラグを立てる
data.isOwner = true;
//マルチシグアドレスの残高を取得
const amount = this.b.nowAmount(multisigAddress);
console.log("multisig tran", { amount });
//トランザクションを生成
const tran = this.b.newTransaction(this.b.address, multisigAddress, 0, {
type: type.MULTISIG,
opt: type.TRAN,
amount,
info
});
console.log("makeMultiSigTransaction done", { tran });
return tran;
}
approveMultiSig
マルチシグの承認を行う関数です。承認情報をトランザクションで送り出したあと、responderに処理が回されます。ソースコードを以下に示します。
//マルチシグの承認
approveMultiSig(info: multisigInfo) {
console.log("approveMultiSig");
if (info.ownerPubKey) {
//マルチシグの情報があるかを調べる
if (Object.keys(this.multiSig).includes(info.multisigAddress)) {
console.log("approveMultiSig start");
//シェアキーを取り出す
const key = this.multiSig[info.multisigAddress].myShare;
//シェアキーをマルチシグトランザクション実行者の公開鍵で暗号化
const shareKeyRsa = crypto
.publicEncrypt(info.ownerPubKey, Buffer.from(key, "base64"))
.toString("base64");
info.sharePubKeyRsa = shareKeyRsa;
//トランザクションを生成
const tran = this.b.newTransaction(
this.b.address,
info.multisigAddress,
0,
{
type: type.MULTISIG,
opt: type.APPROVE,
info: info
}
);
console.log("approveMultiSig done", { tran });
return tran;
}
}
}
private onApproveMultiSig
「responder」から呼び出される関数で、マルチシグトランザクションの実行者がこの関数で承認情報を確認し、最終的にトランザクションの承認関数を呼び出します。
//マルチシグトランザクション実行者の関数
private onApproveMultiSig(info: multisigInfo) {
if (
info.sharePubKeyRsa &&
info.ownerPubKey === this.b.cypher.pubKey &&
Object.keys(this.multiSig).includes(info.multisigAddress)
) {
console.log("type.APPROVE");
const shares = this.multiSig[info.multisigAddress].shares;
//シェアキーの公開鍵暗号を自身の秘密鍵で解除
const shareKey = crypto.privateDecrypt(
this.b.cypher.secKey,
Buffer.from(info.sharePubKeyRsa, "base64")
);
//新しいシェアキーなら保存する。
if (!shares.includes(shareKey)) {
console.log("add sharekey", { shareKey });
shares.push(shareKey);
}
//シェアキーの数がしきい値を超えればトランザクションを承認
if (shares.length >= info.threshold) {
console.log("verify multisig", { shares });
//トランザクションの承認関数
this.verifyMultiSig(info, shares);
this.excuteEvent(this.onMultisigTranDone);
}
}
}
verifyMultiSig
トランザクションの承認を行う関数です。シャミアの復号化もこの関数で行います。問題がなければマルチシグトランザクションを実行します。
//トランザクションの承認
verifyMultiSig(info: multisigInfo, shares: Array) {
console.log("verifyMultiSig start", { shares });
//シャミアのシェアキーからシークレットを復号化
const recovered = sss.combine(shares).toString();
console.log({ recovered });
//aes暗号化されたシークレットキーを取り出す。
const encryptedKey = this.multiSig[info.multisigAddress].encryptSecKey;
//aes暗号を復号化
const secKey = aes256.decrypt(recovered, encryptedKey);
console.log({ secKey });
const cypher = new Cypher(secKey, info.multisigPubKey);
const address = info.multisigAddress;
//マルチシグアドレスの残高を取得
const amount = this.b.nowAmount(address);
//残高があればトランザクションを実行
if (amount > 0) {
const tran = this.b.newTransaction(
address,
this.b.address,
amount,
{ comment: "verifyMultiSig" },
cypher
);
console.log("verifyMultiSig done", { tran });
return tran;
}
}
動作確認
では動作確認に入っていきます。実行ファイルは「multisig.test.js」です。今回のプログラムはすでにTypeScript(.ts)をJavaScript(.js)にコンパイルした上でリポジトリに登録しています。
まず、動作確認に使用する実行ファイルのソースコードについて解説します。このソースコードでは複数のノードをローカルで立ち上げ、最終的にマルチシグトランザクションを実行する形になります。ソースコードを以下に示します。
lib/test/blockchain/multisig.test.js
async function main() {
//マルチシグトランザクションを作る役(作成役とする)
const bc1 = new BlockChain();
//シェアキー共有者
const friends = [];
const cypher = keypair();
friends.push(aes256.encrypt("format", cypher.public));
//承認者1
const bc2 = new BlockChain(cypher.private, cypher.public);
const cypher2 = keypair();
friends.push(aes256.encrypt("format", cypher2.public));
//承認者2
const bc3 = new BlockChain(cypher2.private, cypher2.public);
//作成役がマイニングしてトークンを稼ぐ
const block = await bc1.mine();
console.log({ block });
console.log("amount", bc1.address, bc1.nowAmount(bc1.address));
//作成役と承認者のブロックチェーンのフォークを解決
bc2.chain = bc1.chain;
bc3.chain = bc1.chain;
//作成役がマルチシグアドレスを生成
let tran: any = bc1.multisig.makeNewMultiSigAddress(friends, 2, 1);
//承認者がマルチシグアドレスのトランザクションをresponderに渡す
bc2.multisig.responder(tran);
bc3.multisig.responder(tran);
const multisigAddress = Object.keys(bc1.multisig.multiSig)[0];
//マルチシグトランザクションを作成
tran = bc1.multisig.makeMultiSigTransaction(multisigAddress);
//承認者が承認するためのコールバックを用意
bc2.multisig.events.onMultisigTran["test"] = (info: multisigInfo) => {
//マルチシグの承認
bc1.multisig.responder(bc2.multisig.approveMultiSig(info));
};
bc3.multisig.events.onMultisigTran["test"] = (info: multisigInfo) => {
bc1.multisig.responder(bc3.multisig.approveMultiSig(info));
};
//マルチシグトランザクションをresponderに渡す。
if (tran) bc2.multisig.responder(tran);
if (tran) bc3.multisig.responder(tran);
}
実行はターミナルから行います。
$ git clone https://github.com/shinyoshiaki/blockchain-ts.git
$ cd blockchain-ts
$ yarn
$ cd lib/test/blockchain
/lib/test/blockchain$ node multisig.test.js
プログラムの実行結果を見ると中ほどに
verifyMultiSig done { tran:
{ sender:
'985990632c5af5e0ac509a1fa697b895d9f20df9fef01d8d50bc2e231195848c',
recipient:
'16d20a2d15998795fcea96d9a8a4eeb0bc1fa3cac167ea6baa2f1e03df111aef',
amount: 1,
data: { comment: 'verifyMultiSig' },
「verifyMultiSig done」というマルチシグトランザクションが承認されたというログが確認でき、マルチシグトランザクションを実行できていることがわかります。
まとめ
今回の記事では、シャミアの秘密分散を用いたマルチシグを実装しました。シャミアの秘密分散を用いる方法では、すべてのマルチシグ処理をブロックチェーン上で行えることを確認できました。マルチシグはトークンの盗難防止や、エクスローといった技術に用いられ、主要なプロジェクトのほとんどに既に実装されており、もはやブロックチェーンの基本機能になりつつあります。しかし、マルチシグの実装方法はそれぞれのプロジェクトによってばらばらで、どの方法がベストというのもまだ明らかではない(未知の脆弱性が存在する可能性)ので、これからもマルチシグについて注視する必要がありそうです。
以上