Skip to content

Data outputs

Bitcoin SV Transactions can carry data in their Outputs. The guide’s Data in script page compares the ways of doing it. DartSV has builders for the two common ones.

The examples use the keys and the fundingTransaction helper described in About the examples. This page’s examples share the following setup:

final myAddress = aliceKey.toAddress(networkType: NetworkType.TEST);
final fundingTx = fundingTransaction(P2PKHLockBuilder.fromAddress(myAddress).getScriptPubkey(), BigInt.from(100000));
final signer = DefaultTransactionSigner(sighashAll, aliceKey);

UnspendableDataLockBuilder writes OP_FALSE OP_RETURN <data>. The Output can never be spent, so we give it zero satoshis.

final dataLocker = UnspendableDataLockBuilder(utf8.encode('hello from dartsv'));
final tx = TransactionBuilder()
.spendFromTxnWithSigner(signer, fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER,
P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToLockBuilder(dataLocker, BigInt.zero) // zero satoshis, since this Output can never be spent
.sendChangeToPKH(myAddress)
.withOption(TransactionOption.DISABLE_DUST_OUTPUTS) // a 0-satoshi output is "dust" to the checks
.build(true);
tx.outputs[1].script.toString(type: 'asm'); // 0 OP_RETURN 68656c6c6f2066726f6d20646172747376

Do note that a zero-satoshi Output is below the builder’s dust limit, so build(true) throws a TransactionAmountException unless we add TransactionOption.DISABLE_DUST_OUTPUTS.

UnspendableDataLockBuilder takes a single push. For protocols with several fields, we build the script with ScriptBuilder and pass it through DefaultLockBuilder, which wraps any script as a Locking Script builder:

// For protocols that expect several fields, we build the script ourselves.
final script = ScriptBuilder()
.opFalse()
.opCode(OpCodes.OP_RETURN)
.addData(utf8.encode('19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut')) // e.g. a protocol prefix
.addData(utf8.encode('Hello'))
.addData(utf8.encode('text/plain'))
.build();
final tx = TransactionBuilder()
.spendFromTxnWithSigner(signer, fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER,
P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToLockBuilder(DefaultLockBuilder.fromScript(script), BigInt.zero)
.sendChangeToPKH(myAddress)
.withOption(TransactionOption.DISABLE_DUST_OUTPUTS)
.build(true);

Pushes of any size work, since the script builder picks OP_PUSHDATA1, 2 or 4 as needed.

To read the data from an Output we received:

// script: an Output's Locking Script
final reader = UnspendableDataLockBuilder.fromScript(script);
utf8.decode(reader.dataBuffer!); // hello from dartsv

For several pushes, we walk the script’s chunks. See Building scripts.

P2PKHDataLockBuilder puts <data> OP_DROP in front of an ordinary P2PKH script. The Output holds coins, and spending it signs over the data.

// <data> OP_DROP followed by an ordinary P2PKH script, so the data rides along on a spendable Output.
final dataLocker = P2PKHDataLockBuilder.fromAddress(myAddress, utf8.encode('invoice 1042'));
final tx = TransactionBuilder()
.spendFromTxnWithSigner(signer, fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER,
P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToLockBuilder(dataLocker, BigInt.from(1000))
.sendChangeToPKH(myAddress)
.build(true);

It is spent like any other P2PKH Output:

// It unlocks like any P2PKH Output, with a Signature and Public Key.
final spend = TransactionBuilder()
.spendFromTxnWithSigner(signer, tx, 1, TransactionInput.MAX_SEQ_NUMBER,
P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToPKH(myAddress, BigInt.from(900))
.build(false);

And it is read back with fromScript:

final reader = P2PKHDataLockBuilder.fromScript(tx.outputs[1].script);
utf8.decode(reader.dataBuffer!); // invoice 1042