Intel SGXを使ったシークレットコントラクトがどんな仕組みか、外観を見てみたいと思います。
それにあたって、まずは今回例にあげるコインミキシングの仕組みについてまずはご説明いたします。
A君はイーサリアムアドレス0x00a1に1000ETH持っています。しかしこのアドレスで最近買い物をしていて、その時に店員さんにはこのアドレス0x00a1がA君のものだとバレてしまっています。今後このアドレスから支払いしたり送金したりすると、その店員さんにはA君がどんな送金をしたのか筒抜けになってしまい、とても気持ち悪いです。1000ETHまるごと別のアドレスに送っても、そちらのアドレスを辿られてしまうだけなので、意味がありません。
そこで、A君はコインミキシングサービスを運営しているX君に相談しました。
「ちょうどいいね。君以外にも同じ問題を抱えているB君とC君とD君がいるんだ。この4人でコインミキシングをしよう。今この4人はそれぞれ、A:0x00a1 B:0x00b1 C:0x00c1 D:0x00d1に1000ETHずつ持っているけど、みんなそれぞれ新しいアドレスを作って欲しい。A:0x00a2 B:0x00b2 C:0x00c2 D:0x00d2というように。そこで、自分のアドレスから自分のアドレスに送るんじゃなくて、たすき掛けのようにして送るんだ。0x00a1→0x00b2 0x00b1→0x00c2 0x00c1→0x00d2 0x00d1→0x00a2というイメージだ。そうすると、A君は今後アドレス0x00a2を使うけど、このアドレスの歴史を辿っても、D君のこれまでの行動しかわからないだろう?」
これがコインミキシングの基本的な考え方になります。ビットコインでもこれを行うサービスが一時期流行っていたようですが、どうしても、この例だとX君をトラストしないといけない状況になってしまいます。そこで、MoneroやDashやZcashが登場してきて、誰もトラストしなくても、プライバシーが守られた形で送金ができるようになりました。
では、ETHをプライバシーが守られた形で送金することはできるか、というと、gasはかなり高いけどzk-snarkにより実現することはできます。
そこで、シークレットコントラクトを利用すると、(たぶん)より安いコストでコインミキシングを実現することができるのです。
function makeDeposit(uint32 dealId, bytes encryptedDestAddress)
public
payable
returns (ReturnValue){
require(msg.value > 0, "Deposit value must be positive.");
require(deals[dealId].status == 0, "Illegal state for deposits.");
Deal storage deal = deals[dealId];
require((msg.value % deal.depositInWei) == 0, "Deposit value must be a multiple of claim value");
require(deal.deposit[msg.sender] == 0, "Cannot deposit twice with the same address");
// actual deposit
deal.depositSum += msg.value;
deal.deposit[msg.sender] = msg.value;
deal.encryptedDestAddresses[deal.numDeposits] = encryptedDestAddress;
deal.numDeposits += 1;
emit Deposit(msg.sender, dealId, encryptedDestAddress, msg.value, true, "all good");
if (deal.numDeposits >= deal.numParticipants) {
deal.status = 1;
emit DealFullyFunded(dealId);
} |
return ReturnValue.Ok;
}
ここで、A君、B君、C君、D君は1000ETHずつデポジットしつつ送金先アドレスを指定して、
function mixAddresses(uint32 dealId, address[] destAddresses, uint256 rand)
public
pure
returns (uint32, address[]) {
// Shuffling the specified address using a random seed.
// Doing a Fisher-Yates Shuffle with a single integer
// between 0 and 127. To get more numbers in the loop,
// we'll add 1 to our seed and hash it.
uint i = destAddresses.length;
while (i > 0) {
uint j = uint(sha3(rand + 1)) % i;
// Array swap
if (destAddresses[j] != destAddresses[i - 1]) {
address destAddress = destAddresses[i - 1];
destAddresses[i - 1] = destAddresses[j];
destAddresses[j] = destAddress;
}
i--;
}
return (dealId, destAddresses);
}
この関数でランダムに送り先を決めて、
function distribute(uint32 dealId, address[] destAddresses)
public
onlyEnigma()
returns (ReturnValue){
// Distribute the deposits to destination addresses
require(deals[dealId].status == 1, "Deal is not executed.");
deals[dealId].destAddresses = destAddresses;
for (uint i = 0; i < deals[dealId].destAddresses.length; i++) {
deals[dealId].destAddresses[i].transfer(deals[dealId].depositInWei);
}
emit Distribute(dealId, deals[dealId].depositInWei, uint32(deals[dealId].destAddresses.length), true, "all good");
return ReturnValue.Ok;
}
最後にdistributeしておしまいです!
ね?簡単でしょ?
これが基本的な流れなのですが、これを普通のスマートコントラクトで実装するとどうなるかというと、あらゆる状態が外から見えてしまうため、ミキシングも何も、普通に送金しているのと同じになってしまいます。
そこで、Intel SGXが登場しているわけですが、この場合はmakeDeposit関数の引数にencryptedDestAddressとある点に着目してください。送金先のアドレスは暗号化された状態でコントラクトに渡されています。
A君はコントラクトに1000ETHデポジット、送金先アドレスはqwert
B君はコントラクトに1000ETHデポジット、送金先アドレスはyuiop
C君はコントラクトに1000ETHデポジット、送金先アドレスはasdfg
D君はコントラクトに1000ETHデポジット、送金先アドレスはzxcvb
例えばA君はqwertが0x00a2であることを知っていますが、これはA君だけでなく、SGX内部でも0x00a2に復号することができます。つまり、SGX内の秘密鍵で復号できる形で暗号化されていることになります。SGX内部に秘密鍵があるということは、その秘密鍵がバレたら全部バレちゃうんじゃないの?と思う方がいるかと思いますが、それがSGXのすごいところ。SGX内部での計算過程については、外部から決して見ることはできないのです。Intelの開発力による物理パワーで守られているのです。
こうして、最後のアウトプットとしては、0x00a2、0x00b2、0x00c2、0x00d2に1000ETHずつ送られるわけですが、それぞれ誰が所有しているアドレスかはわからなくなりました。
ね?すごいでしょ?!
シークレットコントラクトの用途としてはあくまでも一例で、この仕組みを応用すれば、プライバシーを担保した形であらゆる計算ができることがわかりますね。
さあ、早速テストネットで遊んでみよう1