Skip to content

Sighash types and signing

A Signature commits to a digest of the Transaction. The Sighash type decides which Inputs and Outputs go into that digest. The guide’s Signature schemes page explains the options.

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 lockingScript = P2PKHLockBuilder.fromAddress(myAddress).getScriptPubkey();
final fundingTx = fundingTransaction(lockingScript, BigInt.from(100000));
final otherFunding = fundingTransaction(P2PKHLockBuilder.fromAddress(bobKey.toAddress(networkType: NetworkType.TEST)).getScriptPubkey(), BigInt.from(5000));
// We combine a base type with modifiers using a bitwise OR. On BSV, FORKID is required.
final all = SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_FORKID.value; // 0x41
final none = SighashType.SIGHASH_NONE.value | SighashType.SIGHASH_FORKID.value; // 0x42
final single = SighashType.SIGHASH_SINGLE.value | SighashType.SIGHASH_FORKID.value; // 0x43
final allAnyoneCanPay = all | SighashType.SIGHASH_ANYONECANPAY.value; // 0xC1

SIGHASH_FORKID selects the digest algorithm that Bitcoin SV uses (BIP143-style, committing to the amount being spent). Nodes reject Signatures without it.

SIGHASH_ANYONECANPAY commits to our own Input only, so that others can add Inputs after we have signed, for example to crowdfund a payment. With plain SIGHASH_ALL, adding an Input invalidates the existing Signatures.

// Alice signs her Input over all the Outputs, but only over her own Input.
final aliceSigner = DefaultTransactionSigner(
SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_ANYONECANPAY.value | SighashType.SIGHASH_FORKID.value,
aliceKey);
final tx = TransactionBuilder()
.spendFromTxnWithSigner(aliceSigner, fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER,
P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToPKH(Address('n3aZKucfWmXeXhX13MREQQnqNfbrWiYKtg'), BigInt.from(104000))
.build(false);
// Bob adds and signs an Input of his own, and Alice's Signature stays valid.
tx.addInput(TransactionInput(otherFunding.id, 0, TransactionInput.MAX_SEQ_NUMBER,
scriptBuilder: P2PKHUnlockBuilder(bobKey.publicKey)));
DefaultTransactionSigner(sighashAll, bobKey).sign(tx, otherFunding.outputs[0], 1);

TransactionSigner is an abstract class with two methods: sign, which signs one Input, and signPreimage. DefaultTransactionSigner implements it with a Private Key held in memory. You would implement it yourself when the key lives elsewhere, e.g. in a hardware wallet, a remote signing service or a platform keystore. The signer computes the digest with Sighash, gets a Signature for it, and adds the Signature to the Input’s Unlocking Script builder:

/// A signer that never holds the Private Key. It computes the digest and asks
/// [signDigest] (a hardware wallet, a remote key service, a secure enclave) for the Signature.
class DelegatingSigner extends TransactionSigner {
@override
final int sigHashType;
final SVSignature Function(String digestHex) signDigest;
DelegatingSigner(this.sigHashType, this.signDigest);
@override
Transaction sign(Transaction unsignedTxn, TransactionOutput utxo, int inputIndex) {
final hash = Sighash().hash(unsignedTxn, sigHashType, inputIndex, utxo.script, utxo.satoshis);
// Sighash.hash returns the digest byte-reversed, so we sign the digest in its natural order.
final digest = HEX.encode(HEX.decode(hash).reversed.toList());
final signature = signDigest(digest)..nhashtype = sigHashType;
unsignedTxn.inputs[inputIndex].scriptBuilder!.signatures.add(signature);
return unsignedTxn;
}
@override
SVSignature signPreimage(Uint8List preImage) =>
signDigest(HEX.encode(sha256Twice(preImage.toList())))..nhashtype = sigHashType;
}

It plugs into the builder in the same way as the default signer, and produces the same Transaction:

final signer = DelegatingSigner(sighashAll, device);
final tx = TransactionBuilder()
.spendFromTxnWithSigner(signer, fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER,
P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToPKH(myAddress, BigInt.from(99900))
.build(true);

Do note that Sighash.hash returns the digest hex in reversed byte order. We reverse it before signing, as the example does. DefaultTransactionSigner does the same.

The preimage is the serialised data from which the digest is computed. Contracts that inspect their own spending Transaction (the OP_PUSH_TX technique) take it as part of the Unlocking Script.

// The bytes a Signature commits to (BIP143-style, as BSV uses with FORKID).
// OP_PUSH_TX-style contracts take this preimage in their Unlocking Script.
final Uint8List preimage = Sighash().createSighashPreImage(
tx, sighashAll, 0, fundingTx.outputs[0].script, fundingTx.outputs[0].satoshis)!;
final digest = sha256Twice(preimage); // what ECDSA signs
// sigHex: a Signature as it appears in an Unlocking Script (DER + Sighash byte)
final signature = SVSignature.fromTxFormat(sigHex);
signature.nhashtype; // 0x41: SIGHASH_ALL | SIGHASH_FORKID
signature.hasLowS(); // true: the library always produces low-S Signatures
signature.r; // the ECDSA values, as BigInt
signature.s;

DefaultTransactionSigner always computes the FORKID digest. To sign with the Chronicle SIGHASH_CHRONICLE flag (which selects the original digest algorithm), one writes a signer that calls Sighash().hash(…, flags: ScriptFlags.SCRIPT_ENABLE_SIGHASH_FORKID | ScriptFlags.SCRIPT_ENABLE_CHRONICLE).