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.
Writing an OP_FALSE OP_RETURN Output
Section titled “Writing an OP_FALSE OP_RETURN Output”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 payloadUnspendableDataLockBuilder 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 aloneThe 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.
Spendable data with P2PKHDataLockBuilder
Section titled “Spendable data with P2PKHDataLockBuilder”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 opcodeOP_1toOP_16(the page on building scripts explains why).

