Skip to content

Data Outputs

BSV Transactions can carry data of any size. Bitcoin4J has two builders for this. One makes a provably unspendable Output that holds nothing but data, and the other attaches data to an ordinary P2PKH Output that stays spendable. The page on data in Script in the Developer Guide describes both patterns, along with the common protocol conventions.

UnspendableDataLockBuilder writes OP_FALSE OP_RETURN followed by one push per buffer. Nobody can spend the Output, so it carries 0 satoshis, and the builder’s dust check leaves it alone.

List<ByteBuffer> pushes = List.of(
ByteBuffer.wrap("example.app".getBytes(StandardCharsets.UTF_8)), // a protocol prefix
ByteBuffer.wrap("hello world".getBytes(StandardCharsets.UTF_8))); // the payload
UnspendableDataLockBuilder dataLocker = new UnspendableDataLockBuilder(pushes);
Transaction tx = new TransactionBuilder()
.spendFromTransaction(new TransactionSigner(SIGHASH, alice), fundingTx, 0,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(dataLocker, BigInteger.ZERO)
.sendChangeTo(aliceAddress)
.build(true); // the dust check leaves zero-satoshi data Outputs alone

The resulting Locking Script is 0 OP_RETURN 6578616d706c652e617070 68656c6c6f20776f726c64 (0 is how ASM writes OP_FALSE).

Splitting our data into several pushes, with a protocol identifier first, is the convention that most indexers expect.

To read the pushes back out of an Output, we use the OP_RETURN Script template, shown on the Script templates page.

P2PKHDataLockBuilder writes <data> OP_DROP in front of a P2PKH script. The data is pushed and dropped again, and the rest is an ordinary pay-to-address check. This means the Output can hold real value, and the owner of the Address spends it as usual.

// The data is pushed and dropped in front of an ordinary P2PKH script, so the Output
// carries our data and stays spendable by the key.
ByteBuffer data = ByteBuffer.wrap("invoice 1234".getBytes(StandardCharsets.UTF_8));
Transaction dataTx = new TransactionBuilder()
.spendFromTransaction(new TransactionSigner(SIGHASH, alice), fundingTx, 0,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHDataLockBuilder(aliceAddress, data), BigInteger.valueOf(10_000))
.sendChangeTo(aliceAddress)
.build(true);

Spending it needs nothing special, only a P2PKHUnlockBuilder as for any P2PKH Output.

// We spend it like any P2PKH Output, with a Signature and the Public Key.
Transaction spendTx = new TransactionBuilder()
.spendFromTransaction(new TransactionSigner(SIGHASH, alice), dataTx, 0,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHLockBuilder(aliceAddress), BigInteger.valueOf(9_744))
.build(true);

To get the data back, we parse the Locking Script with the same builder.

P2PKHDataLockBuilder parsed = new P2PKHDataLockBuilder(dataOutput.getScript());
String text = new String(parsed.getDataBuffer().array(), StandardCharsets.UTF_8);

P2PKHDataLockBuilder takes a single buffer. For several fields, we either encode them into one buffer or write a custom locking builder.

Please note that both builders push each buffer with ScriptBuilder.data(), which writes a one-byte buffer holding a value from 1 to 16 as the opcode OP_1 to OP_16 (the page on building scripts explains why).