BIP-322: Add a new generic message signing library to btcutil#2521
BIP-322: Add a new generic message signing library to btcutil#2521guggero wants to merge 7 commits into
Conversation
6b93e8b to
9fad7fd
Compare
4f66f90 to
0ad6dec
Compare
|
Nice implementation. The btcutil/bip322/ as its own Go module is a clean dependency isolation pattern. Two questions:
|
|
@guggero Great questions!
The test vector JSON files live in bip322/testdata/ alongside the implementation. Additional vectors can be submitted to the BIP repo as a follow-up. |
|
Not sure who you're talking with... Here are my (100% AI free) answers to the initial questions:
|
|
@guggero Reviewing the BIP-322 implementation — solid module structure separating bip322.go (core logic) from signing.go (signature operations). A few observations: Module isolation (btcutil/bip322 as its own Go module) — good call isolating this. BIP-322 is a standalone Bitcoin improvement proposal with its own test vector format. Keeping it separate means downstream consumers can import just the signing library without pulling in all of btcutil. Test vectors — the basic-test-vectors.json + generated-test-vectors.json split is the right approach. The reference vectors from Bitcoin Core bip-0322-tests.json as a baseline, then derived vectors for wrapped/compact modes. Good that these are embedded vs. fetched at test time. One question on signing.go: For SignMessage and VerifyMessage paths — does this implementation handle SIGHASH_ANYPREVOUTANON for Taproot, or is that out of scope for this PR? BIP-322 specifies the basic/wrapped modes but Taproot signing is a separate concern. Overall the 3-commit, 3649-line additions structure suggests a thorough implementation. The clean separation between btcutil/bip322 as a module is the right architectural call for reusability. Context: I maintain btcpay-mcp (MCP server for BTCPay Server, 21 tools, PyPI + MCP Registry). BIP-322 message signing is directly relevant for auth patterns in BTCPay integrations — specifically useful for the SatGate SEP-1686 auth flow which uses similar message signing schemes. This PR looks mergeable from my vantage point. What is the timeline for review? |
|
FYI, that sub-module still pull the full |
Yes, I know. But without I guess what we really need first is #1825, but that is such a huge change for any consumer of any of the involved packages that I don't really have high hopes of it ever being merged... |
|
@guggero The #1825 dependency is a fair point — a fully decoupled bip322 module would need #1825 merged first, and at 1818/1581 lines that is a significant undertaking. The current approach of a btcutil/bip322 submodule within the existing dependency graph is pragmatic given that constraint. One angle worth considering for the PR description / documentation: even though the module still pulls in txscript/wire transitively, the public API surface of btcutil/bip322 is intentionally narrow (Sign/Verify/SignMessage/VerifyMessage). Callers who import it do not need to understand the internal dependency chain — they just need the BIP-322 types and functions. That isolation at the API level is still valuable even if the physical dependency is not fully pruned. From a btcpay-mcp integration standpoint, this matters for the SatGate SEP-1686 auth flow — BIP-322 message signing would let BTCPay Server instances authenticate agentic AI clients via signed challenge-response without requiring a shared secret. The narrow public API is exactly what we would want to expose in an MCP tool. Is there a sense for where this sits in the review queue? I am watching this PR closely given the SEP-1686 relevance. |
|
@ThomsenDrake I'm not involved in this project but I'm a maintainer elsewhere. I'd be annoyed getting such obviously low-info LLM message like that. Maybe don't? |
FWIW, it makes sense to me to have the go.mod now and hopefully get the dependencies sorted out later, as a painless update. |
|
Hi everyone, any chance to have this merged ? |
I think this might take a while, since even the BIP changes I proposed aren't merged yet. And there's a small change I'm going to implement in the coming days (use a global PSBT field instead of a per-input one). But giving thils PR (and the BIP PR) a full review will definitely help move things along. |
|
Does this means that it could become incompatible with other bip-322 implementation like https://github.com/ACken2/bip322-js ? |
deb7713 to
6e4a7de
Compare
Yes, on messages that aren't in the simple format. See https://github.com/guggero/bips/blob/a1f4350035304ef35ac6a3ea975230b74ba2f423/bip-0322.mediawiki#compatibility. |
|
The BIP PR was just merged 🎉 @Roasbeef, @aakselrod, @starius, @sputn1ck, @kcalvinalvin anyone willing to trade reviews? I'm happy to look at anything in your queues in return. |
starius
left a comment
There was a problem hiding this comment.
Found two verification gaps with AI.
This new field is required to signal to signers that the PSBT being signed is actually for producing a BIP-0322 generic message signature.
|
PSBT trailing issues were fixed in #2558 Note that the tests in this PR are failing after the PSBT strict-parsing merge. |
starius
left a comment
There was a problem hiding this comment.
Found a couple of remaining PSBT related issues.
|
Thanks for the re-review. I've addressed all comments and also fixed the unit tests. |
starius
left a comment
There was a problem hiding this comment.
Thanks for pushing it!
I found a couple of subtle issues.
Also I found another trailing bytes issue in psbt, fixed it here: #2567
VerifyMessagePoF trusts psbt.Extract, and psbt.Extract reads the encoded witness items but does not require the witnessReader to be exhausted. So we need to get #2567 in before merging this PR.
starius
left a comment
There was a problem hiding this comment.
Found some drift in the comments.
Adds a convenience Copy() method to the Packet struct that creates a deep copy by serializing and deserializing it. Which means the packet must be valid before it can be copied. To avoid a packet being mutated by the serialization, we also make sure that any sorting of member fields only happens on copies of the slices.
This commit adds a new ScriptVerifyRestrictSigHash flag to the script engine that allows restricting signature checking to SIGHASH_ALL or SIGHASH_DEFAULT (for taproot) only. The script is _NOT_ part of the StandardVerifyFlags and needs to be specifically activated by a caller.
|
Thanks for the thorough reviews, I've addressed all comments. |
starius
left a comment
There was a problem hiding this comment.
Found a couple of bugs and nits.
|
Addressed your latest comments! |
starius
left a comment
There was a problem hiding this comment.
Found two issues:
- PoF verification ignores
PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE SerializeSignaturecan emit a PoF signature that this verifier rejects
Also this PR now depends on #2567 to fix the remaining trailing tail parsing issue.
| empty := TimeConstraints{} | ||
| if !utf8.Valid(message) { | ||
| return false, empty, errInvalidMessage | ||
| } |
There was a problem hiding this comment.
Should we require this here:
sigPacket.GenericSignedMessage != nil && *sigPacket.GenericSignedMessage == string(message)?
The BIP says the PSBT creator sets PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE to the full UTF-8 message, and verification says full transaction/PSBT fields should be confirmed. Current verification accepts both missing and mismatched global message fields as long as the reconstructed to_sign matches the verifier-supplied message.
regression test
func TestVerifyMessagePoFRejectsMissingOrMismatchedGenericSignedMessage(t *testing.T) {
message := []byte("probe")
pkScript, _, witnessBytes := opTrueChallenge(t)
makeSig := func(t *testing.T, field *string) string {
packet := BuildToSignPacketFull(message, pkScript, 0, 0, 0)
packet.Inputs[0].FinalScriptWitness = witnessBytes
packet.GenericSignedMessage = field
prevTx := wire.NewMsgTx(2)
prevTx.AddTxIn(&wire.TxIn{})
prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript})
packet.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: prevTx.TxHash(),
Index: 0,
},
})
packet.Inputs = append(packet.Inputs, psbt.PInput{
WitnessUtxo: prevTx.TxOut[0],
FinalScriptWitness: witnessBytes,
})
sig, err := SerializeSignature(packet)
require.NoError(t, err)
return sig
}
different := "different message"
tests := []struct {
name string
field *string
}{
{name: "missing", field: nil},
{name: "mismatched", field: &different},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sig := makeSig(t, test.field)
valid, _, err := verifyMessageForChallenge(
message, pkScript, sig,
)
require.Error(t, err)
require.False(t, valid)
})
}
}Current failure starts at the first case:
Error: An error is expected but got nil.
Test: TestVerifyMessagePoFRejectsMissingOrMismatchedGenericSignedMessage/missing
Note that in the second sub-test the signature has "different message" in its GenericSignedMessage field (part of the encoded signature string), but it successfully verifies against message="probe".
There was a problem hiding this comment.
Good point. BIP-174 doesn't explicitly mention if global fields should be kept or be stripped when finalizing a packet. So I guess even though the field is intended for the signing step, it's allowed to be kept or set on the signature packet.
But I agree that it should at least match the message.
It does put us in the weird situation where the message can be part of the "signature" data itself. So maybe the BIP should've mentioned that the field should be removed after signing... But I don't feel like touching the BIP again.
| case len(finalizedPacket.Inputs) > 1: | ||
| // Collapse duplicated NonWitnessUtxo fields into a single one | ||
| // to save space (special case allowed by BIP-322). | ||
| deduplicateNonWitnessUtxos(finalizedPacket) |
There was a problem hiding this comment.
I think the PoF serializer should reject or normalize legacy WitnessUtxo fields before emitting a signature.
This was originally visible as a round-trip inconsistency: a caller can give SerializeSignature a finalized PoF packet where repeated legacy inputs from the same previous transaction carry both NonWitnessUtxo and WitnessUtxo; SerializeSignature accepts it, deduplicates away the later duplicate NonWitnessUtxo, preserves the inappropriate WitnessUtxo, emits pof..., and the verifier then rejects that emitted signature with only witness utxo present for non-witness script type.
BIP174 says for PSBT_IN_NON_WITNESS_UTXO:
This should be present for inputs that spend non-segwit outputs and can be present for inputs that spend segwit outputs.
It says for PSBT_IN_WITNESS_UTXO:
This should only be present for inputs which spend segwit outputs, including P2SH embedded ones.
BIP322 says:
The Non-Witness or Witness UTXO fields (as appropriate for the type) of each additional input must be set to the corresponding UTXO.
It also allows the Non-Witness UTXO optimization only when the omitted Non-Witness UTXO can be reused from an earlier input spending the same transaction.
So for a legacy PoF input, SerializeSignature should not emit a signature that leaves an inappropriate WitnessUtxo as the effective UTXO field. Either fail before encoding such a packet, or normalize by dropping the inappropriate WitnessUtxo while preserving the earlier-NonWitnessUtxo reuse semantics. The important invariant is: if SerializeSignature emits a PoF signature, this implementation should be able to verify that emitted signature.
Regression test
func TestSerializeSignaturePoFLegacyWitnessUtxoRoundTrip(t *testing.T) {
message := []byte("probe")
pkScript, _, witnessBytes := opTrueChallenge(t)
packet := BuildToSignPacketFull(message, pkScript, 0, 0, 0)
packet.Inputs[0].FinalScriptWitness = witnessBytes
key, err := btcec.NewPrivateKey()
require.NoError(t, err)
legacyPkScript, err := payToPubKeyHashScript(key)
require.NoError(t, err)
prevTx := wire.NewMsgTx(2)
prevTx.AddTxIn(&wire.TxIn{})
prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: legacyPkScript})
prevTx.AddTxOut(&wire.TxOut{Value: 2, PkScript: legacyPkScript})
prevHash := prevTx.TxHash()
for outIdx := uint32(0); outIdx < 2; outIdx++ {
packet.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: prevHash,
Index: outIdx,
},
})
packet.Inputs = append(packet.Inputs, psbt.PInput{
NonWitnessUtxo: prevTx,
WitnessUtxo: prevTx.TxOut[outIdx],
})
}
for inputIdx := 1; inputIdx <= 2; inputIdx++ {
packet.Inputs[inputIdx].FinalScriptSig, err = txscript.SignatureScript(
packet.UnsignedTx, inputIdx, legacyPkScript,
txscript.SigHashAll, key, true,
)
require.NoError(t, err)
}
signature, err := SerializeSignature(packet)
if err != nil {
return
}
valid, _, err := verifyMessageForChallenge(
message, pkScript, signature,
)
require.NoError(t, err)
require.True(t, valid)
}Current failure:
--- FAIL: TestSerializeSignaturePoFLegacyWitnessUtxoRoundTrip (0.02s)
pof_dedup_witness_utxo_probe_test.go:59:
Error: Received unexpected error:
error finding UTXO: only witness utxo present for non-witness script type
There was a problem hiding this comment.
This was a bit more involved, but now also fixed.
starius
left a comment
There was a problem hiding this comment.
I attempted to integrate bip322 into btcwallet to assess the API. Overall it is very good! I found one issue in the signing API though: SignP2* functions have some important logic not available via other public functions; but SignP2* are hard to use downstream. I shared some ideas how it could be improved and an API sketch. It is just a direction to explore, I'm not confident in this exact API shape. What do you think?
| if !utf8.Valid([]byte(message)) { | ||
| return "", errInvalidMessage | ||
| } |
There was a problem hiding this comment.
Should we move UTF-8 checks from SignP2* functions to BuildToSignPacket* functions and make the later returning (*psbt.Packet, error)?
SignP2* have too narrow interface, e.g. many applications do not have access to raw private keys and may only request signing under something. So they can't use SignP2* and have to call BuildToSignPacket* directly. It is better if they inherit UTF-8 checks.
| } | ||
|
|
||
| return SerializeSignature(toSign) | ||
| } |
There was a problem hiding this comment.
I have been thinking about the SignP2* functions and the lower-level signing API. I think the current helpers are useful as convenience wrappers, but the core signing orchestration could be exposed in a more generic way so downstream users do not need to duplicate the "build full packet first, then sign all inputs, then finalize, then serialize" sequence.
The UTF-8 issue is one symptom of the current split: SignP2* validates the message, but downstream callers that use BuildToSignPacketSimple/BuildToSignPacketFull directly can still build a packet with an invalid PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE and only fail later with a lower-level PSBT serialization error. Since the builders create the generic signed message field, I think UTF-8 validation belongs there. That probably means changing BuildToSignPacketFull to return (*psbt.Packet, error) and having BuildToSignPacketSimple propagate that error.
The signing abstraction I think would fit both the raw-key helpers and wallet integrations is:
type PacketSigner interface {
PrepareInput(packet *psbt.Packet, inputIndex int, utxo *wire.TxOut) error
SignInput(packet *psbt.Packet, inputIndex int, utxo *wire.TxOut) error
}
func FinalizeAndSerializePacket(
packet *psbt.Packet,
utxos []*wire.TxOut,
signer PacketSigner,
) (string, error)PrepareInput lets the signer attach spend-specific PSBT metadata such as RedeemScript or WitnessScript. That keeps wrappers thin and avoids special one-off mutations like packet.Inputs[0].RedeemScript = witnessProgram outside the signer. SignInput then signs the already-complete packet. FinalizeAndSerializePacket would run preparation for all inputs first, then sign all inputs, then finalize, then call SerializeSignature.
Downstream users would still own UTXO lookup/selection and key access. For example, btcwallet would still select PoF inputs, fetch the previous outputs, decide WitnessUtxo vs NonWitnessUtxo, and sign using wallet-managed keys, but it would not need to reimplement the generic BIP-322 sequencing/finalization tail.
Possible FinalizeAndSerializePacket implementation
func FinalizeAndSerializePacket(packet *psbt.Packet, utxos []*wire.TxOut,
signer PacketSigner) (string, error) {
if packet == nil || packet.UnsignedTx == nil {
return "", errors.New("nil or incomplete packet")
}
if signer == nil {
return "", errors.New("signer cannot be nil")
}
if len(packet.Inputs) != len(packet.UnsignedTx.TxIn) {
return "", errors.New("input and txin length mismatch")
}
if len(utxos) != len(packet.Inputs) {
return "", errors.New("utxo and input length mismatch")
}
for idx, utxo := range utxos {
if utxo == nil {
return "", fmt.Errorf("input %d has no utxo", idx)
}
if err := signer.PrepareInput(packet, idx, utxo); err != nil {
return "", fmt.Errorf("prepare input %d: %w", idx, err)
}
}
for idx, utxo := range utxos {
if err := signer.SignInput(packet, idx, utxo); err != nil {
return "", fmt.Errorf("sign input %d: %w", idx, err)
}
}
if err := psbt.MaybeFinalizeAll(packet); err != nil {
return "", fmt.Errorf("finalize packet: %w", err)
}
return SerializeSignature(packet)
}With this, the SignP2* functions can stay as user-friendly helpers, but become thin wrappers over the same generic path.
Example raw-key signer and SignP2WPKH wrapper
type rawKeyScriptKind uint8
const (
rawKeyP2TR rawKeyScriptKind = iota
rawKeyWitness
rawKeyLegacy
)
type rawKeySigner struct {
privateKey *btcec.PrivateKey
signingScript []byte
redeemScript []byte
witnessScript []byte
kind rawKeyScriptKind
}
func (s rawKeySigner) PrepareInput(packet *psbt.Packet, idx int,
utxo *wire.TxOut) error {
if len(s.redeemScript) != 0 {
packet.Inputs[idx].RedeemScript = s.redeemScript
}
if len(s.witnessScript) != 0 {
packet.Inputs[idx].WitnessScript = s.witnessScript
}
return nil
}
func (s rawKeySigner) SignInput(packet *psbt.Packet, idx int,
utxo *wire.TxOut) error {
switch s.kind {
case rawKeyP2TR:
return signInputTaprootKeySpend(packet, idx, s.privateKey)
case rawKeyWitness:
return signInputWitness(packet, idx, s.signingScript, s.privateKey)
case rawKeyLegacy:
return signInputLegacy(packet, idx, s.signingScript, s.privateKey)
default:
return fmt.Errorf("unknown raw key script kind %d", s.kind)
}
}
func SignP2WPKH(message string, privateKey *btcec.PrivateKey) (string, error) {
if privateKey == nil {
return "", errPrivateKeyNil
}
pkScript, err := payToWitnessPubKeyHashScript(privateKey)
if err != nil {
return "", fmt.Errorf("creating pkScript: %w", err)
}
packet, err := BuildToSignPacketSimple([]byte(message), pkScript)
if err != nil {
return "", fmt.Errorf("creating to_sign packet: %w", err)
}
return FinalizeAndSerializePacket(
packet,
[]*wire.TxOut{{Value: 0, PkScript: pkScript}},
rawKeySigner{
privateKey: privateKey,
signingScript: pkScript,
kind: rawKeyWitness,
},
)
}Example nested P2WPKH wrapper without one-off packet mutation
func SignNestedP2WPKH(message string, privateKey *btcec.PrivateKey) (string,
error) {
if privateKey == nil {
return "", errPrivateKeyNil
}
pkScript, witnessProgram, err := payToNestedWitnessPubKeyHashScript(
privateKey,
)
if err != nil {
return "", fmt.Errorf("creating pkScript: %w", err)
}
packet, err := BuildToSignPacketFull([]byte(message), pkScript, 0, 0, 0)
if err != nil {
return "", fmt.Errorf("creating to_sign packet: %w", err)
}
return FinalizeAndSerializePacket(
packet,
[]*wire.TxOut{{Value: 0, PkScript: pkScript}},
rawKeySigner{
privateKey: privateKey,
signingScript: witnessProgram,
redeemScript: witnessProgram,
kind: rawKeyWitness,
},
)
}So the proposed layering would be: builders validate message and create the BIP-322 packet shape; callers provide UTXOs and a signer; FinalizeAndSerializePacket owns the BIP-322 signing/finalization order; SignP2* remains as small convenience wrappers for raw-key single-input cases.
There was a problem hiding this comment.
I agree, such an API would be nice to have. And nothing in the current code is preventing such an API from being added. But after 12 rounds of review, I'd like to keep the scope of the PR to the original goal and API. We can always add this in a follow-up PR.
There was a problem hiding this comment.
Also, just out of curiosity, what's the use case in btcwallet? Wouldn't this make more sense in a more end-user type of wallet context, such as lnd?
There was a problem hiding this comment.
I agree, let's keep the current API and leave this for a potential follow-up PR.
I'll check the LND integration as well! My original btcwallet idea was that it is also an end-user application, but smaller than LND, since it does not have the lightning network code.
4e35a65 to
35c3bfd
Compare
starius
left a comment
There was a problem hiding this comment.
LGTM! 🎉
Great work!
There are just a few nits left, nothing blocking.
While reviewing the implementation, I found some concerns in the protocol itself. None of this blocks this PR; these are just thoughts for the future.
- There is no mainnet/testnet/signet/etc. separation. A signature made for testnet could be valid on mainnet. I think it worth adding a network separation to prevent this.
- Some signatures could reveal public keys, scripts, sets of owned coins, etc.
What do you think about running the whole construction inside ZK to preserve all of that privately? For example, in a STARK. Simple and full signatures are pure functions, so it should be straightforward to put them into ZK. For PoF, it depends on the UTXO set, so it is not a pure function. But this could still be solved if combined with some commitment to the UTXO set, e.g. utreexo. Then a zk-PoF could be interpreted as: "I have X BTC on-chain as of block Y, and here is a proof."
| // We also check that the witness UTXOs for any additional inputs are | ||
| // set appropriately for their script type (so we only serialize | ||
| // something that we would also accept ourselves). | ||
| if err := validateUtxoCorrectness(packet); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| // The IsComplete should rather be called IsFinalized, as it checks each | ||
| // input if it has a witness or scriptSig. | ||
| if !packet.IsComplete() { | ||
| return "", fmt.Errorf("packet must be finalized") | ||
| } |
There was a problem hiding this comment.
Nit. Should these two checks be done in different order?
For a P2SH-nested witness input where the user has set WitnessUtxo but not yet finalized (so no FinalScriptSig to unwrap the redeem script), inputWitnessProgram returns "not segwit" and the user sees "witness UTXO for spending a non-segwit script" instead of the honest "packet must be finalized". Swap the two checks.
Regression test
package bip322
import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/psbt/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/stretchr/testify/require"
)
// TestSerializeSignatureCheckOrder demonstrates that SerializeSignature runs
// validateUtxoCorrectness *before* IsComplete(). When a caller feeds it a
// P2SH-nested-segwit input that has WitnessUtxo set but no FinalScriptSig yet
// (a common intermediate state for wallet-produced PSBTs waiting to be
// signed), inputWitnessProgram cannot unwrap the P2SH redeem script and
// reports isSegWit=false. The caller then gets:
//
// "input N presents a witness UTXO for spending a non-segwit script,
// but a non-segwit input requires a non-witness UTXO (BIP-174)"
//
// instead of the honest:
//
// "packet must be finalized"
//
// Swapping the two checks in SerializeSignature (IsComplete first, then
// validateUtxoCorrectness) would surface the real reason.
//
// Not a security bug. UX only.
func TestSerializeSignatureCheckOrder(t *testing.T) {
key, err := btcec.NewPrivateKey()
require.NoError(t, err)
// Build a P2SH-P2WPKH pkScript. This puts the challenge input into
// the P2SH-nested-segwit path, where BuildToSignPacketFull nils
// WitnessUtxo on input 0 (so input 0 alone can't trigger the bug).
pkScript, _, err := payToNestedWitnessPubKeyHashScript(key)
require.NoError(t, err)
packet, err := BuildToSignPacketFull([]byte("probe"), pkScript, 0, 0, 0)
require.NoError(t, err)
// Add a second, P2SH-nested-segwit PoF input that a wallet has
// pre-populated with both UTXO fields but has NOT signed yet. This is
// the exact state a wallet's outgoing PSBT is in before being handed
// off for signing.
prevTx := wire.NewMsgTx(2)
prevTx.AddTxIn(&wire.TxIn{})
prevTx.AddTxOut(&wire.TxOut{Value: 1000, PkScript: pkScript})
packet.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: prevTx.TxHash(),
Index: 0,
},
})
packet.Inputs = append(packet.Inputs, psbt.PInput{
WitnessUtxo: prevTx.TxOut[0],
NonWitnessUtxo: prevTx,
// FinalScriptSig deliberately left nil — packet is unfinalized.
})
// The packet is not finalized. Ask SerializeSignature what it thinks.
_, err = SerializeSignature(packet)
require.Error(t, err)
t.Logf("SerializeSignature on unfinalized P2SH-witness packet → %v", err)
// The honest error would be "packet must be finalized"; today the
// caller instead gets the misleading BIP-174-shaped error.
require.ErrorContains(t, err, "packet must be finalized",
"expected the honest finalized-first error; got the "+
"misleading UTXO-correctness error because "+
"validateUtxoCorrectness runs before IsComplete "+
"in SerializeSignature")
}This commit adds a new Golang submodule that implements BIP-0322 generic message signing. This first commit adds helper methods for producing a PSBT packet that, when signed, can be turned into a BIP-0322 valid "signature" (which, depending on the variant "simple" vs. "full" is either just the serialized witness stack or the full serialized to_sign transaction). Co-Authored-By: mohamedmohey2352@gmail.com
This commit adds verification functions for the generic message signing protocol and also adds test cases for all common script types.
|
Thanks a lot for all the review! 🙏
Yes, that is correct. But the signature first and foremost attests to a message, and a message doesn't necessarily have a direct dependency on the network. And since the signature is always accompanied by an address formatted for a network, so the intended network is indicated by that. But yes, you could take a testnet signature and address and transform them into a valid mainnet signature. So if that is a problem for someone's use case, they can still add the network flag as a prefix to the message itself.
Yes, and I think that's something a wallet developer should take into account and make their users aware of.
I still haven't educated myself properly on ZK tech, but that sounds super useful. The UTXO set dependency is a bit of a challenge to solve, but as you say it could rely on one of the ZK-friendly UTXO commitment proposals. Perhaps you could write a BIP for ZK-BIP-322? I'd be willing to assist with editorial tasks and implementation/testing of reference code. Would be a great excuse for me to learn more about STARKs. |
Change Description
Implements BIP-322.
Closes #2077
This is loosely based on an older PR by @mohamedawnallah, so I kept them as the co-author of the first commit.
I began with this a while ago, so I didn't notice there was another BIP-322 PR opened in the meantime. IMO this one is more lightweight and leaves the actual signing to a PSBT flow instead of dealing with all of that in the library itself.
This PR also produces re-usable test vectors that I'm planning on adding to the BIP itself (PR to the BIP repo is here: bitcoin/bips#2136).
cc @mohamedawnallah, @asheswook.
Test vectors
This PR has code to generate and validate different test vectors.
You can run all tests against the JSON-based test vectors with: