mirror of
https://github.com/aljazceru/ark.git
synced 2026-02-23 12:12:49 +01:00
New boarding protocol (#279)
* [domain] add reverse boarding inputs in Payment struct * [tx-builder] support reverse boarding script * [wallet] add GetTransaction * [api-spec][application] add reverse boarding support in covenantless * [config] add reverse boarding config * [api-spec] add ReverseBoardingAddress RPC * [domain][application] support empty forfeits txs in EndFinalization events * [tx-builder] optional connector output in round tx * [btc-embedded] fix getTx and taproot finalizer * whitelist ReverseBoardingAddress RPC * [test] add reverse boarding integration test * [client] support reverse boarding * [sdk] support reverse boarding * [e2e] add sleep time after faucet * [test] run using bitcoin-core RPC * [tx-builder] fix GetSweepInput * [application][tx-builder] support reverse onboarding in covenant * [cli] support reverse onboarding in covenant CLI * [test] rework integration tests * [sdk] remove onchain wallet, replace by onboarding address * remove old onboarding protocols * [sdk] Fix RegisterPayment * [e2e] add more funds to covenant ASP * [e2e] add sleeping time * several fixes * descriptor boarding * remove boarding delay from info * [sdk] implement descriptor boarding * go mod tidy * fixes and revert error msgs * move descriptor pkg to common * add replace in go.mod * [sdk] fix unit tests * rename DescriptorInput --> BoardingInput * genrest in SDK * remove boarding input from domain * remove all "reverse boarding" * rename "onboarding" ==> "boarding" * remove outdate payment unit test * use tmpfs docker volument for compose testing files * several fixes
This commit is contained in:
48
common/bitcointree/descriptor.go
Normal file
48
common/bitcointree/descriptor.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package bitcointree
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
)
|
||||
|
||||
func ComputeOutputScript(desc descriptor.TaprootDescriptor) ([]byte, error) {
|
||||
leaves := make([]txscript.TapLeaf, 0)
|
||||
for _, leaf := range desc.ScriptTree {
|
||||
scriptHex, err := leaf.Script(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script, err := hex.DecodeString(scriptHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
leaves = append(leaves, txscript.NewBaseTapLeaf(script))
|
||||
}
|
||||
|
||||
taprootTree := txscript.AssembleTaprootScriptTree(leaves...)
|
||||
|
||||
root := taprootTree.RootNode.TapHash()
|
||||
internalKey, err := hex.DecodeString(desc.InternalKey.Hex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
internalKeyParsed, err := schnorr.ParsePubKey(internalKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taprootKey := txscript.ComputeTaprootOutputKey(internalKeyParsed, root[:])
|
||||
|
||||
outputScript, err := taprootOutputScript(taprootKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return outputScript, nil
|
||||
}
|
||||
45
common/descriptor/ark.go
Normal file
45
common/descriptor/ark.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package descriptor
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
|
||||
const BoardingDescriptorTemplate = "tr(%s,{ and(pk(%s), pk(%s)), and(older(%d), pk(%s)) })"
|
||||
|
||||
func ParseBoardingDescriptor(
|
||||
desc TaprootDescriptor,
|
||||
) (user *secp256k1.PublicKey, timeout uint, err error) {
|
||||
for _, leaf := range desc.ScriptTree {
|
||||
if andLeaf, ok := leaf.(*And); ok {
|
||||
if first, ok := andLeaf.First.(*Older); ok {
|
||||
timeout = first.Timeout
|
||||
}
|
||||
|
||||
if second, ok := andLeaf.Second.(*PK); ok {
|
||||
keyBytes, err := hex.DecodeString(second.Key.Hex)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
user, err = schnorr.ParsePubKey(keyBytes)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, 0, errors.New("boarding descriptor is invalid")
|
||||
}
|
||||
|
||||
if timeout == 0 {
|
||||
return nil, 0, errors.New("boarding descriptor is invalid")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
224
common/descriptor/expression.go
Normal file
224
common/descriptor/expression.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package descriptor
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidXOnlyKey = errors.New("invalid x only public key")
|
||||
ErrInvalidPkPolicy = errors.New("invalid public key policy")
|
||||
ErrInvalidOlderPolicy = errors.New("invalid older policy")
|
||||
ErrInvalidAndPolicy = errors.New("invalid and() policy")
|
||||
ErrNotExpectedPolicy = errors.New("not the expected policy")
|
||||
)
|
||||
|
||||
type Expression interface {
|
||||
Parse(policy string) error
|
||||
Script(verify bool) (string, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type XOnlyKey struct {
|
||||
Key
|
||||
}
|
||||
|
||||
func (e *XOnlyKey) Parse(policy string) error {
|
||||
if len(policy) != 64 {
|
||||
fmt.Println(policy)
|
||||
return ErrInvalidXOnlyKey
|
||||
}
|
||||
|
||||
e.Hex = policy
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *XOnlyKey) Script() string {
|
||||
return e.Hex
|
||||
}
|
||||
|
||||
// pk(xonlypubkey)
|
||||
type PK struct {
|
||||
Key XOnlyKey
|
||||
}
|
||||
|
||||
func (e *PK) String() string {
|
||||
return fmt.Sprintf("pk(%s)", e.Key.Hex)
|
||||
}
|
||||
|
||||
func (e *PK) Parse(policy string) error {
|
||||
if !strings.HasPrefix(policy, "pk(") {
|
||||
return ErrNotExpectedPolicy
|
||||
}
|
||||
if len(policy) != 3+64+1 {
|
||||
return ErrInvalidPkPolicy
|
||||
}
|
||||
|
||||
var key XOnlyKey
|
||||
if err := key.Parse(policy[3 : 64+3]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Key = key
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *PK) Script(verify bool) (string, error) {
|
||||
pubkeyBytes, err := hex.DecodeString(e.Key.Hex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
checksig := txscript.OP_CHECKSIG
|
||||
if verify {
|
||||
checksig = txscript.OP_CHECKSIGVERIFY
|
||||
}
|
||||
|
||||
script, err := txscript.NewScriptBuilder().AddData(
|
||||
pubkeyBytes,
|
||||
).AddOp(
|
||||
byte(checksig),
|
||||
).Script()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(script), nil
|
||||
}
|
||||
|
||||
type Older struct {
|
||||
Timeout uint
|
||||
}
|
||||
|
||||
func (e *Older) String() string {
|
||||
return fmt.Sprintf("older(%d)", e.Timeout)
|
||||
}
|
||||
|
||||
func (e *Older) Parse(policy string) error {
|
||||
if !strings.HasPrefix(policy, "older(") {
|
||||
return ErrNotExpectedPolicy
|
||||
}
|
||||
|
||||
index := strings.IndexRune(policy, ')')
|
||||
if index == -1 {
|
||||
return ErrInvalidOlderPolicy
|
||||
}
|
||||
|
||||
number := policy[6:index]
|
||||
if len(number) == 0 {
|
||||
return ErrInvalidOlderPolicy
|
||||
}
|
||||
|
||||
timeout, err := strconv.Atoi(number)
|
||||
if err != nil {
|
||||
return ErrInvalidOlderPolicy
|
||||
}
|
||||
|
||||
e.Timeout = uint(timeout)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Older) Script(bool) (string, error) {
|
||||
sequence, err := common.BIP68Encode(e.Timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
script, err := txscript.NewScriptBuilder().AddData(sequence).AddOps([]byte{
|
||||
txscript.OP_CHECKSEQUENCEVERIFY,
|
||||
txscript.OP_DROP,
|
||||
}).Script()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(script), nil
|
||||
}
|
||||
|
||||
type And struct {
|
||||
First Expression
|
||||
Second Expression
|
||||
}
|
||||
|
||||
func (e *And) String() string {
|
||||
return fmt.Sprintf("and(%s,%s)", e.First.String(), e.Second.String())
|
||||
}
|
||||
|
||||
func (e *And) Parse(policy string) error {
|
||||
if !strings.HasPrefix(policy, "and(") {
|
||||
return ErrNotExpectedPolicy
|
||||
}
|
||||
|
||||
index := strings.LastIndexByte(policy, ')')
|
||||
if index == -1 {
|
||||
return ErrInvalidAndPolicy
|
||||
}
|
||||
|
||||
childrenPolicy := policy[4:index]
|
||||
if len(childrenPolicy) == 0 {
|
||||
return ErrInvalidAndPolicy
|
||||
}
|
||||
|
||||
children := strings.Split(childrenPolicy, ",")
|
||||
if len(children) != 2 {
|
||||
fmt.Println(children)
|
||||
return ErrInvalidAndPolicy
|
||||
}
|
||||
|
||||
first, err := parseExpression(children[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
second, err := parseExpression(children[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.First = first
|
||||
e.Second = second
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *And) Script(verify bool) (string, error) {
|
||||
firstScript, err := e.First.Script(true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
secondScript, err := e.Second.Script(verify)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return firstScript + secondScript, nil
|
||||
}
|
||||
|
||||
func parseExpression(policy string) (Expression, error) {
|
||||
policy = strings.TrimSpace(policy)
|
||||
expressions := make([]Expression, 0)
|
||||
expressions = append(expressions, &PK{})
|
||||
expressions = append(expressions, &Older{})
|
||||
expressions = append(expressions, &And{})
|
||||
|
||||
for _, e := range expressions {
|
||||
if err := e.Parse(policy); err != nil {
|
||||
if err != ErrNotExpectedPolicy {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to parse expression '%s'", policy)
|
||||
}
|
||||
125
common/descriptor/parser.go
Normal file
125
common/descriptor/parser.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package descriptor
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UnspendableKey is the x-only pubkey of the secp256k1 base point G
|
||||
const UnspendableKey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
|
||||
func ParseTaprootDescriptor(desc string) (*TaprootDescriptor, error) {
|
||||
desc = strings.ReplaceAll(desc, " ", "")
|
||||
|
||||
if !strings.HasPrefix(desc, "tr(") || !strings.HasSuffix(desc, ")") {
|
||||
return nil, fmt.Errorf("invalid descriptor format")
|
||||
}
|
||||
|
||||
content := desc[3 : len(desc)-1]
|
||||
parts := strings.SplitN(content, ",", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid descriptor format: missing script tree")
|
||||
}
|
||||
|
||||
internalKey, err := parseKey(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scriptTreeStr := parts[1]
|
||||
if !strings.HasPrefix(scriptTreeStr, "{") || !strings.HasSuffix(scriptTreeStr, "}") {
|
||||
return nil, fmt.Errorf("invalid script tree format")
|
||||
}
|
||||
scriptTreeStr = scriptTreeStr[1 : len(scriptTreeStr)-1]
|
||||
|
||||
scriptTree := []Expression{}
|
||||
if scriptTreeStr != "" {
|
||||
scriptParts, err := splitScriptTree(scriptTreeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, scriptStr := range scriptParts {
|
||||
leaf, err := parseExpression(scriptStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scriptTree = append(scriptTree, leaf)
|
||||
}
|
||||
}
|
||||
|
||||
return &TaprootDescriptor{
|
||||
InternalKey: internalKey,
|
||||
ScriptTree: scriptTree,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CompileDescriptor compiles a TaprootDescriptor struct back into a descriptor string
|
||||
func CompileDescriptor(desc TaprootDescriptor) string {
|
||||
scriptParts := make([]string, len(desc.ScriptTree))
|
||||
for i, leaf := range desc.ScriptTree {
|
||||
scriptParts[i] = leaf.String()
|
||||
}
|
||||
scriptTree := strings.Join(scriptParts, ",")
|
||||
return fmt.Sprintf("tr(%s,{%s})", desc.InternalKey.Hex, scriptTree)
|
||||
}
|
||||
|
||||
func parseKey(keyStr string) (Key, error) {
|
||||
decoded, err := hex.DecodeString(keyStr)
|
||||
if err != nil {
|
||||
return Key{}, fmt.Errorf("invalid key: not a valid hex string: %v", err)
|
||||
}
|
||||
|
||||
switch len(decoded) {
|
||||
case 32:
|
||||
// x-only public key, this is correct for Taproot
|
||||
return Key{Hex: keyStr}, nil
|
||||
case 33:
|
||||
// compressed public key, we need to remove the prefix byte
|
||||
return Key{Hex: keyStr[2:]}, nil
|
||||
default:
|
||||
return Key{}, fmt.Errorf("invalid key length: expected 32 or 33 bytes, got %d", len(decoded))
|
||||
}
|
||||
}
|
||||
func splitScriptTree(scriptTreeStr string) ([]string, error) {
|
||||
var result []string
|
||||
var current strings.Builder
|
||||
depth := 0
|
||||
|
||||
for _, char := range scriptTreeStr {
|
||||
switch char {
|
||||
case '(':
|
||||
depth++
|
||||
current.WriteRune(char)
|
||||
case ')':
|
||||
depth--
|
||||
current.WriteRune(char)
|
||||
if depth == 0 {
|
||||
result = append(result, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
case ',':
|
||||
if depth == 0 {
|
||||
if current.Len() > 0 {
|
||||
result = append(result, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
} else {
|
||||
current.WriteRune(char)
|
||||
}
|
||||
default:
|
||||
current.WriteRune(char)
|
||||
}
|
||||
}
|
||||
|
||||
if current.Len() > 0 {
|
||||
result = append(result, current.String())
|
||||
}
|
||||
|
||||
if depth != 0 {
|
||||
return nil, fmt.Errorf("mismatched parentheses in script tree")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
350
common/descriptor/parser_test.go
Normal file
350
common/descriptor/parser_test.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package descriptor_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseTaprootDescriptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
desc string
|
||||
expected descriptor.TaprootDescriptor
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Basic Taproot",
|
||||
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)})",
|
||||
expected: descriptor.TaprootDescriptor{
|
||||
InternalKey: descriptor.Key{Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"},
|
||||
ScriptTree: []descriptor.Expression{
|
||||
&descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "VTXO",
|
||||
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c),and(pk(59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8),older(144))})",
|
||||
expected: descriptor.TaprootDescriptor{
|
||||
InternalKey: descriptor.Key{Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"},
|
||||
ScriptTree: []descriptor.Expression{
|
||||
&descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
&descriptor.And{
|
||||
First: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8",
|
||||
},
|
||||
},
|
||||
},
|
||||
Second: &descriptor.Older{
|
||||
Timeout: 144,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Boarding",
|
||||
desc: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)), and(older(604672), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)) })",
|
||||
expected: descriptor.TaprootDescriptor{
|
||||
InternalKey: descriptor.Key{Hex: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"},
|
||||
ScriptTree: []descriptor.Expression{
|
||||
&descriptor.And{
|
||||
First: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465",
|
||||
},
|
||||
},
|
||||
},
|
||||
Second: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&descriptor.And{
|
||||
Second: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
},
|
||||
},
|
||||
},
|
||||
First: &descriptor.Older{
|
||||
Timeout: 604672,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid Key",
|
||||
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798G,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)})",
|
||||
expected: descriptor.TaprootDescriptor{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Descriptor Format",
|
||||
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
|
||||
expected: descriptor.TaprootDescriptor{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid Descriptor Format - Missing Script Tree",
|
||||
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
|
||||
expected: descriptor.TaprootDescriptor{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Valid Empty Script Tree",
|
||||
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{})",
|
||||
expected: descriptor.TaprootDescriptor{
|
||||
InternalKey: descriptor.Key{Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"},
|
||||
ScriptTree: []descriptor.Expression{},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := descriptor.ParseTaprootDescriptor(tt.desc)
|
||||
if (err != nil) != tt.wantErr {
|
||||
require.Equal(t, tt.wantErr, err != nil, err)
|
||||
return
|
||||
}
|
||||
require.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileDescriptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
desc descriptor.TaprootDescriptor
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Basic Taproot",
|
||||
desc: descriptor.TaprootDescriptor{
|
||||
InternalKey: descriptor.Key{Hex: descriptor.UnspendableKey},
|
||||
ScriptTree: []descriptor.Expression{
|
||||
&descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)})",
|
||||
},
|
||||
{
|
||||
name: "VTXO",
|
||||
desc: descriptor.TaprootDescriptor{
|
||||
InternalKey: descriptor.Key{Hex: descriptor.UnspendableKey},
|
||||
ScriptTree: []descriptor.Expression{
|
||||
&descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
&descriptor.And{
|
||||
First: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8",
|
||||
},
|
||||
},
|
||||
},
|
||||
Second: &descriptor.Older{
|
||||
Timeout: 1024,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c),and(pk(59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8),older(1024))})",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := descriptor.CompileDescriptor(tt.desc)
|
||||
require.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePk(t *testing.T) {
|
||||
tests := []struct {
|
||||
policy string
|
||||
expectedScript string
|
||||
expected descriptor.PK
|
||||
verify bool
|
||||
}{
|
||||
{
|
||||
policy: "pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)",
|
||||
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cac",
|
||||
verify: false,
|
||||
expected: descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
policy: "pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)",
|
||||
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cad",
|
||||
verify: true,
|
||||
expected: descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var parsed descriptor.PK
|
||||
err := parsed.Parse(test.policy)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, parsed)
|
||||
|
||||
script, err := parsed.Script(test.verify)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedScript, script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOlder(t *testing.T) {
|
||||
tests := []struct {
|
||||
policy string
|
||||
expectedScript string
|
||||
expected descriptor.Older
|
||||
}{
|
||||
{
|
||||
policy: "older(512)",
|
||||
expectedScript: "03010040b275",
|
||||
expected: descriptor.Older{
|
||||
Timeout: uint(512),
|
||||
},
|
||||
},
|
||||
{
|
||||
policy: "older(1024)",
|
||||
expectedScript: "03020040b275",
|
||||
expected: descriptor.Older{
|
||||
Timeout: uint(1024),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var parsed descriptor.Older
|
||||
err := parsed.Parse(test.policy)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, parsed)
|
||||
|
||||
script, err := parsed.Script(false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedScript, script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAnd(t *testing.T) {
|
||||
tests := []struct {
|
||||
policy string
|
||||
expectedScript string
|
||||
expected descriptor.And
|
||||
}{
|
||||
{
|
||||
policy: "and(pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c), older(512))",
|
||||
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cad03010040b275",
|
||||
expected: descriptor.And{
|
||||
First: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
Second: &descriptor.Older{
|
||||
Timeout: 512,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
policy: "and(older(512), pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c))",
|
||||
expectedScript: "03010040b2752081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cac",
|
||||
expected: descriptor.And{
|
||||
Second: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
First: &descriptor.Older{
|
||||
Timeout: 512,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
policy: "and(pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798))",
|
||||
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cad2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac",
|
||||
expected: descriptor.And{
|
||||
First: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
|
||||
},
|
||||
},
|
||||
},
|
||||
Second: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var parsed descriptor.And
|
||||
err := parsed.Parse(test.policy)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, parsed)
|
||||
|
||||
script, err := parsed.Script(false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedScript, script)
|
||||
}
|
||||
}
|
||||
10
common/descriptor/types.go
Normal file
10
common/descriptor/types.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package descriptor
|
||||
|
||||
type Key struct {
|
||||
Hex string
|
||||
}
|
||||
|
||||
type TaprootDescriptor struct {
|
||||
InternalKey Key
|
||||
ScriptTree []Expression
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func EncodeAddress(
|
||||
|
||||
func DecodeAddress(
|
||||
addr string,
|
||||
) (hrp string, userKey *secp256k1.PublicKey, aspKey *secp256k1.PublicKey, err error) {
|
||||
) (hrp string, userKey, aspKey *secp256k1.PublicKey, err error) {
|
||||
prefix, buf, err := bech32.DecodeNoLimit(addr)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
52
common/tree/descriptor.go
Normal file
52
common/tree/descriptor.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
func ComputeOutputScript(desc descriptor.TaprootDescriptor) ([]byte, error) {
|
||||
leaves := make([]taproot.TapElementsLeaf, 0)
|
||||
|
||||
for _, l := range desc.ScriptTree {
|
||||
script, err := l.Script(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scriptBytes, err := hex.DecodeString(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
leaves = append(leaves, taproot.NewBaseTapElementsLeaf(scriptBytes))
|
||||
}
|
||||
|
||||
taprootTree := taproot.AssembleTaprootScriptTree(
|
||||
leaves...,
|
||||
)
|
||||
|
||||
root := taprootTree.RootNode.TapHash()
|
||||
|
||||
internalKey, err := hex.DecodeString(desc.InternalKey.Hex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
internalKeyParsed, err := schnorr.ParsePubKey(internalKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taprootKey := taproot.ComputeTaprootOutputKey(internalKeyParsed, root[:])
|
||||
|
||||
outputScript, err := taprootOutputScript(taprootKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return outputScript, nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package tree
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
@@ -62,7 +63,7 @@ func DecodeClosure(script []byte) (Closure, error) {
|
||||
return closure, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid closure script")
|
||||
return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script))
|
||||
}
|
||||
|
||||
func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
|
||||
Reference in New Issue
Block a user