Bitcoin Processing Unit
Transform Bitcoin Transactions into Virtual Procedure Call Units.
- Turns a Bitcoin transaction inputs and outputs into a structured format called "tape"
- Chunk the push data sequence into "cells"
- Transform each push data through a custom transform function
BPU is a fork of TXO, but with a little different structure, and with customization features.
At a high level, BPU is a library that creates a nested structure of tapes and cells from a Bitcoin transaction with the following rule:
i
: positional index for whichever item it's attached to. applies to bothtape
andcell
.ii
: global positional index. only applies tocell
. For example, a cell can have a local indexi
of 0, but global indexii
of 3.b
: base64 representation of a push data.s
: UTF8 representation of a push data.op
: a Bitcoin opcode number. only applies to push data items which are opcodes. (not buffer type push data)ops
: a Bitcoin opcode string. only applies to push data items which are opcodes. (not buffer type push data)
BPU also lets you generate your own custom serialization format. One example is BOB (Bitcoin OP_RETURN Bytecode).
You can learn more about BOB here: https://medium.com/@_unwriter/hello-bob-94701d278afb
Also see how this format can be stored in a DB: https://bob.planaria.network/
Here's an example BPU parsed object, more specifically a BOB serialization format:
{
"tx": {
"h": "7e5011d81d01c67e81ab9a50ab3077bb62b340d1b50592931e4b52afa7198e65"
},
"in": [
{
"i": 0,
"tape": [
{
"cell": [
{
"b": "MEQCIAwf5m6RfA1UICSFTlG2h4NEyfFui7qtxgria2EL1iVJAiA23uD0THoqzh9ONMMoxRMlJqqI8YU0dacNhqCkMaPY0kE=",
"s": "0D\u0002 \f\u001f�n�|\rT $�NQ���D��n����\n�ka\u000b�%I\u0002 6���Lz*�\u001fN4�(�\u0013%&���4u�\r���1���A",
"ii": 0,
"i": 0
},
{
"b": "AvLkgpq1RgHcLPYH7Q9uxCZnRgT0TDQwP2OEYmHZuWWb",
"s": "\u0002�䂚�F\u0001�,�\u0007�\u000fn�&gF\u0004�L40?c�baٹe�",
"ii": 1,
"i": 1
}
],
"i": 0
}
],
"e": {
"h": "9c4e6f32d9b49d439c7413ecb14662cc4853f9fb7f66b99965d8eb2610c0653d",
"i": 1,
"a": "17SnqQYGNZuD4zpu8eemt26TrdbvoJC5ie"
}
}
],
"out": [
{
"i": 0,
"tape": [
{
"cell": [
{
"op": 106,
"ops": "OP_RETURN",
"ii": 0,
"i": 0
}
],
"i": 0
},
{
"cell": [
{
"b": "MTlIeGlnVjRReUJ2M3RIcFFWY1VFUXlxMXB6WlZkb0F1dA==",
"s": "19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut",
"ii": 1,
"i": 0
},
{
"b": "SEFIQUhBSEE=",
"s": "HAHAHAHA",
"ii": 2,
"i": 1
},
{
"b": "dGV4dC9wbGFpbg==",
"s": "text/plain",
"ii": 3,
"i": 2
},
{
"b": "dGV4dA==",
"s": "text",
"ii": 4,
"i": 3
},
{
"b": "dHdldGNoX3R3dGV4dF8xNTY1MTMxNDIzNTU2LnR4dA==",
"s": "twetch_twtext_1565131423556.txt",
"ii": 5,
"i": 4
}
],
"i": 1
},
{
"cell": [
{
"b": "fA==",
"s": "|",
"ii": 6,
"i": 0
}
],
"i": 2
},
{
"cell": [
{
"b": "MVB1UWE3SzYyTWlLQ3Rzc1NMS3kxa2g1NldXVTdNdFVSNQ==",
"s": "1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5",
"ii": 7,
"i": 0
},
{
"b": "U0VU",
"s": "SET",
"ii": 8,
"i": 1
},
{
"b": "dHdkYXRhX2pzb24=",
"s": "twdata_json",
"ii": 9,
"i": 2
},
{
"b": "bnVsbA==",
"s": "null",
"ii": 10,
"i": 3
},
{
"b": "dXJs",
"s": "url",
"ii": 11,
"i": 4
},
{
"b": "bnVsbA==",
"s": "null",
"ii": 12,
"i": 5
},
{
"b": "Y29tbWVudA==",
"s": "comment",
"ii": 13,
"i": 6
},
{
"b": "bnVsbA==",
"s": "null",
"ii": 14,
"i": 7
},
{
"b": "bWJfdXNlcg==",
"s": "mb_user",
"ii": 15,
"i": 8
},
{
"b": "MzY2Nw==",
"s": "3667",
"ii": 16,
"i": 9
},
{
"b": "cmVwbHk=",
"s": "reply",
"ii": 17,
"i": 10
},
{
"b": "ODg0OTc3MDA4ZjdkMGE2Y2U1YjM5NzZkNTA4MzJjMGJiOGQ3ZTU5MjRiMWZkZTYwMGQ0YjE3NDFjZTVkYTBmOQ==",
"s": "884977008f7d0a6ce5b3976d50832c0bb8d7e5924b1fde600d4b1741ce5da0f9",
"ii": 18,
"i": 11
},
{
"b": "dHlwZQ==",
"s": "type",
"ii": 19,
"i": 12
},
{
"b": "cmVwbHk=",
"s": "reply",
"ii": 20,
"i": 13
},
{
"b": "dGltZXN0YW1w",
"s": "timestamp",
"ii": 21,
"i": 14
},
{
"b": "Mzk5NjU4OTEwMTMwMDc=",
"s": "39965891013007",
"ii": 22,
"i": 15
},
{
"b": "YXBw",
"s": "app",
"ii": 23,
"i": 16
},
{
"b": "dHdldGNo",
"s": "twetch",
"ii": 24,
"i": 17
}
],
"i": 3
},
{
"cell": [
{
"b": "fA==",
"s": "|",
"ii": 25,
"i": 0
}
],
"i": 4
},
{
"cell": [
{
"b": "MTVQY2lIRzIyU05MUUpYTW9TVWFXVmk3V1NxYzdoQ2Z2YQ==",
"s": "15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva",
"ii": 26,
"i": 0
},
{
"b": "QklUQ09JTl9FQ0RTQQ==",
"s": "BITCOIN_ECDSA",
"ii": 27,
"i": 1
},
{
"b": "MUFLSFZpWWdCR2JteGk4cWlKa052b0hOZUR1OW0zTWZQRQ==",
"s": "1AKHViYgBGbmxi8qiJkNvoHNeDu9m3MfPE",
"ii": 28,
"i": 2
},
{
"b": "IyNjb21wdXRlZF9zaWcjIw==",
"s": "##computed_sig##",
"ii": 29,
"i": 3
}
],
"i": 5
}
],
"e": {
"v": 0,
"i": 0,
"a": "false"
}
},
{
"i": 1,
"tape": [
{
"cell": [
{
"op": 118,
"ops": "OP_DUP",
"ii": 0,
"i": 0
},
{
"op": 169,
"ops": "OP_HASH160",
"ii": 1,
"i": 1
},
{
"b": "CUcuns23XoX3EFhf0EVmDIPPqXk=",
"s": "\tG.�ͷ^��\u0010X_�Ef\f�ϩy",
"ii": 2,
"i": 2
},
{
"op": 136,
"ops": "OP_EQUALVERIFY",
"ii": 3,
"i": 3
},
{
"op": 172,
"ops": "OP_CHECKSIG",
"ii": 4,
"i": 4
}
],
"i": 0
}
],
"e": {
"v": 6900,
"i": 1,
"a": "1r4MYgWbmzz7g2HdEqLDusTQ4ZwrtEVCY"
}
},
{
"i": 2,
"tape": [
{
"cell": [
{
"op": 118,
"ops": "OP_DUP",
"ii": 0,
"i": 0
},
{
"op": 169,
"ops": "OP_HASH160",
"ii": 1,
"i": 1
},
{
"b": "QRtHnMuB1770Vq3uNkvofZDiKyM=",
"s": "A\u001bG�ˁ�V��6K�}��+#",
"ii": 2,
"i": 2
},
{
"op": 136,
"ops": "OP_EQUALVERIFY",
"ii": 3,
"i": 3
},
{
"op": 172,
"ops": "OP_CHECKSIG",
"ii": 4,
"i": 4
}
],
"i": 0
}
],
"e": {
"v": 6900,
"i": 2,
"a": "16wFbu6pRa2sqDrnha6N56HVN91VppofyF"
}
},
{
"i": 3,
"tape": [
{
"cell": [
{
"op": 118,
"ops": "OP_DUP",
"ii": 0,
"i": 0
},
{
"op": 169,
"ops": "OP_HASH160",
"ii": 1,
"i": 1
},
{
"b": "QRtHnMuB1770Vq3uNkvofZDiKyM=",
"s": "A\u001bG�ˁ�V��6K�}��+#",
"ii": 2,
"i": 2
},
{
"op": 136,
"ops": "OP_EQUALVERIFY",
"ii": 3,
"i": 3
},
{
"op": 172,
"ops": "OP_CHECKSIG",
"ii": 4,
"i": 4
}
],
"i": 0
}
],
"e": {
"v": 43793,
"i": 3,
"a": "16wFbu6pRa2sqDrnha6N56HVN91VppofyF"
}
}
]
}
npm install --save bpu
const BPU = require('bpu');
// 'rawtx' is a raw transaction string
(async function() {
let result = await BPU.parse({
tx: { r: rawtx }
})
})();
In case you have access to JSON-RPC, you can parse directly from txid.
The first step is to update the .env
file
BITCOIN_USERNAME=[Bitcoin Node Username]
BITCOIN_PASSWORD=[Bitcoin Node Password]
BITCOIN_IP=[Bitcoin Node IP]
BITCOIN_PORT=[Bitcoin Node Port]
Then you can use it like this:
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid }
})
})();
Split with "|" (in UTF8)
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { s: "|" }
}]
})
})();
Split with "fA==" in base64
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { b: "fA==" }
}]
})
})();
Split with OP_RETURN ({op: 106}
)
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { op: 106 }
}]
})
})();
Split with OP_RETURN ({ops: "OP_RETURN"}
)
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { ops: "OP_RETURN" }
}]
})
})();
Split with OP_CODESEPARATOR ({ops: "OP_CODESEPARATOR"}
)
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { ops: "OP_CODESEPARATOR" }
}]
})
})();
By default, "split" discards the delimiter from the result.
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { s: "|" }
}]
})
})();
With "include": "l"
, you can merge the delimiter to the left side of the split arrays:
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { s: "|" },
include: "l"
}]
})
})();
With "include": "r"
, you can merge the delimiter to the right side of the split arrays:
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { s: "|" },
include: "r"
}]
})
})();
With "include": "c"
, you can create a new standalone cell which contains the delimiter
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { s: "|" },
include: "c"
}]
})
})();
You can split based on multiple delimiters. For example BOB chunks scripts based on OP_RETURN
and |
.
const BPU = require('bpu');
(async function() {
let result = await BPU.parse({
tx: { r: raw },
split: [{
token: { s: "|" }
}, {
token: { ops: "OP_RETURN" },
include: "l"
}]
})
})();
Transform each input/output script object:
const BPU = require('bpu');
// 'txid' is a transaction id
(async function() {
let result = await BPU.parse({
tx: { h: txid },
split: [{
token: { s: "|" }
}],
transform: function(o, c) {
// if the buffer is larger than 512 bytes,
// replace the key with "l" prepended attribute
if (c.buf && c.buf.byteLength > 512) {
o["ls"] = o.s;
o["lb"] = o.b;
delete o.s;
delete o.b;
}
return o;
},
})
})