Skip to content

Commit 0a394ce

Browse files
committed
Implement Taproot key-path wallet signing
1 parent 00ab1b7 commit 0a394ce

9 files changed

Lines changed: 1030 additions & 29 deletions

‎go.mod‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ require (
6161

6262
require (
6363
github.com/davecgh/go-spew v1.1.1 // indirect
64+
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
6465
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
6566
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
6667
github.com/pion/datachannel v1.5.10 // indirect

‎pkg/bitcoin/script.go‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
P2WPKHScript
2020
P2SHScript
2121
P2WSHScript
22+
P2TRScript
2223
)
2324

2425
func (st ScriptType) String() string {
@@ -31,6 +32,8 @@ func (st ScriptType) String() string {
3132
return "P2SH"
3233
case P2WSHScript:
3334
return "P2WSH"
35+
case P2TRScript:
36+
return "P2TR"
3437
default:
3538
return "NonStandard"
3639
}
@@ -147,8 +150,21 @@ func PayToScriptHash(scriptHash [20]byte) (Script, error) {
147150
Script()
148151
}
149152

153+
// PayToTaproot constructs a P2TR script for the provided 32-byte x-only
154+
// Taproot output key. The function assumes the provided output key is valid.
155+
func PayToTaproot(outputKey [32]byte) (Script, error) {
156+
return txscript.NewScriptBuilder().
157+
AddOp(txscript.OP_1).
158+
AddData(outputKey[:]).
159+
Script()
160+
}
161+
150162
// GetScriptType gets the ScriptType of the given Script.
151163
func GetScriptType(script Script) ScriptType {
164+
if isPayToTaproot(script) {
165+
return P2TRScript
166+
}
167+
152168
switch txscript.GetScriptClass(script) {
153169
case txscript.PubKeyHashTy:
154170
return P2PKHScript
@@ -163,6 +179,12 @@ func GetScriptType(script Script) ScriptType {
163179
}
164180
}
165181

182+
func isPayToTaproot(script Script) bool {
183+
return len(script) == 34 &&
184+
script[0] == txscript.OP_1 &&
185+
script[1] == txscript.OP_DATA_32
186+
}
187+
166188
// ExtractPublicKeyHash extracts the public key hash from a P2WPKH or P2PKH
167189
// script.
168190
func ExtractPublicKeyHash(script Script) ([20]byte, error) {
@@ -189,3 +211,15 @@ func ExtractPublicKeyHash(script Script) ([20]byte, error) {
189211

190212
return publicKeyHash, nil
191213
}
214+
215+
// ExtractTaprootKey extracts the x-only output key from a P2TR script.
216+
func ExtractTaprootKey(script Script) ([32]byte, error) {
217+
if GetScriptType(script) != P2TRScript {
218+
return [32]byte{}, fmt.Errorf("not a P2TR script")
219+
}
220+
221+
var outputKey [32]byte
222+
copy(outputKey[:], script[2:])
223+
224+
return outputKey, nil
225+
}

‎pkg/bitcoin/script_test.go‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,32 @@ func TestPayToScriptHash(t *testing.T) {
334334
testutils.AssertBytesEqual(t, expectedResult, result[:])
335335
}
336336

337+
func TestPayToTaproot(t *testing.T) {
338+
outputKeyBytes, err := hex.DecodeString(
339+
"1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
340+
)
341+
if err != nil {
342+
t.Fatal(err)
343+
}
344+
345+
var outputKey [32]byte
346+
copy(outputKey[:], outputKeyBytes)
347+
348+
result, err := PayToTaproot(outputKey)
349+
if err != nil {
350+
t.Fatal(err)
351+
}
352+
353+
expectedResult, err := hex.DecodeString(
354+
"51201b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
355+
)
356+
if err != nil {
357+
t.Fatal(err)
358+
}
359+
360+
testutils.AssertBytesEqual(t, expectedResult, result[:])
361+
}
362+
337363
func TestGetScriptType(t *testing.T) {
338364
fromHex := func(hexString string) []byte {
339365
bytes, err := hex.DecodeString(hexString)
@@ -363,6 +389,10 @@ func TestGetScriptType(t *testing.T) {
363389
script: fromHex("002086a303cdd2e2eab1d1679f1a813835dc5a1b65321077cdccaf08f98cbf04ca96"),
364390
expectedType: P2WSHScript,
365391
},
392+
"p2tr script": {
393+
script: fromHex("51201b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"),
394+
expectedType: P2TRScript,
395+
},
366396
"non-standard script": {
367397
script: fromHex(
368398
"14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d0003" +
@@ -387,6 +417,59 @@ func TestGetScriptType(t *testing.T) {
387417
}
388418
}
389419

420+
func TestExtractTaprootKey(t *testing.T) {
421+
fromHex := func(hexString string) []byte {
422+
bytes, err := hex.DecodeString(hexString)
423+
if err != nil {
424+
t.Fatal(err)
425+
}
426+
return bytes
427+
}
428+
429+
var outputKey [32]byte
430+
copy(
431+
outputKey[:],
432+
fromHex("1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"),
433+
)
434+
435+
var tests = map[string]struct {
436+
script Script
437+
expectedOutputKey [32]byte
438+
expectedErr error
439+
}{
440+
"P2TR script": {
441+
script: fromHex("51201b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"),
442+
expectedOutputKey: outputKey,
443+
},
444+
"other script": {
445+
script: fromHex("00148db50eb52063ea9d98b3eac91489a90f738986f6"),
446+
expectedErr: fmt.Errorf("not a P2TR script"),
447+
},
448+
}
449+
450+
for testName, test := range tests {
451+
t.Run(testName, func(t *testing.T) {
452+
actualOutputKey, err := ExtractTaprootKey(test.script)
453+
454+
if !reflect.DeepEqual(test.expectedErr, err) {
455+
t.Errorf(
456+
"unexpected error\nexpected: %+v\nactual: %+v\n",
457+
test.expectedErr,
458+
err,
459+
)
460+
}
461+
462+
if test.expectedOutputKey != actualOutputKey {
463+
t.Errorf(
464+
"unexpected taproot output key\nexpected: 0x%x\nactual: 0x%x\n",
465+
test.expectedOutputKey,
466+
actualOutputKey,
467+
)
468+
}
469+
})
470+
}
471+
}
472+
390473
func TestExtractPublicKeyHash(t *testing.T) {
391474
fromHex := func(hexString string) []byte {
392475
bytes, err := hex.DecodeString(hexString)

0 commit comments

Comments
 (0)