Data in Script

Bitcoin script provides several OP_CODES specifically targeted at pushing data onto the stack to be part of any computation one might want to perform. There exists a very specific edge case wherein one can essentially use the bitcoin ledger for arbitrary data storage. It should be noted that to the extent that one has to pay the Transaction Processors (miners) a fee, and to the extent that this fee is calculated in terms of bitcoin-per-byte, pushing large amounts of data into a transaction can get prohibitively expensive at the moment. There are however plenty of use-cases wherein one might push small amounts of data which has some monetization strategy tied to it. The general model for pushing data onto the bitcoin ledger involves:

  • making a transaction output unspendable by prepending OP_FALSE OP_RETURN as the first two opcodes of the scriptPubkey (output script).
  • adding arbitrary data elements to the bitcoin script, in accordance with some data model using OP_PUSHDATA operations.
  • setting the output amount of satoshis to ZERO. Failing to take this step will permanently lock up funds.

Locking script (scriptPubkey)

The scriptPubkey still needs to be a valid script. It therefore follows that after making the output unspendable by prepending OP_FALSE OP_RETURN to the start of the script, that one can add any arbitrary, valid bitcoin script operators and operands after that. For the purposes of using this method for the storage of data however, one might want to replicate a domain model as a series of OP_PUSHDATA operations, or even one OP_PUSHDATA operation which e.g. contains a serialized Protocol Buffer

The general form of the script is therefore :

OP_FALSE OP_RETURN <data 1> <data 2> <data n>

Unlocking script (scriptSig)

Data-only Transaction Outputs do not have corresponding unlocking scripts. They are by definition unspendable.

A code example

The SDK contains a very simple DataLockBuilder, which will take a byte buffer, and create a data output script that has the following form :

OP_FALSE OP_RETURN <data>

We can now modify the code from the P2PKH section, and add a data output as follows:


  //Let's reconstruct our Private Key
  var privateKey = SVPrivateKey.fromWIF("cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs");

  //Creates Address : "mu4DpTaD75nheE4z5CQazqm1ivej1vzL4L"
  var recipientAddress = privateKey.toAddress();  // my address

  var changeAddress = Address("n3aZKucfWmXeXhX13MREQQnqNfbrWiYKtg"); //bitcoin-cli wallet address

  //Create a Transaction instance from the RAW transaction data created by bitcoin-cli.
  //Remember that this transaction contains the UTXO we are interested in
 var rawTx = '020000000380e0e6a8a9da9fd29a7d870ee2b923e8590503d12d646b456606176dd268b0550000000049483045022100a3ba204ce3462ad32c2f8def14226ac616a7fb1ab91c50228e9494957f6170ba022048b7318a8d2fd3acf37e0150f222d345ffd4f5ebfe2a808f82c0f7698efcbd1541feffffffa58b78d2779f8174b4402f56826b07e09f5e20a71d889b6d9d11f82aaa5448020000000049483045022100caa4460fb6cf9dacffa0f0202823cf2abc92042638067ea7113cdf719b4da58a02200c4a85f19e875d207312c694f3e6986c2cf1212bafcb18d872646264ad4312b941feffffffd0ed2ee51546fb5c6ab96182679dc1e16003a8cee2858477cfe7b18adb33a5b70000000049483045022100f82a281c57b9ad7f9fa2d8238e8cedb2f7be6049fa0293a8e82dea460ef7ab3e022017bf39316d7e64ea6a5dc279acdfc3dafeef127111f7cf6e78d3a4ba8ea1cf4b41feffffff0200e40b54020000001976a91494837d2d5d6106aa97db38957dcc294181ee91e988ac30d80295000000001976a91421e0df2870cb0a8948d7fa6165049c99416988b588ac4e010000';

 var txWithUTXO = Transaction.fromHex(rawTx);

  //Let's create the set of Spending Transaction Inputs. These Transaction Inputs need to refer to the Outputs in
  //the Transaction we are spending from.
  var utxo = txWithUTXO.outputs[0]; //looking at the decoded JSON we can see that our UTXO is at vout[0]

  //create a data output builder
  var encodedData = utf8.encode("Hello, onchain data world!");
  var dataLockBuilder = DataLockBuilder(encodedData);

  var unlockBuilder = P2PKHUnlockBuilder(privateKey.publicKey);
  var transaction = new Transaction()
      .spendFromOutput(utxo, Transaction.NLOCKTIME_MAX_VALUE, scriptBuilder: unlockBuilder) //implicitly creates correct Inputs in spending transaction
      .spendTo(recipientAddress, BigInt.from(50000000), scriptBuilder: P2PKHLockBuilder(recipientAddress)) //spend half a bitcoin == 50 million satoshis (creates Outputs in spending transaction)
      .addData(scriptBuilder: dataLockBuilder) //add the output
      .sendChangeTo(changeAddress, scriptBuilder: P2PKHLockBuilder(changeAddress)) // spend change to myself
      .withFeePerKb(1000); //how much we are willing to pay the miners

  //Sign the Transaction Input
  transaction.signInput(0, privateKey, sighashType: SighashType.SIGHASH_ALL | SighashType.SIGHASH_FORKID);

  print(transaction.serialize());

Send it!

Data Serialization Protocols

Within the BitcoinSV ecosystem there are essentially two major methods of serializing Data-only Transaction Outputs at the moment. Both of these methods use a “protocol prefix” as the first data push following OP_FALSE OP_RETURN:

  • _unwriter’s B:// protocol employs a user-defined Bitcoin Address as the “protocol prefix”

  • individuals and organizations claim an arbitrary constant value as their “protocol prefix”

  • Flags in OP_RETURN (credit Jonathan Aird ?)

Bitcoin Address Prefix

Arbitrary Constant Prefix