はじめに
ビットコインのScript言語を使った「スクリプトパズル」というお遊びサービスを開発しました。
スクリプトパズルを通じてビットコインのScript言語や、所有権の有効性チェックについて理解を深めることが目的です。
前提条件
以下について、基礎知識を持った方が対象です。
- UTXO
- トランザクションのインプットとアウトプット
- ビットコインのScript言語について(OPコード)
- P2PKHやマルチシグ、P2SHといった標準的なトランザクション形式
以降は、これらをある程度知っている前提で進めます。
スクリプトパズルとは
よく「ビットコインを送金する」という言い方をしますが、ビットコインは厳密には送金ではなく、トランザクションのUTXOに含まれているlocking script(解除条件)を、unlocking scriptで解除することで「ビットコインの所有権を証明する」という言い方が正しいです。
これをトランザクションの有効性チェックと言ったりもしますが、スクリプトパズルはこのlocking scriptとunlocking scriptを自由に組み合わせてビットコインの有効性チェックをするサービスです。
なおlocking scriptをScriptPubKy、unlocking scriptをScriptSigとも言います。
通常は特定の人物宛てにBTCの所有権を移譲するため、locking scriptに送信先の公開鍵、unlocking scriptに秘密鍵を使った署名を用いますが、スクリプトパズルではあえて秘密鍵や署名を含めずに、純粋にScript言語(OPコード)のみで解除条件を組み立てます。
つまり簡潔に言うと、
「パズル(locking script)を解いた人がBTCをアンロックして、その所有権を得ることができるサービス」です。
免責事項【重要】
はじめに申した通り、スクリプトパズルはビットコインのScript言語の理解を深めるために個人が開発したお遊びサービスです。
そのため厳密なバリデーションや例外処理などは未実装です(正常系のみ確認)。
仮にGOXしても自己責任となります。
そのため、最小限のBTCでご利用することを推奨します。
また私の判断で事前告知なしにサービスを停止する場合もございます。
それらを念頭に、ご理解いただける方のみスクリプトパズルで遊んでみて下さい。
スクリプトパズルで遊んでみよう
さっそくスクリプトパズルで遊んでみましょう。
※こちら現在は停止しております。
スクリプトパズル画面を表示する
Lock script
画面を表示したら、以下の操作を行います。
①Lock scriptタブをクリックします
②ここにlocking script(解除条件)のOPコードを半角スペース区切りで入力します
③解除条件ができたら「Generate Bitcoin Address」ボタン押下
④参考URLです。bitcoin script debuggerなどで事前にScriptが正しいかチェックすることを推奨します
③まで完了すると、画面下部にビットコインアドレスとそのQRコードが表示されます。
今回は例として、locking scriptに以下のOPコードを半角スペース区切りで入力します。
OP_ADD OP_4 OP_EQUALVERIFY OP_SUB OP_2 OP_EQUAL
そうすると、以下のようにビットコインアドレスが生成されます。
この「3AdTXLBPi2Mo5XKKS1MYUmJQi2pyShHSaA」が入力したlocking script(パズル)で所有権をロックされたビットコインアドレスになります。
次はこのアドレスに対していくつかBTCを送ります。
この操作はウォレットアプリなどを利用して、各自行ってください。
ウォレットアプリからQRコードを読み込めば簡単にできるはずです。
因みに私はGincoというウォレットアプリを利用しました。
これでこのパズルの回答を知っている者であれば誰でもアドレスに紐づくBTCを利用できるわけです。
Unlock script
次に、上記で生成したアドレスのUnlock scriptを入力します。
①Unlock scriptタブをクリックします
②ここは「all UTXO」を選択してください(他は選択肢は未実装)
③ここにはLock scriptでロックされた先ほどのアドレスが自動で表示されるのでこのままで
④ここにUnlock scriptを半角スペース区切りで入力します
⑤無事にロックが解除できたら、BTCの所有権を渡すアドレスを入力します。自分のアドレスでよいです
⑥トランザクションの手数料です、デフォルトは500satoshi
⑦すべて準備できたら押下します。ロックが解除され、新たなトランザクションがブロードキャストされます
⑧問題なくブロードキャストできたら、トランザクションIDが表示されます。パズルが解除できないなど、エラーが起きたらその内容が表示されます
ちなみに上記パズルの回答は以下のOPコードになります。
OP_3 OP_1
最終的にトランザクションは、以下のように有効性チェックされます。
この結果、Trueが返されればパズルの解除成功というわけで、無事にBTCの所有権を得ることができ、再利用することが可能です。
システム構成
以下がスクリプトパズルのシステム構成です。
- C# 7
- .NET Core 2.1.0
- .NET Sdk Functions 1.0.28
- NBitcoin 4.1.2.32
- QBitNinja.Client 1.0.3.49
スクリプトパズルはAzure Functionを利用してます。
コードをみる
まずLock Scriptの処理です。
コードはVisual studio 2017で書きました。
IDEが自動インポートするような処理は割愛してます。
namespace FunctionApp1
{
public static class Function1
{
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
// クライアントからLock scriptの文字列を受け取る
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
// ScriptクラスへLock scriptを渡す
Script redeemScript = new Script(requestBody);
// P2SH形式のビットコインアドレスを生成する
BitcoinAddress address = redeemScript.Hash.GetAddress(Network.Main);
// 生成されたビットコインアドレスを返す
return address != null
? (ActionResult)new OkObjectResult(address.ToString())
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}
}
続いて、Unlock Scriptの処理です。
namespace ScriptPazzleUnlock
{
public static class Function1
{
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string network = req.Query["network"];
network = network ?? "BTC";
// トランザクションをブロードキャストするか否かのフラグ
bool broadcast = (req.Query["b"] == "1");
StringBuilder sb = new System.Text.StringBuilder("");
sb.AppendFormat("Network:{0}", network).AppendLine();
// クライアントからのパラメータを受け取る
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
string btcAddress = data.btcAddress;
string lockingScript = data.lockingScript;
string unlockingScript = data.unlockingScript;
string sendToAddress = data.sendToAddress;
var feeSatoshi = data.feeSatoshi;
// chain.soでビットコインアドレスのUTXOを取得する
WebRequest request = WebRequest.Create("https://chain.so/api/v2/get_tx_unspent/" + network + "/" + btcAddress);
WebResponse response = request.GetResponse();
using (Stream dataStream = response.GetResponseStream())
{
StreamReader reader = new StreamReader(dataStream);
string responseFromServer = reader.ReadToEnd();
dynamic res = JsonConvert.DeserializeObject(responseFromServer);
var client = new QBitNinjaClient(Network.Main);
var transaction = Transaction.Create(Network.Main);
var hallOfTheMakersAddress = BitcoinAddress.Create(btcAddress);
var hallOfTheSendAddress = BitcoinAddress.Create(sendToAddress);
var minerFee = new Money((decimal)feeSatoshi, MoneyUnit.Satoshi);
foreach (dynamic tx in res.data.txs)
{
var transactionResponse = client.GetTransaction(uint256.Parse(tx.txid.ToString())).Result;
//Transaction tran = transactionResponse.Transaction;
var receivedCoins = transactionResponse.ReceivedCoins;
OutPoint outPointToSpend = null;
foreach (var coin in receivedCoins)
{
if (coin.TxOut.ScriptPubKey == hallOfTheMakersAddress.ScriptPubKey)
{
outPointToSpend = coin.Outpoint;
if (outPointToSpend == null)
{
throw new Exception("No transaction output has a ScriptPubKey for the coin that sent it.");
}
transaction.Inputs.Add(new TxIn()
{
PrevOut = outPointToSpend
});
var txInAmount = receivedCoins[(int)outPointToSpend.N].TxOut.Value;
Money sendValue = txInAmount - minerFee;
transaction.Outputs.Add(new TxOut()
{
Value = sendValue,
ScriptPubKey = hallOfTheSendAddress.ScriptPubKey
});
}
}
}
// 半角スペース区切りでOPコードを取得する
String[] unlockingScripts = unlockingScript.Split(" ", StringSplitOptions.RemoveEmptyEntries);
// OPコードを格納する配列
Op[] ops = new Op[] {};
foreach (string op in unlockingScripts)
{
OpcodeType opcode;
if (Op.GetOpCode(op, out opcode))
{
Array.Resize(ref ops, ops.Length + 1);
ops[ops.Length - 1] = opcode;
}
}
var p2shProof = PayToScriptHashTemplate
.Instance
.GenerateScriptSig(ops, new Script(lockingScript));
for (int i = 0; i < transaction.Inputs.Count; i++)
{
transaction.Inputs[i].ScriptSig = p2shProof;
// ここでパズルの答え合わせをしてる
if (transaction.Inputs.AsIndexedInputs().First().VerifyScript(hallOfTheMakersAddress.ScriptPubKey) == false)
{
throw new Exception("Verify script is fail. Can't unlook.");
}
}
if (broadcast) {
BroadcastResponse broadcastResponse = client.Broadcast(transaction).Result;
if (!broadcastResponse.Success)
{
throw new Exception("Error message: " + broadcastResponse.Error.Reason);
}
else
{
Console.WriteLine("Success! You can check out the hash of the transaciton in any block explorer:");
Console.WriteLine(transaction.GetHash());
}
}
sb.AppendFormat("Success! You can check out the hash of the transaciton in any block explorer.");
sb.AppendFormat("Transaction id:{0}", transaction.GetHash().ToString());
}
response.Close();
return sb != null
? (ActionResult)new OkObjectResult(sb.ToString())
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}
}
注意事項
locking scriptが簡単すぎると、すぐに取られてしまう恐れがあります。
例えば「OP_2 OP_EQUAL」で生成したアドレスなどです。
これらは悪意ある自動ボットから、容易に所有権を奪われる可能性が高いです。
そのため、少しは難易度の高いパズルにしましょう。
しかし難易度が高すぎるのも問題です。
なぜなら正しいスクリプトが分からなくなると、一生解除できなくなるからです。
それはビットコインの所有権を放棄したと同義で、セルフGOXに値します。
これはある意味仕方のないことなので、パズルの回答は間違いなく把握するようにしましょう。
さいごに
いかがでしたでしょうか。
ビットコインの所有権については、必ずしも秘密鍵や署名が必要ではないことを知れたと思います。
これがビットコインの面白い仕様であり、難しいところでもあると思います。
これを応用すると、例えばある人の誕生日を知っている人や、秘密のフレーズを知っている人だけがビットコインの所有権を得ることができる、という仕組みも実現できます。
興味ある方は更に調べてみると面白いと思います。
最後まで読んで頂き、ありがとうございました。