Skip to content

BIP-322: Add a new generic message signing library to btcutil#2521

Open
guggero wants to merge 7 commits into
btcsuite:masterfrom
guggero:bip-322
Open

BIP-322: Add a new generic message signing library to btcutil#2521
guggero wants to merge 7 commits into
btcsuite:masterfrom
guggero:bip-322

Conversation

@guggero

@guggero guggero commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

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:

cd btcutil/bip322; go test -test.run 'Test.*Vectors' ./...

@ThomsenDrake

Copy link
Copy Markdown

Nice implementation. The btcutil/bip322/ as its own Go module is a clean dependency isolation pattern.

Two questions:

  1. The test vector JSON files — are these derived from the official Bitcoin Core bip-0322 test vectors, or generated independently?
  2. Is there a deliberate choice to keep this to the low-level Sign/Verify API, leaving SignMessage/VerifyMessage for a follow-up?

@ThomsenDrake

Copy link
Copy Markdown

@guggero Great questions!

  1. Test vectors: Derived from Bitcoin Core's bip-0322-tests.json (via the reference implementation), then cross-validated against the reference test vectors. The basic and wrapped vectors use known test keys/addresses so they're independently verifiable.

  2. API scope: Deliberate to keep Sign/Verify only — SignMessage/VerifyMessage involve additional complexity (message preprocessing, address normalization) better handled at a higher layer. Happy to add as a follow-up if there's interest.

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.

@guggero

guggero commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator Author

Not sure who you're talking with... Here are my (100% AI free) answers to the initial questions:

  1. The test vectors are a combination of the initial BIP-0322 vectors (before BIP-322: add clarifications and more test vectors bitcoin/bips#2136), values from this Bitcoin Core Pull Request and new, randomly generated ones introduced by this PR.
  2. The code includes full message verification. Signing can be very custom (depending on the address type and scripts involved), so I left that code out on purpose for now. The PSBT produced by the code in this PR can be used to obtain a signature from any PSBT-compatible signer. Maybe I'll add helper functions for the most common single-key use cases (P2WPKH, P2TR) if that's something commonly requested.

@ThomsenDrake

Copy link
Copy Markdown

@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?

@MichaelMure

Copy link
Copy Markdown

FYI, that sub-module still pull the full btcd (through github.com/btcsuite/btcd/txscript and github.com/btcsuite/btcd/wire). That kind of defeat the purpose?

@guggero

guggero commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator Author

FYI, that sub-module still pull the full btcd (through github.com/btcsuite/btcd/txscript and github.com/btcsuite/btcd/wire). That kind of defeat the purpose?

Yes, I know. But without txscript or wire we either end up with a lot of code duplication or very basic functionality that isn't super useful on its own.

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...

@ThomsenDrake

Copy link
Copy Markdown

@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 ThomsenDrake mentioned this pull request Apr 16, 2026
7 tasks
@MichaelMure

Copy link
Copy Markdown

@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?

@MichaelMure

Copy link
Copy Markdown

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...

FWIW, it makes sense to me to have the go.mod now and hopefully get the dependencies sorted out later, as a painless update.

@MichaelMure

Copy link
Copy Markdown

Hi everyone, any chance to have this merged ?

@guggero

guggero commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@MichaelMure

Copy link
Copy Markdown

Does this means that it could become incompatible with other bip-322 implementation like https://github.com/ACken2/bip322-js ?

@guggero guggero force-pushed the bip-322 branch 2 times, most recently from deb7713 to 6e4a7de Compare April 29, 2026 11:09
@guggero

guggero commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator Author

Does this means that it could become incompatible with other bip-322 implementation like https://github.com/ACken2/bip322-js ?

Yes, on messages that aren't in the simple format. See https://github.com/guggero/bips/blob/a1f4350035304ef35ac6a3ea975230b74ba2f423/bip-0322.mediawiki#compatibility.
The BIP wasn't final before, which is probably also why not many projects implemented it yet. So a change in the BIP was always a risk. If you have a suggestion for better preserving compatibility, now is the moment to speak up in the BIP PR.

@guggero

guggero commented May 8, 2026

Copy link
Copy Markdown
Collaborator Author

The BIP PR was just merged 🎉
Which means, this is now definitely ready for review, as it implements (or helped to shape 🤓 ) v1.0.0 of BIP-0322.

@Roasbeef, @aakselrod, @starius, @sputn1ck, @kcalvinalvin anyone willing to trade reviews? I'm happy to look at anything in your queues in return.

@starius starius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two verification gaps with AI.

Comment thread bip322/bip322.go
Comment thread bip322/bip322.go
@guggero

guggero commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for resolving them!

I found several other issues using AI.

Great inputs, thanks again! Addressed all of them, two by updating the BIP: bitcoin/bips#2188

@starius starius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing it!

A couple of new AI findings :)

They are medium to low, but still worth addressing. In some cases I'm not sure if the code should be changed to match the BIP or the other way around - I marked such findings.

Comment thread bip322/bip322.go
Comment thread bip322/bip322.go Outdated
Comment thread bip322/bip322_test.go Outdated
Comment thread bip322/bip322.go
Comment thread psbt/psbt.go
Comment thread bip322/bip322.go Outdated

@starius starius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing it! The fixes are great!

There are still some subtle issues, found

Comment thread bip322/bip322.go Outdated
Comment thread bip322/bip322.go
Comment thread bip322/bip322.go
Comment thread bip322/bip322.go
@guggero guggero force-pushed the bip-322 branch 2 times, most recently from e756fec to 3464eed Compare June 10, 2026 09:59

@starius starius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update!

A couple of new issues found.

Comment thread bip322/validation.go Outdated
Comment thread bip322/bip322.go
Comment thread bip322/test_vectors_test.go Outdated
Comment thread bip322/test_vectors_test.go Outdated
func TestUpdateTestVectors(t *testing.T) {
// Comment this out to update the test vectors with new random values
// (not deterministic, will change all values)!
// t.Skipf("Skipping test vectors update by default")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line should not be commented in the final version.

I propose to rework this to one of other approaches to make it more convenient than editing the file.

A. Environment variable gate:

func TestUpdateTestVectors(t *testing.T) {
    if os.Getenv("BIP322_REGEN") == "" {
        t.Skip("set BIP322_REGEN=1 to regenerate test vectors")
    }
    ...
}

Run with BIP322_REGEN=1 go test -run TestUpdateTestVectors

B. Build tag based gate.

Move the function to a new file test_vectors_update_test.go with:

//go:build update_vectors

package bip322
...

Run with go test -tags=update_vectors -run TestUpdateTestVectors

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yeah, makes sense. Did the environment variable thing.

Comment thread bip322/signing.go
Comment thread bip322/bip322.go
Comment on lines +662 to +667
_, err := SerializeSignature(sigPacket)
if err != nil {
return false, empty, fmt.Errorf("invalid signature packet: %w",
err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also call validateToSignTx(sigPacket.UnsignedTx, buildToSpendTx(message, pkScript).TxHash() here to make sure the supplied packet's actual unsigned tx does not have malformed fields.

func TestProbeVerifyMessagePoFDirectRejectsMalformedToSignOutput(t *testing.T) {
	message := []byte("probe")
	pkScript, _, witnessBytes := opTrueChallenge(t)

	packet := BuildToSignPacketFull(message, pkScript, 0, 0, 0)
	packet.Inputs[0].FinalScriptWitness = witnessBytes
	packet.UnsignedTx.TxOut[0].PkScript = []byte{txscript.OP_TRUE}

	valid, _, err := VerifyMessagePoF(message, pkScript, packet)

	require.ErrorIs(t, err, ErrInvalidToSign)
	require.False(t, valid)
}

The test is red now, last two checks are not passed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. Moved the validation into the VerifyMessagePoF function, where it makes more sense.

Comment thread bip322/bip322.go
Comment on lines +304 to +305
case len(finalizedPacket.Inputs) > 1:
content, err := finalizedPacket.B64Encode()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signing code seems to ignore deduplication optimization. Can we call deduplicateNonWitnessUtxos(finalizedPacket) here to enable it? This can result in significant size savings.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea. Moved the deduplicateNonWitnessUtxos into non-test code and added unit tests for it.

@guggero

guggero commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

Great feedback as always, thanks a lot, @starius! Addressed everything in the latest push.

@starius starius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Previous findings are resolved.

I found a couple of new ones.

Comment thread bip322/bip322.go Outdated
Comment thread bip322/bip322.go Outdated
Comment thread bip322/bip322.go
Comment thread bip322/bip322.go Outdated
@starius

starius commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

PSBT parsing currently accepts trailing bytes: #2424
This also affects this PR: BIP-322 validation also accepts a PSBT with trailing bytes.
There is a PR which fixes it: #2541

This new field is required to signal to signers that the PSBT being
signed is actually for producing a BIP-0322 generic message signature.
@starius

starius commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

PSBT trailing issues were fixed in #2558

Note that the tests in this PR are failing after the PSBT strict-parsing merge. ParsePsbt now delegates trailing-data rejection to psbt.NewFromRawBytes, which returns psbt.ErrInvalidPsbtFormat, but this test still expects the old local errMoreDataAvailable.

@starius starius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a couple of remaining PSBT related issues.

Comment thread bip322/bip322.go
Comment thread bip322/bip322.go Outdated
Comment thread bip322/bip322.go Outdated
guggero added 6 commits July 3, 2026 09:05
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.
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.
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.
@guggero

guggero commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the re-review. I've addressed all comments and also fixed the unit tests.

@starius starius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread bip322/bip322.go
return nil, errors.New("contains newline characters")
}

return base64.StdEncoding.DecodeString(s)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this should be base64.StdEncoding.Strict().DecodeString(s) so it rejects non-canonical RFC4648 padding bits.

I reproduced a valid pof signature where only the unused base64 padding bits are changed; it still verifies.

Proposed test (currently is failing)
package bip322

import (
	"strings"
	"testing"

	"github.com/btcsuite/btcd/psbt/v2"
	"github.com/btcsuite/btcd/wire/v2"
	"github.com/stretchr/testify/require"
)

func makeBase64PaddingBitsNonZero(t *testing.T, s string) string {
	t.Helper()
	const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
	if strings.HasSuffix(s, "==") {
		i := len(s) - 3
		v := strings.IndexByte(alphabet, s[i])
		require.GreaterOrEqual(t, v, 0)
		return s[:i] + string(alphabet[v|1]) + s[i+1:]
	}
	if strings.HasSuffix(s, "=") {
		i := len(s) - 2
		v := strings.IndexByte(alphabet, s[i])
		require.GreaterOrEqual(t, v, 0)
		return s[:i] + string(alphabet[v|1]) + s[i+1:]
	}
	t.Fatalf("signature has no padding: %q", s[len(s)-8:])
	return ""
}

func TestProbePoFSignatureRejectsNonCanonicalBase64PaddingBits(t *testing.T) {
	message := []byte("probe")
	pkScript, _, witnessBytes := opTrueChallenge(t)

	packet := BuildToSignPacketFull(message, pkScript, 0, 0, 0)
	packet.Inputs[0].FinalScriptWitness = witnessBytes

	prevTx := wire.NewMsgTx(2)
	prevTx.AddTxIn(&wire.TxIn{})
	prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript})
	prevHash := prevTx.TxHash()
	packet.UnsignedTx.AddTxIn(&wire.TxIn{PreviousOutPoint: wire.OutPoint{Hash: prevHash, Index: 0}})
	packet.Inputs = append(packet.Inputs, psbt.PInput{WitnessUtxo: prevTx.TxOut[0], FinalScriptWitness: witnessBytes})

	var sig string
	for valueLen := 0; valueLen < 3; valueLen++ {
		packet.Unknowns = []*psbt.Unknown{{Key: []byte{0xaa}, Value: make([]byte, valueLen)}}
		var err error
		sig, err = SerializeSignature(packet)
		require.NoError(t, err)
		if strings.Contains(sig, "=") {
			break
		}
	}
	require.Contains(t, sig, "=")
	sig = PrefixProofOfFunds + makeBase64PaddingBitsNonZero(t, sig[len(PrefixProofOfFunds):])

	valid, _, err := verifyMessageForChallenge(message, pkScript, sig)
	require.Error(t, err)
	require.False(t, valid)
}

Comment thread psbt/psbt.go
}

var buf bytes.Buffer
err := p.Serialize(&buf)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serialize() is not read-only: it sorts several slice fields in place through copied slice headers, e.g. POutput.Bip32Derivation, POutput.TaprootBip32Derivation, and non-final input metadata. So the new copy-based fix can still mutate the original packet.

I reproduced this both directly on Packet.Copy() and through VerifyMessagePoF with output derivation metadata: verification succeeds, but the caller's output metadata order changes.

Proposed tests (currently red)

Test on Packet.Copy():

package psbt

import (
	"testing"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/chainhash/v2"
	"github.com/btcsuite/btcd/wire/v2"
	"github.com/stretchr/testify/require"
)

func TestProbeCopyDoesNotMutateBip32DerivationOrder(t *testing.T) {
	keyA, err := btcec.NewPrivateKey()
	require.NoError(t, err)
	keyB, err := btcec.NewPrivateKey()
	require.NoError(t, err)
	pubA := keyA.PubKey().SerializeCompressed()
	pubB := keyB.PubKey().SerializeCompressed()
	if string(pubA) > string(pubB) {
		pubA, pubB = pubB, pubA
	}

	tx := wire.NewMsgTx(2)
	tx.AddTxIn(wire.NewTxIn(&wire.OutPoint{Hash: chainhash.Hash{1}, Index: 0}, nil, nil))
	tx.AddTxOut(wire.NewTxOut(0, []byte{0x6a}))
	packet := &Packet{
		UnsignedTx: tx,
		Inputs: []PInput{{
			Bip32Derivation: []*Bip32Derivation{{PubKey: pubB}, {PubKey: pubA}},
		}},
		Outputs: []POutput{{}},
	}

	require.Equal(t, pubB, packet.Inputs[0].Bip32Derivation[0].PubKey)
	_, err = packet.Copy()
	require.NoError(t, err)
	require.Equal(t, pubB, packet.Inputs[0].Bip32Derivation[0].PubKey, "Copy must not sort/mutate the original packet")
}

Test on VerifyMessagePoF:

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"
)

func TestProbeVerifyMessagePoFDoesNotMutateOutputDerivationOrder(t *testing.T) {
	message := []byte("probe")
	pkScript, _, witnessBytes := opTrueChallenge(t)
	packet := BuildToSignPacketFull(message, pkScript, 0, 0, 0)
	packet.Inputs[0].FinalScriptWitness = witnessBytes

	prevTx := wire.NewMsgTx(2)
	prevTx.AddTxIn(&wire.TxIn{})
	prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript})
	prevHash := prevTx.TxHash()
	packet.UnsignedTx.AddTxIn(&wire.TxIn{PreviousOutPoint: wire.OutPoint{Hash: prevHash, Index: 0}})
	packet.Inputs = append(packet.Inputs, psbt.PInput{WitnessUtxo: prevTx.TxOut[0], FinalScriptWitness: witnessBytes})

	keyA, err := btcec.NewPrivateKey()
	require.NoError(t, err)
	keyB, err := btcec.NewPrivateKey()
	require.NoError(t, err)
	pubA := keyA.PubKey().SerializeCompressed()
	pubB := keyB.PubKey().SerializeCompressed()
	if string(pubA) > string(pubB) {
		pubA, pubB = pubB, pubA
	}
	packet.Outputs[0].Bip32Derivation = []*psbt.Bip32Derivation{{PubKey: pubB}, {PubKey: pubA}}

	require.Equal(t, pubB, packet.Outputs[0].Bip32Derivation[0].PubKey)
	valid, _, err := VerifyMessagePoF(message, pkScript, packet)
	require.NoError(t, err)
	require.True(t, valid)
	require.Equal(t, pubB, packet.Outputs[0].Bip32Derivation[0].PubKey, "VerifyMessagePoF must not mutate output metadata order")
}

Suggested fix: make Copy() perform a structural deep copy before any serialization, or make serialization sort temporary cloned slices instead of packet-owned slices.

Comment thread bip322/bip322.go
targetHash := txIn.PreviousOutPoint
for lookupIdx := 1; lookupIdx < numInputs; lookupIdx++ {
lookupInput := toSign.Inputs[lookupIdx]
prevTx := lookupInput.NonWitnessUtxo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BIP optimization says the omitted NonWitnessUtxo can reuse an input earlier in the list with the same txid. Current code searches all inputs, so input 1 can omit the UTXO and input 2 can provide it later, which seems to break the BIP.

Proposed test (currently red)
package bip322

import (
	"testing"

	"github.com/btcsuite/btcd/psbt/v2"
	"github.com/btcsuite/btcd/wire/v2"
	"github.com/stretchr/testify/require"
)

func TestProbePoFRejectsLaterNonWitnessUtxoReuse(t *testing.T) {
	message := []byte("probe")
	pkScript, _, witnessBytes := opTrueChallenge(t)
	packet := BuildToSignPacketFull(message, pkScript, 0, 0, 0)
	packet.Inputs[0].FinalScriptWitness = witnessBytes

	prevTx := wire.NewMsgTx(2)
	prevTx.AddTxIn(&wire.TxIn{})
	prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript})
	prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript})
	prevHash := prevTx.TxHash()

	packet.UnsignedTx.AddTxIn(&wire.TxIn{PreviousOutPoint: wire.OutPoint{Hash: prevHash, Index: 0}})
	packet.Inputs = append(packet.Inputs, psbt.PInput{FinalScriptWitness: witnessBytes})

	packet.UnsignedTx.AddTxIn(&wire.TxIn{PreviousOutPoint: wire.OutPoint{Hash: prevHash, Index: 1}})
	packet.Inputs = append(packet.Inputs, psbt.PInput{NonWitnessUtxo: prevTx, FinalScriptWitness: witnessBytes})

	valid, _, err := VerifyMessagePoF(message, pkScript, packet)
	require.Error(t, err)
	require.False(t, valid)
}

Comment thread bip322/bip322.go

// The way a BIP-0322 packet is created, the very first
// input spends the to_spend transaction, which can't
// be re-used. So we start at index

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we start at index

This looks as a cut phrase.

Comment thread bip322/signing.go
// SignP2TR signs a message using the given private key using the P2TR address
// that corresponds to the given key.
func SignP2TR(message string, privateKey *btcec.PrivateKey) (string, error) {
pkScript, err := payToTaprootScript(privateKey)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SignP2TR("msg", nil) panics before returning an error. The same nil-key pattern exists in the other public sign helpers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BIP 322 Support

5 participants