Skip to content

Commit 605e72a

Browse files
committed
sweepbatcher: reject malformed MuSig2 cosign data
The cooperative batch sweep path receives a server nonce and partial signature before constructing a keyspend witness. Validate both byte slice lengths before registering the nonce or combining signatures, so malformed server responses fail explicitly instead of being zero-padded into fixed-size MuSig2 buffers. Update batcher test helpers to return size-correct placeholder signing data under the stricter validation.
1 parent db9bd06 commit 605e72a

2 files changed

Lines changed: 88 additions & 6 deletions

File tree

sweepbatcher/sweep_batch.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,6 +1882,12 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
18821882
return nil, err
18831883
}
18841884

1885+
if err := validateServerMuSig2SigningData(
1886+
serverNonce, serverSig,
1887+
); err != nil {
1888+
return nil, err
1889+
}
1890+
18851891
var serverPublicNonce [musig2.PubNonceSize]byte
18861892
copy(serverPublicNonce[:], serverNonce)
18871893

@@ -1934,6 +1940,26 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
19341940
return finalSig, nil
19351941
}
19361942

1943+
// validateServerMuSig2SigningData rejects malformed MuSig2 cosigning data
1944+
// received from the server by checking that the nonce and partial signature
1945+
// have the expected lengths before they are passed to the signer.
1946+
func validateServerMuSig2SigningData(serverNonce,
1947+
serverSig []byte) error {
1948+
1949+
if len(serverNonce) != musig2.PubNonceSize {
1950+
return fmt.Errorf("invalid server nonce length: got %d, "+
1951+
"want %d", len(serverNonce), musig2.PubNonceSize)
1952+
}
1953+
1954+
if len(serverSig) != input.MuSig2PartialSigSize {
1955+
return fmt.Errorf("invalid server partial signature "+
1956+
"length: got %d, want %d", len(serverSig),
1957+
input.MuSig2PartialSigSize)
1958+
}
1959+
1960+
return nil
1961+
}
1962+
19371963
// updateRbfRate updates the fee rate we should use for the new batch
19381964
// transaction. This fee rate does not guarantee RBF success, but the continuous
19391965
// increase leads to an eventual successful RBF replacement.

sweepbatcher/sweep_batcher_test.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/btcsuite/btcd/blockchain"
1717
"github.com/btcsuite/btcd/btcec/v2"
18+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
1819
"github.com/btcsuite/btcd/btcutil"
1920
"github.com/btcsuite/btcd/chaincfg"
2021
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -87,7 +88,62 @@ func testMuSig2SignSweep(ctx context.Context,
8788
prevoutMap map[wire.OutPoint]*wire.TxOut) (
8889
[]byte, []byte, error) {
8990

90-
return nil, nil, nil
91+
return testMuSig2SigningData()
92+
}
93+
94+
// testMuSig2SigningData returns size-correct placeholder data. These tests
95+
// only exercise control flow around the signing response, not cryptographic
96+
// validity.
97+
func testMuSig2SigningData() ([]byte, []byte, error) {
98+
return make([]byte, musig2.PubNonceSize),
99+
make([]byte, input.MuSig2PartialSigSize), nil
100+
}
101+
102+
// TestValidateServerMuSig2SigningData ensures that MuSig2 cosigning data from
103+
// the server is accepted when well-formed and rejected when the nonce or
104+
// partial signature has an unexpected length.
105+
func TestValidateServerMuSig2SigningData(t *testing.T) {
106+
validNonce, validSig, err := testMuSig2SigningData()
107+
require.NoError(t, err)
108+
109+
testCases := []struct {
110+
name string
111+
serverNonce []byte
112+
serverSig []byte
113+
errContains string
114+
}{
115+
{
116+
name: "valid signing data",
117+
serverNonce: validNonce,
118+
serverSig: validSig,
119+
},
120+
{
121+
name: "invalid nonce length",
122+
serverNonce: validNonce[:musig2.PubNonceSize-1],
123+
serverSig: validSig,
124+
errContains: "invalid server nonce length",
125+
},
126+
{
127+
name: "invalid partial signature length",
128+
serverNonce: validNonce,
129+
serverSig: validSig[:input.MuSig2PartialSigSize-1],
130+
errContains: "invalid server partial signature length",
131+
},
132+
}
133+
134+
for _, tc := range testCases {
135+
t.Run(tc.name, func(t *testing.T) {
136+
err := validateServerMuSig2SigningData(
137+
tc.serverNonce, tc.serverSig,
138+
)
139+
if tc.errContains == "" {
140+
require.NoError(t, err)
141+
return
142+
}
143+
144+
require.ErrorContains(t, err, tc.errContains)
145+
})
146+
}
91147
}
92148

93149
var customSignature = func() []byte {
@@ -5024,7 +5080,7 @@ func testWithMixedBatch(t *testing.T, store testStore,
50245080
[]byte, []byte, error) {
50255081

50265082
if swapHash == swapHashes[2] {
5027-
return nil, nil, nil
5083+
return testMuSig2SigningData()
50285084
} else {
50295085
return nil, nil, fmt.Errorf("test error")
50305086
}
@@ -5377,14 +5433,14 @@ func testWithMixedBatchLarge(t *testing.T, store testStore,
53775433
} else {
53785434
swapHash2Used = true
53795435

5380-
return nil, nil, nil
5436+
return testMuSig2SigningData()
53815437
}
53825438

53835439
case swapHash == preimages[5].Hash():
5384-
return nil, nil, nil
5440+
return testMuSig2SigningData()
53855441

53865442
case swapHash == preimages[8].Hash():
5387-
return nil, nil, nil
5443+
return testMuSig2SigningData()
53885444

53895445
default:
53905446
return nil, nil, fmt.Errorf("test error")
@@ -5431,7 +5487,7 @@ func testWithMixedBatchCoopOnly(t *testing.T, store testStore,
54315487
prevoutMap map[wire.OutPoint]*wire.TxOut) (
54325488
[]byte, []byte, error) {
54335489

5434-
return nil, nil, nil
5490+
return testMuSig2SigningData()
54355491
}
54365492

54375493
// All the sweeps are cooperative.

0 commit comments

Comments
 (0)