lib/bip322 implements the BIP-322
"generic signed message format" in full format only.
The package builds and validates the virtual transactions described by the spec:
to_spendto_sign- full-format signature payload (
sig) = serializedto_sign
What is implemented:
- message hash construction (
MessageHash) to_spendconstruction (BuildToSpend)to_signPSBT construction (BuildToSign)- low-level raw
to_signconstruction (BuildToSignTx) - full signature encoding/decoding (
Sig,DecodeSig, base64 helpers) - validation with BIP-322 result states (
ValidateAuthPkg) - application-level intent envelope (
Intent) - proof-of-funds additional inputs on
to_sign
What is intentionally not implemented:
- simple signature format (witness stack only)
message bytes
-> SHA256_tagged("BIP0322-signed-message", message)
-> 32-byte message hash
to_spend is deterministic for (message hash, message challenge script).
message
|
v
BIP-340 tagged hash
("BIP0322-signed-message")
|
v
+-------------------+
| to_spend |
|-------------------|
| version: 0 |
| locktime: 0 |
| |
| input 0: |
| prevout: 00..00 |
| :ffff |
| scriptSig: |
| OP_0 |
| PUSH32 |
| <msg_hash> |
| sequence: 0 |
| |
| output 0: |
| value: 0 |
| pkScript: |
| <challenge> |
+-------------------+
to_spend:
version: 0
locktime: 0
vin[0]:
prevout: 0000..0000:0xffffffff
scriptSig: OP_0 <message_hash>
sequence: 0
vout[0]:
value: 0
scriptPubKey: <message_challenge>
to_sign always spends to_spend:0 as input 0 and has a single
OP_RETURN output. Additional inputs are optional and represent
proof-of-funds UTXOs.
+-------------------+
| to_sign |
|-------------------|
| version: 0 or 2 |
| locktime: config |
| |
| input 0: |
| prevout: |
| <to_spend_txid> |
| :0 |
| sequence: config|
| script/witness: |
| signer data |
| |
| input 1..N: |
| proof-of-funds |
| UTXO inputs |
| |
| output 0: |
| value: 0 |
| pkScript: |
| OP_RETURN |
+-------------------+
to_sign:
version: 0 or 2
locktime: configurable
vin[0]:
prevout: <to_spend_txid>:0
sequence: configurable (valid-at-age field on success)
scriptSig/witness: challenge witness data
vin[1..N] (optional):
proof-of-funds inputs
vout[0]:
value: 0
scriptPubKey: OP_RETURN
In full format, signature bytes are exactly:
sig = serialize(to_sign)
sig_base64 = base64(sig)
ValidateAuthPkg returns:
validinconclusiveinvalid
It validates using this sequence:
- Check auth package completeness (
message, challenge script,sig). - Rebuild deterministic
to_spendfrom message/challenge. - Copy and structurally validate full-format
to_sign. - Apply upgradeable version rule (
to_signversion must be0or2). - Build prevout metadata for every input:
- input 0 from rebuilt
to_spend - inputs 1..N from
ProofPrevOutputsmap
- input 0 from rebuilt
- Execute
txscript.NewEnginefor each input using standard verify flags. - Return:
valid+ValidAtTime=to_sign.nLockTimevalid+ValidAtAge=to_sign.vin[0].nSequence- otherwise
invalid/inconclusivewith reason
inconclusive is used when the verifier cannot fully evaluate according to
BIP-322 upgradeable behavior (for example unsupported versions/features or
missing proof prevout data).
For application policy, this package provides Intent:
Payload(the canonical application message)ValidFrom(inclusive lower bound)ValidUntil(inclusive upper bound,0= no expiry)
Intent.SigningMessage deterministically serializes this metadata so
application validity fields are part of the BIP-322 message commitment.
Intent.Validate and Intent.ValidateAtHeight provide local checks for
metadata consistency and chain-height validity.
Use BuildAndSignFullTx when you have direct access to the signing key
and implement the TxSigner interface. This is the simplest path — it
builds both virtual transactions, signs, and returns the signature in one
call.
msg := []byte("Hello World")
// The challenge script is the scriptPubKey the signer must satisfy.
// In practice this is typically a P2TR or P2WPKH script.
challengeScript := myP2TRScript
// Sign the message. The signer fills in witness data for all inputs.
sig, err := bip322.BuildAndSignFullTx(
msg, challengeScript,
mySigner, // implements bip322.TxSigner
bip322.WithToSignVersion(2),
bip322.WithToSignLockTime(800_000),
)
if err != nil {
return err
}
// Encode as base64 for transport.
sigB64, err := sig.EncodeBase64()Use BuildToSign when the signer speaks PSBT (hardware wallets, remote
signing services). Build the unsigned PSBT, hand it off, then finalize.
msg := []byte("Hello World")
challengeScript := myP2TRScript
// Step 1: Build to_spend.
msgHash := bip322.MessageHash(msg)
toSpend, err := bip322.BuildToSpend(msgHash, challengeScript)
if err != nil {
return err
}
// Step 2: Build unsigned to_sign PSBT with witness-UTXO metadata
// already attached for the signer.
packet, err := bip322.BuildToSign(toSpend)
if err != nil {
return err
}
// Step 3: Pass the PSBT to your external signer.
err = externalSigner.SignPSBT(packet)
if err != nil {
return err
}
// Step 4: Finalize and extract the full-format signature.
sig, err := bip322.FinalizeToSignPSBT(packet)
if err != nil {
return err
}
sigB64, err := sig.EncodeBase64()Append additional inputs to prove ownership of on-chain UTXOs alongside the message signature. The signer must produce witnesses for all inputs.
sig, err := bip322.BuildAndSignFullTx(
msg, challengeScript, mySigner,
// Append proof-of-funds UTXOs after input 0.
bip322.WithToSignAdditionalInputs(
bip322.AdditionalInput{
PreviousOutPoint: fundingOutpoint,
Sequence: 0,
// WitnessUtxo is required so the signer can
// compute the correct sighash.
WitnessUtxo: &wire.TxOut{
Value: 1_000_000,
PkScript: fundingPkScript,
},
},
),
)ValidateAuthPkg returns a three-state result: valid, invalid, or
inconclusive (when upgradeable script features prevent full evaluation).
// Decode the base64 signature received over the wire.
parsedSig, err := bip322.DecodeSigBase64(sigB64)
if err != nil {
return err
}
result := bip322.ValidateAuthPkg(&bip322.AuthPkg{
Message: msg,
MessageChallenge: challengeScript,
Sig: parsedSig,
// For proof-of-funds verification, supply the UTXO metadata
// for each additional input. Without this, those inputs are
// marked inconclusive.
ProofPrevOutputs: map[wire.OutPoint]*wire.TxOut{
fundingOutpoint: {
Value: 1_000_000,
PkScript: fundingPkScript,
},
},
})
switch result.State {
case bip322.VerificationStateValid:
// result.ValidAtTime = to_sign nLockTime
// result.ValidAtAge = to_sign vin[0] nSequence
case bip322.VerificationStateInvalid:
// result.Reason describes the failure
case bip322.VerificationStateInconclusive:
// result.Reason describes what couldn't be evaluated
}Intent keeps validity metadata at the application layer while still
committing to it in the BIP-322 digest.
intent := &bip322.Intent{
Payload: msg,
ValidFrom: 840_000,
ValidUntil: 840_144, // 0 = no expiry
}
intentMsg, err := intent.SigningMessage()
if err != nil {
return err
}
sig, err := bip322.BuildAndSignFullTx(
intentMsg, challengeScript, mySigner,
bip322.WithToSignVersion(2),
)
if err != nil {
return err
}
if err := intent.ValidateAtHeight(840_050); err != nil {
return err
}
result := bip322.ValidateAuthPkg(&bip322.AuthPkg{
Message: intentMsg,
MessageChallenge: challengeScript,
Sig: sig,
})