Closed41

GetBlock で Bitcoin のエンドポイントを作ってみる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Docker から bitcoin-cli を使う

mkdir getblocks
cd getblocks
touch Dockerfile
Dockerfile
FROM lncm/bitcoind:v22.0

USER root

RUN apk add --update bash 
ENTRYPOINT bash
$ docker build -t bitcoind .
[+] Building 4.2s (6/6) FINISHED                                                
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 121B                                       0.0s
 => [internal] load .dockerignore                                          0.0s
 => => transferring context: 2B                                            0.0s
 => [internal] load metadata for docker.io/lncm/bitcoind:v22.0             0.0s
 => CACHED [1/2] FROM docker.io/lncm/bitcoind:v22.0                        0.0s
 => [2/2] RUN apk add --update bash                                        4.1s
 => exporting to image                                                     0.1s
 => => exporting layers                                                    0.0s
 => => writing image sha256:26b5ac1e384b6af3e6b702654bea9d1010ea06a778b2e  0.0s
 => => naming to docker.io/library/bitcoind                                0.0s
                                                                                
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
$ docker run -it bitcoind
bash-5.0# bitcoin-cli --version
Bitcoin Core RPC client version v22.0.0
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

bitcoin-cli では IP アドレスしか設定できない?

となると下記のように HTTP リクエストを送ることになる

curl \
  --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getrpcinfo", "params": []}' \
  -H 'content-type: text/plain;' \
  https://btc.getblock.io/00000000-0000-0000-0000-000000000000/testnet/
{
  "result": {
    "active_commands": [{ "method": "getrpcinfo", "duration": 24 }],
    "logpath": "/home/bitcoin/.bitcoin/testnet3/debug.log"
  },
  "error": null,
  "id": "curltest"
}

すいません、Docker とかの手順は不要でした

とはいえ bitcoin-cli を使えないのは不便だな

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

リクエストは1日10万回まで送信できるらしい

Each day we send you 100K requests for free! Unused requests from the free package can’t be transferred to the next day.

太っ腹

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

createwallet とかはできない

共有ノードだから当たり前か

touch create-wallet.json
create-wallet.json
{
  "jsonrpc": "1.0",
  "id": "curltest",
  "method": "createwallet",
  "params": ["testwallet"]
}
curl -v --data-binary @create-wallet.json \
  -H 'content-type: text/plain;' \
  https://btc.getblock.io/00000000-0000-0000-0000-000000000000/testnet/
*   Trying 65.108.42.230:443...
* Connected to btc.getblock.io (65.108.42.230) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: CN=*.getblock.io
*  start date: Nov  7 09:00:26 2022 GMT
*  expire date: Feb  5 09:00:25 2023 GMT
*  subjectAltName: host "btc.getblock.io" matched cert's "*.getblock.io"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
> POST /7f321c06-a74f-4b80-92c3-cb90de0c5d10/testnet/ HTTP/1.1
> Host: btc.getblock.io
> User-Agent: curl/7.79.1
> Accept: */*
> content-type: text/plain;
> Content-Length: 99
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< date: Tue, 03 Jan 2023 08:28:58 GMT
< content-length: 0
< content-type: text/html; charset=ISO-8859-1
< x-envoy-upstream-service-time: 249
< x-cluster: Shared nodes
< server: envoy
< 
* Connection #0 to host btc.getblock.io left intact

鍵を作ったりするのには結局 Docker が必要になるのか...

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Docker でもテストネットの鍵を作るには regtest モード以外で bitcoind を起動する必要がある

これでは何のために GetBlocks を使うのかがわからなくなる

なんとなくだけど GetBlocks の JSON-RPC エンドポイントに RawTransaction を送っても大丈夫そうな気がする

RawTransaction を作るには下記のようなライブラリが役に立ちそう

https://www.npmjs.com/package/bitcore-lib

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

テストネット用アドレスの作成

npm init -y
npm init --save-dev @types/bitcore-lib bitcore-lib ts-node
touch address.ts
address.ts
import { Address, Networks, PrivateKey } from "bitcore-lib";

const privateKey = new PrivateKey();
const publicKey = privateKey.toPublicKey();
const address = new Address(publicKey, Networks.testnet);

console.log(JSON.stringify({
  privateKey: privateKey.toString(),
  address: address.toString(),
}, null, 2))
npx ts-node address.ts > address.json
address.json
{
  "privateKey": "0000000000000000000000000000000000000000000000000000000000000000",
  "address": "mofDKpPWP8vG2ZaLQeSKxF9bZ2aQjkDeqk"
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

bitcoinjs-lib アドレス作るのからして難しい

mkdir hello-bitcoinjs
cd hello-bitcoinjs
npm init -y
npm install --save-dev bitcoinjs-lib ecpair tiny-secp256k1 ts-node
touch address.ts
address.ts
import { ECPairFactory, networks } from "ecpair";
import * as bitcoin from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";

const ECPair = ECPairFactory(ecc);
const keyPair = ECPair.makeRandom({
  network: networks.testnet,
});

const { address: p2pkh } = bitcoin.payments.p2pkh({
  pubkey: keyPair.publicKey,
  network: networks.testnet,
});

const { address: p2sh } = bitcoin.payments.p2sh({
  redeem: bitcoin.payments.p2ms({
    m: 1,
    pubkeys: [keyPair.publicKey],
    network: networks.testnet,
  }),
});

const wif = keyPair.toWIF();

const { address: p2wpkh } = bitcoin.payments.p2wpkh({
  pubkey: keyPair.publicKey,
  network: networks.testnet,
});

console.log(JSON.stringify({ p2pkh, p2sh, wif, p2wpkh }, null, 2));
npx ts-node address.ts
{
  "p2pkh": "mh64DSu26HBVXtbgeDgLmBe6Q4Fiaw5sWp",
  "p2sh": "2N5nhhE6W5Xq9AWRPezpLbmBUiqTmxUBBPw",
  "wif": "cP6vmWkxz4WAZYQSFRDhkK1ozEKR1jjyHN1LxxakVv7SrgnCuh54",
  "p2wpkh": "tb1qzyavka0atu2u3924rz4vp77urlasv74yhpkkts"
}

でも色々な形式のアドレスを作れるのは嬉しい

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

bitcoinjs-lib でトランザクションを作る

https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts

it can create a 1-to-1 Transaction のテストコードが何やっているかよくわからない

https://github.com/bitcoinjs/bitcoinjs-lib/issues/1580

スタックオーバーフローに関連する質問があってよかった

https://qiita.com/rndstone/items/f29ff0d6fdb7da95af8d

PSBT: Partially Signed Bitcoin Transactions(部分的署名ビットコイン取引)はトランザクションのフォーマットみたいなものなのかな

https://tech.bitbank.cc/20201213/

Bitcoin testnet3 の テスト用 BTC を下記の Faucet から貰う、ありがとう

https://coinfaucet.eu/

Segwit: Segregated Witness(隔離された証人)もトランザクションの一形式のようだ、PSBT との違いは何だろう

https://tech.bitbank.cc/20201213/

Bitbank の記事が勉強になる

トランザクションの ID や内容、生データについては下記から調べられる

https://blockstream.info/testnet/api/address/mfs8TLBd62Pusu9dsjCugFGkop7qveLASn/utxo

https://blockstream.info/testnet/api/tx/36503c549d044dd64de1e2a5f1ecd3472f2ac6a1d11902ac3b5c5f01f5d61474

https://blockstream.info/testnet/api/tx/36503c549d044dd64de1e2a5f1ecd3472f2ac6a1d11902ac3b5c5f01f5d61474/hex

blockstream.info の API については下記がドキュメント

https://github.com/Blockstream/esplora/blob/master/API.md

自分のアドレス tb1qj3tmla4tmjtvwu0c5adl6vt6m68v3rw246hg0u を使って調べてみた

UTXO
[
  {
    "txid": "cd94b51ca31e6c743dae0d1c0d562848e04ef1e7d7b96622fa5327e066a41043",
    "vout": 0,
    "status": {
      "confirmed": true,
      "block_height": 2414788,
      "block_hash": "000000000000002be58f00eb479097af19ee998064aa2d870a131fe962407b01",
      "block_time": 1672961447
    },
    "value": 1750922
  }
]
トランザクションのメタデータ
{
  "txid": "cd94b51ca31e6c743dae0d1c0d562848e04ef1e7d7b96622fa5327e066a41043",
  "version": 2,
  "locktime": 2414707,
  "vin": [
    {
      "txid": "279d533a6004b283c1437ecf39c9870bc8ebb14b901b850d10e45b41e84934de",
      "vout": 1,
      "prevout": {
        "scriptpubkey": "0014dc97571f85e558d3e79b7fa8452ce1379bbbe7f4",
        "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 dc97571f85e558d3e79b7fa8452ce1379bbbe7f4",
        "scriptpubkey_type": "v0_p2wpkh",
        "scriptpubkey_address": "tb1qmjt4w8u9u4vd8eum075y2t8px7dmhel5fm3cx9",
        "value": 869411879
      },
      "scriptsig": "",
      "scriptsig_asm": "",
      "witness": [
        "304402203ec88e56641439dd63b2e79cbecfc8b4ecb4a09b28ab757043ff59ebb39608080220484b28fa88caf0b7144a76bb70a2e9f895389fcee04d26b87ffd2ad2de73c60601",
        "025edf5111c728ceb5c1439adf8fc702c14e2c4b5e17594d686a593a02462f44ba"
      ],
      "is_coinbase": false,
      "sequence": 4294967294
    }
  ],
  "vout": [
    {
      "scriptpubkey": "00149457bff6abdc96c771f8a75bfd317ade8ec88dca",
      "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 9457bff6abdc96c771f8a75bfd317ade8ec88dca",
      "scriptpubkey_type": "v0_p2wpkh",
      "scriptpubkey_address": "tb1qj3tmla4tmjtvwu0c5adl6vt6m68v3rw246hg0u",
      "value": 1750922
    },
    {
      "scriptpubkey": "0014336ae5fbce04dca6856d61913ee54363786b9590",
      "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 336ae5fbce04dca6856d61913ee54363786b9590",
      "scriptpubkey_type": "v0_p2wpkh",
      "scriptpubkey_address": "tb1qxd4wt77wqnw2dptdvxgnae2rvduxh9vsnkqx2r",
      "value": 867646534
    }
  ],
  "size": 222,
  "weight": 561,
  "fee": 14423,
  "status": {
    "confirmed": true,
    "block_height": 2414788,
    "block_hash": "000000000000002be58f00eb479097af19ee998064aa2d870a131fe962407b01",
    "block_time": 1672961447
  }
}
トランザクション生データ
02000000000101de3449e8415be4100d851b904bb1ebc80b87c939cf7e43c183b204603a539d270100000000feffffff028ab71a00000000001600149457bff6abdc96c771f8a75bfd317ade8ec88dca463cb73300000000160014336ae5fbce04dca6856d61913ee54363786b95900247304402203ec88e56641439dd63b2e79cbecfc8b4ecb4a09b28ab757043ff59ebb39608080220484b28fa88caf0b7144a76bb70a2e9f895389fcee04d26b87ffd2ad2de73c6060121025edf5111c728ceb5c1439adf8fc702c14e2c4b5e17594d686a593a02462f44ba73d82400

確かに両方に ScriptPubKey の 00149457bff6abdc96c771f8a75bfd317ade8ec88dca が含まれていることがわかる

トランザクションが segwit であるかどうかはどうやって調べれば良いんだろう

Blockstream でトランザクションを確認すると segwit であることはとりあえずわかる

https://blockstream.info/testnet/tx/cd94b51ca31e6c743dae0d1c0d562848e04ef1e7d7b96622fa5327e066a41043

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

何でも知っている Stack Overflow は偉大

https://bitcoin.stackexchange.com/questions/77286/how-to-verify-if-its-segwit-transaction-or-not

1つ以上のトランザクション入力が witness を含むか、生トランザクションの 5 バイト目が 00 だったら segwit のようだ

A transaction is a segwit tx if at least one of the inputs contain a witness. Or if you are inspecting the raw tx then you check the 5th byte (the input count) and if it is 0x00 then it is a segwit tx.

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

トランザクションが作れない

touch transaction.ts
transaction.ts
import { Psbt } from "bitcoinjs-lib";
import { ECPairFactory, networks } from "ecpair";
import { readFileSync } from "fs";
import * as ecc from "tiny-secp256k1";

const ECPair = ECPairFactory(ecc);
const { wif, p2wpkh } = JSON.parse(readFileSync("address.json", "utf8"));
const keyPair = ECPair.fromWIF(wif, networks.testnet);
const psbt = new Psbt();

psbt.addInput({
  hash: "cd94b51ca31e6c743dae0d1c0d562848e04ef1e7d7b96622fa5327e066a41043",
  index: 0,
  witnessUtxo: {
    script: Buffer.from("00149457bff6abdc96c771f8a75bfd317ade8ec88dca", "hex"),
    value: 175_0922,
  },
});

psbt.addOutput({
  address: p2wpkh,
  value: 175_0000,
});

psbt.signInput(0, keyPair);

const validator = (
  pubkey: Buffer,
  msghash: Buffer,
  signature: Buffer
): boolean =>
  ECPair.fromPublicKey(pubkey, { network: networks.testnet }).verify(
    msghash,
    signature
  );

psbt.validateSignaturesOfInput(0, validator);
psbt.finalizeAllInputs();
console.log(psbt.extractTransaction().toHex());
npx ts-node transaction.ts
/Users/susukida/workspace/js/hello-bitcoinjs/node_modules/bitcoinjs-lib/src/address.js:137
        throw new Error(address + ' has an invalid prefix');
              ^
Error: tb1qj3tmla4tmjtvwu0c5adl6vt6m68v3rw246hg0u has an invalid prefix
    at toOutputScript (/Users/susukida/workspace/js/hello-bitcoinjs/node_modules/bitcoinjs-lib/src/address.js:137:15)
    at Psbt.addOutput (/Users/susukida/workspace/js/hello-bitcoinjs/node_modules/bitcoinjs-lib/src/psbt.js:240:51)
    at Object.<anonymous> (/Users/susukida/workspace/js/hello-bitcoinjs/transaction.ts:20:6)
    at Module._compile (node:internal/modules/cjs/loader:1126:14)
    at Module.m._compile (/Users/susukida/workspace/js/hello-bitcoinjs/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/susukida/workspace/js/hello-bitcoinjs/node_modules/ts-node/src/index.ts:1621:12)
    at Module.load (node:internal/modules/cjs/loader:1004:32)
    at Function.Module._load (node:internal/modules/cjs/loader:839:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

やっとトランザクションらしきものを作れた

ポイントは下記の通り

  • payments.p2wpkh() を使うこと
  • アドレスではなく pubkey を指定すること
  • psbt.addOutput() を呼び出すときに address ではなく script フィールドを使うこと
transaction.ts
import { payments, Psbt } from "bitcoinjs-lib";
import { ECPairFactory, networks } from "ecpair";
import { readFileSync } from "fs";
import * as ecc from "tiny-secp256k1";

function main() {
  const ECPair = ECPairFactory(ecc);
  const address = JSON.parse(readFileSync("address.json", "utf8"));
  const keyPair = ECPair.fromWIF(address.wif, networks.testnet);
  const psbt = new Psbt();

  psbt.addInput({
    hash: "cd94b51ca31e6c743dae0d1c0d562848e04ef1e7d7b96622fa5327e066a41043",
    index: 0,
    witnessUtxo: {
      script: Buffer.from(
        "00149457bff6abdc96c771f8a75bfd317ade8ec88dca",
        "hex"
      ),
      value: 175_0922,
    },
  });

  const payment = payments.p2wpkh({
    pubkey: keyPair.publicKey,
    network: networks.testnet,
  });

  psbt.addOutput({
    script: payment.pubkey!,
    value: 175_0000,
  });

  psbt.signInput(0, keyPair);

  const validator = (
    pubkey: Buffer,
    msghash: Buffer,
    signature: Buffer
  ): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature);

  psbt.validateSignaturesOfInput(0, validator);
  psbt.finalizeAllInputs();

  console.log(psbt.extractTransaction().toHex());
}

main();

実行結果

020000000001014310a466e02753fa2266b9d7e7f14ee04828560d1c0dae3d746c1ea31cb594cd0000000000ffffffff01f0b31a00000000002102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa02483045022100bd0a86def594e8131b0588f0607dd05bfbd4da02fcd6942fe540b84cafe092aa022072862fce9271e1e3792c027eda81e136691f1cb5e0eec98a2dd2f5668c108c89012102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa00000000

【追記】上記のトランザクションを送信した結果、送金先が Unknown になって時空の間に消えていったのでご注意ください

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

curl する時に面倒なので

API キーをエクスポートしておく

apiKey=00000000-0000-0000-0000-000000000000

使うときは下記のような感じ

echo https://btc.getblock.io/${apiKey}/testnet/
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

生トランザクションを投げてみた

curl \
  --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "sendrawtransaction", "params": ["020000000001014310a466e02753fa2266b9d7e7f14ee04828560d1c0dae3d746c1ea31cb594cd0000000000ffffffff01f0b31a00000000002102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa02483045022100bd0a86def594e8131b0588f0607dd05bfbd4da02fcd6942fe540b84cafe092aa022072862fce9271e1e3792c027eda81e136691f1cb5e0eec98a2dd2f5668c108c89012102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa00000000"]}' \
  -H 'content-type: application/json;' \
  https://btc.getblock.io/${apiKey}/testnet/
{
  "result": "859faeb8471af436db112d007a5f627f57c9e6ed6f151111846ca6a555c46c84",
  "error": null,
  "id": "curltest"
}

なんか一瞬にしてテスト用のビットコインを失ってしまった気がするぞ笑

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

有効な Script Pubkey

00149457bff6abdc96c771f8a75bfd317ade8ec88dca
0014336ae5fbce04dca6856d61913ee54363786b9590

どうやら 0014 から始まっている様子

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

そもそも Script Pubkey って何?

https://programmingblockchain.gitbook.io/programmingblockchain-japanese/bitcoin_transfer/payment_script

Script Pubkey は送金するための条件と考えて良さそう

OP_DUP OP_HASH160 14836dbe7f38c5ac3d49e8d790af808a4ee9edcf OP_EQUALVERIFY OP_CHECKSIG

上記の先頭に1つ以上の何らかの命令を追加して実行結果が有効であれば送金が行われる

何が正しい命令かどうかは秘密鍵を持っている人だけが生成することができる

Script Pubkey の中に公開鍵が含まれている

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Script Pubkey らしいものがあった

const payment = payments.p2wpkh({
  pubkey: keyPair.publicKey,
  network: networks.testnet,
});

console.log(payment.output);
実行結果
<Buffer 00 14 94 57 bf f6 ab dc 96 c7 71 f8 a7 5b fd 31 7a de 8e c8 8d ca>

これって witnessUtxo に指定しているものと同じだ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

仕方ない勢いでやってみよう

transaction.ts
import { payments, Psbt } from "bitcoinjs-lib";
import { ECPairFactory, networks } from "ecpair";
import { readFileSync } from "fs";
import * as ecc from "tiny-secp256k1";

function main() {
  const ECPair = ECPairFactory(ecc);
  const address = JSON.parse(readFileSync("address.json", "utf8"));
  const keyPair = ECPair.fromWIF(address.wif, networks.testnet);
  const psbt = new Psbt();

  const tx = "634e8307a6df2b8ebfacd5d845d26a55c15f07b637ad403d35515a99b1f25727";
  const index = 1;
  const satoshi = 197_2910
  const fee = 200

  psbt.addInput({
    hash: tx,
    index: index,
    witnessUtxo: {
      script: Buffer.from(
        "00149457bff6abdc96c771f8a75bfd317ade8ec88dca",
        "hex"
      ),
      value: satoshi,
    },
  });

  const payment = payments.p2wpkh({
    pubkey: keyPair.publicKey,
    network: networks.testnet,
  });

  psbt.addOutput({
    script: payment.output!,
    value: satoshi - fee,
  });

  psbt.signInput(0, keyPair);

  const validator = (
    pubkey: Buffer,
    msghash: Buffer,
    signature: Buffer
  ): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature);

  psbt.validateSignaturesOfInput(0, validator);
  psbt.finalizeAllInputs();

  console.log(psbt.extractTransaction().toHex());
}

main();
npx ts-node transaction.ts > transaction.txt
transaction.txt
020000000001012757f2b1995a51353d40ad37b6075fc1556ad245d8d5acbf8e2bdfa607834e630100000000ffffffff01e6191e00000000001600149457bff6abdc96c771f8a75bfd317ade8ec88dca02483045022100902aee8b50d9b9b820dfa6b6ae7562235f71bf37b316b6ade83d7dcd07d80f7002205688ed84ede141e334d6adcb1235ac0b3bb124cfdebb8c59bc5f7518904e8d34012102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa00000000
curl \
  --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "sendrawtransaction", "params": ["020000000001012757f2b1995a51353d40ad37b6075fc1556ad245d8d5acbf8e2bdfa607834e630100000000ffffffff01e6191e00000000001600149457bff6abdc96c771f8a75bfd317ade8ec88dca02483045022100902aee8b50d9b9b820dfa6b6ae7562235f71bf37b316b6ade83d7dcd07d80f7002205688ed84ede141e334d6adcb1235ac0b3bb124cfdebb8c59bc5f7518904e8d34012102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa00000000"]}' \
  -H 'content-type: application/json;' \
  https://btc.getblock.io/${apiKey}/testnet/
{"result":"c6096734d8a854a79235ece9fe567deba370419b57318f50fce6f493416ca560","error":null,"id":"curltest"}

https://blockstream.info/testnet/tx/c6096734d8a854a79235ece9fe567deba370419b57318f50fce6f493416ca560

どうやらできているっぽいぞ!

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

もしかしてこれで OP_RETURN もできる?

transaction.ts
import { payments, Psbt } from "bitcoinjs-lib";
import { ECPairFactory, networks } from "ecpair";
import { readFileSync } from "fs";
import * as ecc from "tiny-secp256k1";

function main() {
  const ECPair = ECPairFactory(ecc);
  const address = JSON.parse(readFileSync("address.json", "utf8"));
  const keyPair = ECPair.fromWIF(address.wif, networks.testnet);
  const psbt = new Psbt();

  const tx = "9e2b73905beeac7b8416e152a13632c2196d06797e32327bc328d6305a64a817";
  const index = 1;
  const satoshi = 1_0000;
  const fee = 250;

  psbt.addInput({
    hash: tx,
    index: index,
    witnessUtxo: {
      script: Buffer.from(
        "00149457bff6abdc96c771f8a75bfd317ade8ec88dca",
        "hex"
      ),
      value: satoshi,
    },
  });

  const payment = payments.p2wpkh({
    pubkey: keyPair.publicKey,
    network: networks.testnet,
  });

  psbt.addOutput({
    script: payment.output!,
    value: satoshi - fee,
  });

  psbt.addOutput({
    script: payments.embed({
      data: [Buffer.from('https://zenn.dev/tatsuyasusukida/scraps/1943afd6008301', 'utf8')]
    }).output!,
    value: 0,
  });

  psbt.signInput(0, keyPair);

  const validator = (
    pubkey: Buffer,
    msghash: Buffer,
    signature: Buffer
  ): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature);

  psbt.validateSignaturesOfInput(0, validator);
  psbt.finalizeAllInputs();

  console.log(psbt.extractTransaction().toHex());
}

main();
npx ts-node transaction.ts
0200000000010117a8645a30d628c37b32327e79066d19c23236a152e116847bacee5b90732b9e0100000000ffffffff0216260000000000001600149457bff6abdc96c771f8a75bfd317ade8ec88dca0000000000000000386a3668747470733a2f2f7a656e6e2e6465762f74617473757961737573756b6964612f7363726170732f313934336166643630303833303102483045022100c392f0fb0cf15f471ffef3522ae5e159a31faf9e8e893ac5732e22ee2a2b1a1c022060247d8270f2e3a2d4f5fa767bf175d1f4f6ff2de267244f8d5bd68e1c5bcdcd012102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa00000000
curl \
  --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "sendrawtransaction", "params": ["0200000000010117a8645a30d628c37b32327e79066d19c23236a152e116847bacee5b90732b9e0100000000ffffffff0216260000000000001600149457bff6abdc96c771f8a75bfd317ade8ec88dca0000000000000000386a3668747470733a2f2f7a656e6e2e6465762f74617473757961737573756b6964612f7363726170732f313934336166643630303833303102483045022100c392f0fb0cf15f471ffef3522ae5e159a31faf9e8e893ac5732e22ee2a2b1a1c022060247d8270f2e3a2d4f5fa767bf175d1f4f6ff2de267244f8d5bd68e1c5bcdcd012102cacb5776c24ee5d6070c2363d32a0737ebac0720ce5fc201fdd15e1db44c5baa00000000"]}' \
  -H 'content-type: application/json;' \
  https://btc.getblock.io/${apiKey}/testnet/
{"result":"77e5e4505b1847368cb96b361b334704551c2e905751af94753ef0ce6f1b37bb","error":null,"id":"curltest"}

https://blockstream.info/testnet/tx/9e2b73905beeac7b8416e152a13632c2196d06797e32327bc328d6305a64a817

https://live.blockcypher.com/btc-testnet/tx/77e5e4505b1847368cb96b361b334704551c2e905751af94753ef0ce6f1b37bb/

人生初めての OP_RETURN ができた!テストネットだけど感動!

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

おわりに

目標は達成したので一旦クローズ

GetBlock は Bitcoin 標準の RPC を手軽に使えるようにしてくれるのでおすすめです

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

返却するためのコード

transaction.ts
import { payments, Psbt } from "bitcoinjs-lib";
import { ECPairFactory, networks } from "ecpair";
import { readFileSync } from "fs";
import * as ecc from "tiny-secp256k1";

function main() {
  const ECPair = ECPairFactory(ecc);
  const address = JSON.parse(readFileSync("address.json", "utf8"));
  const keyPair = ECPair.fromWIF(address.wif, networks.testnet);
  const psbt = new Psbt();

  const tx = "9e2b73905beeac7b8416e152a13632c2196d06797e32327bc328d6305a64a817";
  const index = 0;
  const satoshi = 0.0196246e8;
  const fee = 120;

  psbt.addInput({
    hash: tx,
    index: index,
    witnessUtxo: {
      script: Buffer.from(
        "00149457bff6abdc96c771f8a75bfd317ade8ec88dca",
        "hex"
      ),
      value: satoshi,
    },
  });

  const payment = payments.p2pkh({
    address: 'mv4rnyY3Su5gjcDNzbMLKBQkBicCtHUtFB',
    network: networks.testnet,
  });

  psbt.addOutput({
    script: payment.output!,
    value: satoshi - fee,
  });

  psbt.signInput(0, keyPair);

  const validator = (
    pubkey: Buffer,
    msghash: Buffer,
    signature: Buffer
  ): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature);

  psbt.validateSignaturesOfInput(0, validator);
  psbt.finalizeAllInputs();

  console.log(psbt.extractTransaction().toHex());
}

main();

https://blockstream.info/testnet/tx/83f670c2f2865e0a11efed6b6bbba8c127546051131b29f368c42b62b4374c56

https://blockstream.info/testnet/tx/83f670c2f2865e0a11efed6b6bbba8c127546051131b29f368c42b62b4374c56

このスクラップは2023/01/09にクローズされました