mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 12:14:21 +01:00
Covenant-less TxBuilder (#178)
* initial commit * wip * add bitcointree pkg in common * add bitcoin txbuilder * fix BuildPoolTx test * fix sweeper * v0 musig2 congestion tree * bitcointree: add signatures support * add Makefile in common * fix lint * fix go.mod and TxBuilder * go mod tidy * rename "pset" --> "psbt" * add GetSweepInput method in TxBuilder * fix extractSweepLeaf (bitcoin tx builder)
This commit is contained in:
@@ -35,8 +35,8 @@ require (
|
|||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
|
||||||
google.golang.org/grpc v1.64.0
|
google.golang.org/grpc v1.64.0
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -128,10 +128,10 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
|||||||
2
common/Makefile
Normal file
2
common/Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
375
common/bitcointree/builder.go
Normal file
375
common/bitcointree/builder.go
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
package bitcointree
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common/tree"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CraftSharedOutput returns the taproot script and the amount of the initial root output
|
||||||
|
func CraftSharedOutput(
|
||||||
|
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||||
|
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
|
||||||
|
) ([]byte, int64, error) {
|
||||||
|
aggregatedKey, _, err := createAggregatedKeyWithSweep(
|
||||||
|
cosigners, aspPubkey, roundLifetime,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := createRootNode(aggregatedKey, aspPubkey, receivers, feeSatsPerNode, unilateralExitDelay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amount := root.getAmount() + int64(feeSatsPerNode)
|
||||||
|
|
||||||
|
scriptPubKey, err := taprootOutputScript(aggregatedKey.FinalKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scriptPubKey, amount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CraftCongestionTree creates all the tree's transactions
|
||||||
|
func CraftCongestionTree(
|
||||||
|
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||||
|
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
|
||||||
|
) (tree.CongestionTree, error) {
|
||||||
|
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
|
||||||
|
cosigners, aspPubkey, roundLifetime,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := createRootNode(aggregatedKey, aspPubkey, receivers, feeSatsPerNode, unilateralExitDelay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
congestionTree := make(tree.CongestionTree, 0)
|
||||||
|
|
||||||
|
ins := []*wire.OutPoint{initialInput}
|
||||||
|
nodes := []node{root}
|
||||||
|
|
||||||
|
for len(nodes) > 0 {
|
||||||
|
nextNodes := make([]node, 0)
|
||||||
|
nextInputsArgs := make([]*wire.OutPoint, 0)
|
||||||
|
|
||||||
|
treeLevel := make([]tree.Node, 0)
|
||||||
|
|
||||||
|
for i, node := range nodes {
|
||||||
|
treeNode, err := getTreeNode(node, ins[i], schnorr.SerializePubKey(aggregatedKey.PreTweakedKey), sweepTapLeaf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeTxHash, err := chainhash.NewHashFromStr(treeNode.Txid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
treeLevel = append(treeLevel, treeNode)
|
||||||
|
|
||||||
|
children := node.getChildren()
|
||||||
|
|
||||||
|
for i, child := range children {
|
||||||
|
nextNodes = append(nextNodes, child)
|
||||||
|
|
||||||
|
nextInputsArgs = append(nextInputsArgs, &wire.OutPoint{
|
||||||
|
Hash: *nodeTxHash,
|
||||||
|
Index: uint32(i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
congestionTree = append(congestionTree, treeLevel)
|
||||||
|
nodes = append([]node{}, nextNodes...)
|
||||||
|
ins = append([]*wire.OutPoint{}, nextInputsArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return congestionTree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type node interface {
|
||||||
|
getAmount() int64 // returns the input amount of the node = sum of all receivers' amounts + fees
|
||||||
|
getOutputs() ([]*wire.TxOut, error)
|
||||||
|
getChildren() []node
|
||||||
|
}
|
||||||
|
|
||||||
|
type leaf struct {
|
||||||
|
aspKey *secp256k1.PublicKey
|
||||||
|
vtxoKey *secp256k1.PublicKey
|
||||||
|
exitDelay int64
|
||||||
|
amount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type branch struct {
|
||||||
|
aggregatedKey *musig2.AggregateKey
|
||||||
|
children []node
|
||||||
|
feeAmount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *branch) getChildren() []node {
|
||||||
|
return b.children
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *leaf) getChildren() []node {
|
||||||
|
return []node{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *branch) getAmount() int64 {
|
||||||
|
amount := int64(0)
|
||||||
|
for _, child := range b.children {
|
||||||
|
amount += child.getAmount()
|
||||||
|
amount += b.feeAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *leaf) getAmount() int64 {
|
||||||
|
return l.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
|
||||||
|
redeemClosure := &CSVSigClosure{
|
||||||
|
Pubkey: l.vtxoKey,
|
||||||
|
Seconds: uint(l.exitDelay),
|
||||||
|
}
|
||||||
|
|
||||||
|
redeemLeaf, err := redeemClosure.Leaf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitClosure := &ForfeitClosure{
|
||||||
|
Pubkey: l.vtxoKey,
|
||||||
|
AspPubkey: l.aspKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
leafTaprootTree := txscript.AssembleTaprootScriptTree(
|
||||||
|
*redeemLeaf, *forfeitLeaf,
|
||||||
|
)
|
||||||
|
root := leafTaprootTree.RootNode.TapHash()
|
||||||
|
|
||||||
|
taprootKey := txscript.ComputeTaprootOutputKey(
|
||||||
|
UnspendableKey(),
|
||||||
|
root[:],
|
||||||
|
)
|
||||||
|
|
||||||
|
script, err := taprootOutputScript(taprootKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output := &wire.TxOut{
|
||||||
|
Value: l.amount,
|
||||||
|
PkScript: script,
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*wire.TxOut{output}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *branch) getOutputs() ([]*wire.TxOut, error) {
|
||||||
|
sharedOutputScript, err := taprootOutputScript(b.aggregatedKey.FinalKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := make([]*wire.TxOut, 0)
|
||||||
|
|
||||||
|
for _, child := range b.children {
|
||||||
|
outputs = append(outputs, &wire.TxOut{
|
||||||
|
Value: child.getAmount() + b.feeAmount,
|
||||||
|
PkScript: sharedOutputScript,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTreeNode(
|
||||||
|
n node,
|
||||||
|
input *wire.OutPoint,
|
||||||
|
inputTapInternalKey []byte,
|
||||||
|
inputSweepTapLeaf *psbt.TaprootTapLeafScript,
|
||||||
|
) (tree.Node, error) {
|
||||||
|
partialTx, err := getTx(n, input, inputTapInternalKey, inputSweepTapLeaf)
|
||||||
|
if err != nil {
|
||||||
|
return tree.Node{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
txid := partialTx.UnsignedTx.TxHash().String()
|
||||||
|
|
||||||
|
tx, err := partialTx.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return tree.Node{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree.Node{
|
||||||
|
Txid: txid,
|
||||||
|
Tx: tx,
|
||||||
|
ParentTxid: input.Hash.String(),
|
||||||
|
Leaf: len(n.getChildren()) == 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTx(
|
||||||
|
n node,
|
||||||
|
input *wire.OutPoint,
|
||||||
|
inputTapInternalKey []byte,
|
||||||
|
inputSweepTapLeaf *psbt.TaprootTapLeafScript,
|
||||||
|
) (*psbt.Packet, error) {
|
||||||
|
outputs, err := n.getOutputs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := psbt.New([]*wire.OutPoint{input}, outputs, 2, 0, []uint32{wire.MaxTxInSequenceNum})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updater, err := psbt.NewUpdater(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInSighashType(0, int(txscript.SigHashDefault)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Inputs[0].TaprootInternalKey = inputTapInternalKey
|
||||||
|
tx.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{inputSweepTapLeaf}
|
||||||
|
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRootNode(
|
||||||
|
aggregatedKey *musig2.AggregateKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||||
|
feeSatsPerNode uint64, unilateralExitDelay int64,
|
||||||
|
) (root node, err error) {
|
||||||
|
if len(receivers) == 0 {
|
||||||
|
return nil, fmt.Errorf("no receivers provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := make([]node, 0, len(receivers))
|
||||||
|
for _, r := range receivers {
|
||||||
|
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverKey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
leafNode := &leaf{
|
||||||
|
aspKey: aspPubkey,
|
||||||
|
vtxoKey: receiverKey,
|
||||||
|
exitDelay: unilateralExitDelay,
|
||||||
|
amount: int64(r.Amount),
|
||||||
|
}
|
||||||
|
nodes = append(nodes, leafNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(nodes) > 1 {
|
||||||
|
nodes, err = createUpperLevel(nodes, aggregatedKey, int64(feeSatsPerNode))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAggregatedKeyWithSweep(
|
||||||
|
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, roundLifetime int64,
|
||||||
|
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
|
||||||
|
sweepClosure := &CSVSigClosure{
|
||||||
|
Pubkey: aspPubkey,
|
||||||
|
Seconds: uint(roundLifetime),
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepLeaf, err := sweepClosure.Leaf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tapTree := txscript.AssembleTaprootScriptTree(*sweepLeaf)
|
||||||
|
tapTreeRoot := tapTree.RootNode.TapHash()
|
||||||
|
|
||||||
|
aggregatedKey, err := AggregateKeys(
|
||||||
|
cosigners, tapTreeRoot[:],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
index := tapTree.LeafProofIndex[sweepLeaf.TapHash()]
|
||||||
|
proof := tapTree.LeafMerkleProofs[index]
|
||||||
|
|
||||||
|
controlBlock := proof.ToControlBlock(aggregatedKey.PreTweakedKey)
|
||||||
|
controlBlockBytes, err := controlBlock.ToBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tapLeaf := &psbt.TaprootTapLeafScript{
|
||||||
|
ControlBlock: controlBlockBytes,
|
||||||
|
Script: sweepLeaf.Script,
|
||||||
|
LeafVersion: sweepLeaf.LeafVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregatedKey, tapLeaf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUpperLevel(nodes []node, aggregatedKey *musig2.AggregateKey, feeAmount int64) ([]node, error) {
|
||||||
|
if len(nodes)%2 != 0 {
|
||||||
|
last := nodes[len(nodes)-1]
|
||||||
|
pairs, err := createUpperLevel(nodes[:len(nodes)-1], aggregatedKey, feeAmount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(pairs, last), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs := make([]node, 0, len(nodes)/2)
|
||||||
|
for i := 0; i < len(nodes); i += 2 {
|
||||||
|
left := nodes[i]
|
||||||
|
right := nodes[i+1]
|
||||||
|
branchNode := &branch{
|
||||||
|
aggregatedKey: aggregatedKey,
|
||||||
|
feeAmount: feeAmount,
|
||||||
|
children: []node{left, right},
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs = append(pairs, branchNode)
|
||||||
|
}
|
||||||
|
return pairs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
|
||||||
|
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(
|
||||||
|
schnorr.SerializePubKey(taprootKey),
|
||||||
|
).Script()
|
||||||
|
}
|
||||||
505
common/bitcointree/musig2.go
Normal file
505
common/bitcointree/musig2.go
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
package bitcointree
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common/tree"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCongestionTreeNotSet = errors.New("congestion tree not set")
|
||||||
|
ErrAggregateKeyNotSet = errors.New("aggregate key not set")
|
||||||
|
)
|
||||||
|
|
||||||
|
type TreeNonces [][][66]byte // public nonces
|
||||||
|
type TreePartialSigs [][]*musig2.PartialSignature
|
||||||
|
|
||||||
|
type SignerSession interface {
|
||||||
|
GetNonces(*btcec.PublicKey) (TreeNonces, error) // generate of return cached nonce for this session
|
||||||
|
SetKeys([]*btcec.PublicKey, TreeNonces) error // set the keys for this session (with the combined nonces)
|
||||||
|
Sign(*btcec.PrivateKey) (TreePartialSigs, error) // sign the tree
|
||||||
|
}
|
||||||
|
|
||||||
|
type CoordinatorSession interface {
|
||||||
|
AddNonce(*btcec.PublicKey, TreeNonces) error
|
||||||
|
AggregateNonces() (TreeNonces, error)
|
||||||
|
AddSig(*btcec.PublicKey, TreePartialSigs) error
|
||||||
|
// SignTree combines the signatures and add them to the tree's psbts
|
||||||
|
SignTree() (tree.CongestionTree, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AggregateKeys(
|
||||||
|
pubkeys []*btcec.PublicKey,
|
||||||
|
scriptRoot []byte,
|
||||||
|
) (*musig2.AggregateKey, error) {
|
||||||
|
key, _, _, err := musig2.AggregateKeys(pubkeys, true,
|
||||||
|
musig2.WithTaprootKeyTweak(scriptRoot),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateTreeSigs(
|
||||||
|
minRelayFee int64,
|
||||||
|
scriptRoot []byte,
|
||||||
|
finalAggregatedKey *btcec.PublicKey,
|
||||||
|
tree tree.CongestionTree,
|
||||||
|
) error {
|
||||||
|
prevoutFetcher, err := prevOutFetcherFactory(minRelayFee, finalAggregatedKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, level := range tree {
|
||||||
|
for _, node := range level {
|
||||||
|
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := partialTx.Inputs[0].TaprootKeySpendSig
|
||||||
|
if len(sig) == 0 {
|
||||||
|
return errors.New("unsigned tree input")
|
||||||
|
}
|
||||||
|
|
||||||
|
schnorrSig, err := schnorr.ParseSignature(sig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFetcher := prevoutFetcher(partialTx)
|
||||||
|
|
||||||
|
message, err := txscript.CalcTaprootSignatureHash(
|
||||||
|
txscript.NewTxSigHashes(partialTx.UnsignedTx, inputFetcher),
|
||||||
|
txscript.SigHashDefault,
|
||||||
|
partialTx.UnsignedTx,
|
||||||
|
0,
|
||||||
|
inputFetcher,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !schnorrSig.Verify(message, finalAggregatedKey) {
|
||||||
|
return errors.New("invalid signature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n TreeNonces) Decode(r io.Reader, matrixFormat []int) error {
|
||||||
|
for i := 0; i < len(matrixFormat); i++ {
|
||||||
|
for j := 0; j < matrixFormat[i]; j++ {
|
||||||
|
// read 66 bytes
|
||||||
|
nonce := make([]byte, 66)
|
||||||
|
_, err := r.Read(nonce)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n[i][j] = [66]byte(nonce)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n TreeNonces) Encode(w io.Writer) error {
|
||||||
|
for i := 0; i < len(n); i++ {
|
||||||
|
for j := 0; j < len(n[i]); j++ {
|
||||||
|
nonce := n[i][j][:]
|
||||||
|
_, err := w.Write(nonce)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n TreePartialSigs) Decode(r io.Reader, matrixFormat []int) error {
|
||||||
|
for i := 0; i < len(matrixFormat); i++ {
|
||||||
|
for j := 0; j < matrixFormat[i]; j++ {
|
||||||
|
sig := &musig2.PartialSignature{}
|
||||||
|
if err := sig.Decode(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n TreePartialSigs) Encode(w io.Writer) error {
|
||||||
|
for i := 0; i < len(n); i++ {
|
||||||
|
for j := 0; j < len(n[i]); j++ {
|
||||||
|
if err := n[i][j].Encode(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTreeSignerSession(
|
||||||
|
congestionTree tree.CongestionTree,
|
||||||
|
minRelayFee int64,
|
||||||
|
scriptRoot []byte,
|
||||||
|
) SignerSession {
|
||||||
|
return &treeSignerSession{
|
||||||
|
tree: congestionTree,
|
||||||
|
minRelayFee: minRelayFee,
|
||||||
|
scriptRoot: scriptRoot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type treeSignerSession struct {
|
||||||
|
tree tree.CongestionTree
|
||||||
|
myNonces [][]*musig2.Nonces
|
||||||
|
keys []*btcec.PublicKey
|
||||||
|
aggregateNonces TreeNonces
|
||||||
|
minRelayFee int64
|
||||||
|
scriptRoot []byte
|
||||||
|
prevoutFetcher func(*psbt.Packet) txscript.PrevOutputFetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeSignerSession) generateNonces(key *btcec.PublicKey) error {
|
||||||
|
if t.tree == nil {
|
||||||
|
return ErrCongestionTreeNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
myNonces := make([][]*musig2.Nonces, 0)
|
||||||
|
|
||||||
|
for _, level := range t.tree {
|
||||||
|
levelNonces := make([]*musig2.Nonces, 0)
|
||||||
|
for range level {
|
||||||
|
nonce, err := musig2.GenNonces(
|
||||||
|
musig2.WithPublicKey(key),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
levelNonces = append(levelNonces, nonce)
|
||||||
|
}
|
||||||
|
myNonces = append(myNonces, levelNonces)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.myNonces = myNonces
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeSignerSession) GetNonces(key *btcec.PublicKey) (TreeNonces, error) {
|
||||||
|
if t.tree == nil {
|
||||||
|
return nil, ErrCongestionTreeNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.myNonces == nil {
|
||||||
|
if err := t.generateNonces(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonces := make(TreeNonces, 0)
|
||||||
|
|
||||||
|
for _, level := range t.myNonces {
|
||||||
|
levelNonces := make([][66]byte, 0)
|
||||||
|
for _, nonce := range level {
|
||||||
|
levelNonces = append(levelNonces, nonce.PubNonce)
|
||||||
|
}
|
||||||
|
nonces = append(nonces, levelNonces)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeSignerSession) SetKeys(keys []*btcec.PublicKey, nonces TreeNonces) error {
|
||||||
|
if t.keys != nil {
|
||||||
|
return errors.New("keys already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.aggregateNonces != nil {
|
||||||
|
return errors.New("nonces already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregateKey, err := AggregateKeys(keys, t.scriptRoot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevoutFetcher, err := prevOutFetcherFactory(t.minRelayFee, aggregateKey.FinalKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.prevoutFetcher = prevoutFetcher
|
||||||
|
t.aggregateNonces = nonces
|
||||||
|
t.keys = keys
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeSignerSession) Sign(seckey *secp256k1.PrivateKey) (TreePartialSigs, error) {
|
||||||
|
if t.tree == nil {
|
||||||
|
return nil, ErrCongestionTreeNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.keys == nil {
|
||||||
|
return nil, ErrAggregateKeyNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.aggregateNonces == nil {
|
||||||
|
return nil, errors.New("nonces not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs := make(TreePartialSigs, 0)
|
||||||
|
|
||||||
|
for i, level := range t.tree {
|
||||||
|
levelSigs := make([]*musig2.PartialSignature, 0)
|
||||||
|
|
||||||
|
for j, node := range level {
|
||||||
|
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// sign the node
|
||||||
|
sig, err := t.signPartial(partialTx, i, j, seckey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
levelSigs = append(levelSigs, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs = append(sigs, levelSigs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sigs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeSignerSession) signPartial(partialTx *psbt.Packet, posx int, posy int, seckey *btcec.PrivateKey) (*musig2.PartialSignature, error) {
|
||||||
|
inputFetcher := t.prevoutFetcher(partialTx)
|
||||||
|
|
||||||
|
myNonce := t.myNonces[posx][posy]
|
||||||
|
aggregatedNonce := t.aggregateNonces[posx][posy]
|
||||||
|
|
||||||
|
message, err := txscript.CalcTaprootSignatureHash(
|
||||||
|
txscript.NewTxSigHashes(partialTx.UnsignedTx, inputFetcher),
|
||||||
|
txscript.SigHashDefault,
|
||||||
|
partialTx.UnsignedTx,
|
||||||
|
0,
|
||||||
|
inputFetcher,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return musig2.Sign(
|
||||||
|
myNonce.SecNonce, seckey, aggregatedNonce, t.keys, [32]byte(message),
|
||||||
|
musig2.WithSortedKeys(), musig2.WithTaprootSignTweak(t.scriptRoot),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type treeCoordinatorSession struct {
|
||||||
|
scriptRoot []byte
|
||||||
|
tree tree.CongestionTree
|
||||||
|
keys []*btcec.PublicKey
|
||||||
|
nonces []TreeNonces
|
||||||
|
sigs []TreePartialSigs
|
||||||
|
prevoutFetcher func(*psbt.Packet) txscript.PrevOutputFetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTreeCoordinatorSession(congestionTree tree.CongestionTree, minRelayFee int64, scriptRoot []byte, keys []*btcec.PublicKey) (CoordinatorSession, error) {
|
||||||
|
aggregateKey, err := AggregateKeys(keys, scriptRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevoutFetcher, err := prevOutFetcherFactory(minRelayFee, aggregateKey.FinalKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &treeCoordinatorSession{
|
||||||
|
scriptRoot: scriptRoot,
|
||||||
|
tree: congestionTree,
|
||||||
|
keys: keys,
|
||||||
|
nonces: make([]TreeNonces, len(keys)),
|
||||||
|
sigs: make([]TreePartialSigs, len(keys)),
|
||||||
|
prevoutFetcher: prevoutFetcher,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeCoordinatorSession) getPubkeyIndex(pubkey *btcec.PublicKey) int {
|
||||||
|
for i, key := range t.keys {
|
||||||
|
if key.IsEqual(pubkey) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeCoordinatorSession) AddNonce(pubkey *btcec.PublicKey, nonce TreeNonces) error {
|
||||||
|
index := t.getPubkeyIndex(pubkey)
|
||||||
|
if index == -1 {
|
||||||
|
return errors.New("public key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.nonces[index] = nonce
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeCoordinatorSession) AddSig(pubkey *btcec.PublicKey, sig TreePartialSigs) error {
|
||||||
|
index := t.getPubkeyIndex(pubkey)
|
||||||
|
if index == -1 {
|
||||||
|
return errors.New("public key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.sigs[index] = sig
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeCoordinatorSession) AggregateNonces() (TreeNonces, error) {
|
||||||
|
for _, nonce := range t.nonces {
|
||||||
|
if nonce == nil {
|
||||||
|
return nil, errors.New("nonces not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregatedNonces := make(TreeNonces, 0)
|
||||||
|
|
||||||
|
for i, level := range t.tree {
|
||||||
|
levelNonces := make([][66]byte, 0)
|
||||||
|
for j := range level {
|
||||||
|
|
||||||
|
nonces := make([][66]byte, 0)
|
||||||
|
for _, n := range t.nonces {
|
||||||
|
nonces = append(nonces, n[i][j])
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregatedNonce, err := musig2.AggregateNonces(nonces)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
levelNonces = append(levelNonces, aggregatedNonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregatedNonces = append(aggregatedNonces, levelNonces)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregatedNonces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignTree implements CoordinatorSession.
|
||||||
|
func (t *treeCoordinatorSession) SignTree() (tree.CongestionTree, error) {
|
||||||
|
for _, sig := range t.sigs {
|
||||||
|
if sig == nil {
|
||||||
|
return nil, errors.New("signatures not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregatedKey, err := AggregateKeys(t.keys, t.scriptRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, level := range t.tree {
|
||||||
|
for j, node := range level {
|
||||||
|
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs := make([]*musig2.PartialSignature, 0)
|
||||||
|
for _, sig := range t.sigs {
|
||||||
|
sigs = append(sigs, sig[i][j])
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFetcher := t.prevoutFetcher(partialTx)
|
||||||
|
|
||||||
|
message, err := txscript.CalcTaprootSignatureHash(
|
||||||
|
txscript.NewTxSigHashes(partialTx.UnsignedTx, inputFetcher),
|
||||||
|
txscript.SigHashDefault,
|
||||||
|
partialTx.UnsignedTx,
|
||||||
|
0,
|
||||||
|
inputFetcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
combinedSig := musig2.CombineSigs(
|
||||||
|
sigs[0].R, sigs,
|
||||||
|
musig2.WithTaprootTweakedCombine([32]byte(message), t.keys, t.scriptRoot, true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !combinedSig.Verify(message, aggregatedKey.FinalKey) {
|
||||||
|
return nil, errors.New("invalid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
partialTx.Inputs[0].TaprootKeySpendSig = combinedSig.Serialize()
|
||||||
|
|
||||||
|
encodedSignedTx, err := partialTx.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Tx = encodedSignedTx
|
||||||
|
t.tree[i][j] = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.tree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a final aggregated key and a min-relay-fee, returns the expected prevout
|
||||||
|
func prevOutFetcherFactory(
|
||||||
|
feeAmount int64, finalAggregatedKey *btcec.PublicKey,
|
||||||
|
) (
|
||||||
|
func(partial *psbt.Packet) txscript.PrevOutputFetcher, error,
|
||||||
|
) {
|
||||||
|
pkscript, err := taprootOutputScript(finalAggregatedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(partial *psbt.Packet) txscript.PrevOutputFetcher {
|
||||||
|
outputsAmount := int64(0)
|
||||||
|
for _, output := range partial.UnsignedTx.TxOut {
|
||||||
|
outputsAmount += output.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return &treePrevOutFetcher{
|
||||||
|
prevout: &wire.TxOut{
|
||||||
|
Value: outputsAmount + feeAmount,
|
||||||
|
PkScript: pkscript,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type treePrevOutFetcher struct {
|
||||||
|
prevout *wire.TxOut
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *treePrevOutFetcher) FetchPrevOutput(wire.OutPoint) *wire.TxOut {
|
||||||
|
return f.prevout
|
||||||
|
}
|
||||||
176
common/bitcointree/musig2_test.go
Normal file
176
common/bitcointree/musig2_test.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package bitcointree_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common/bitcointree"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minRelayFee = 1000
|
||||||
|
exitDelay = 512
|
||||||
|
lifetime = 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
var testTxid, _ = chainhash.NewHashFromStr("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12")
|
||||||
|
|
||||||
|
func TestRoundTripSignTree(t *testing.T) {
|
||||||
|
fixtures := parseFixtures(t)
|
||||||
|
for _, f := range fixtures.Valid {
|
||||||
|
alice, err := secp256k1.GeneratePrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bob, err := secp256k1.GeneratePrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
asp, err := secp256k1.GeneratePrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cosigners := make([]*secp256k1.PublicKey, 0)
|
||||||
|
cosigners = append(cosigners, alice.PubKey())
|
||||||
|
cosigners = append(cosigners, bob.PubKey())
|
||||||
|
cosigners = append(cosigners, asp.PubKey())
|
||||||
|
|
||||||
|
// Create a new tree
|
||||||
|
tree, err := bitcointree.CraftCongestionTree(
|
||||||
|
&wire.OutPoint{
|
||||||
|
Hash: *testTxid,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
cosigners,
|
||||||
|
asp.PubKey(),
|
||||||
|
f.Receivers,
|
||||||
|
minRelayFee,
|
||||||
|
lifetime,
|
||||||
|
exitDelay,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sweepClosure := bitcointree.CSVSigClosure{
|
||||||
|
Pubkey: asp.PubKey(),
|
||||||
|
Seconds: lifetime,
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTapLeaf, err := sweepClosure.Leaf()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
|
||||||
|
root := sweepTapTree.RootNode.TapHash()
|
||||||
|
|
||||||
|
aspCoordinator, err := bitcointree.NewTreeCoordinatorSession(
|
||||||
|
tree,
|
||||||
|
minRelayFee,
|
||||||
|
root.CloneBytes(),
|
||||||
|
[]*secp256k1.PublicKey{alice.PubKey(), bob.PubKey(), asp.PubKey()},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
aliceSession := bitcointree.NewTreeSignerSession(tree, minRelayFee, root.CloneBytes())
|
||||||
|
bobSession := bitcointree.NewTreeSignerSession(tree, minRelayFee, root.CloneBytes())
|
||||||
|
aspSession := bitcointree.NewTreeSignerSession(tree, minRelayFee, root.CloneBytes())
|
||||||
|
|
||||||
|
aliceNonces, err := aliceSession.GetNonces(alice.PubKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bobNonces, err := bobSession.GetNonces(bob.PubKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
aspNonces, err := aspSession.GetNonces(asp.PubKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = aspCoordinator.AddNonce(alice.PubKey(), aliceNonces)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = aspCoordinator.AddNonce(bob.PubKey(), bobNonces)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = aspCoordinator.AddNonce(asp.PubKey(), aspNonces)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
aggregatedNonce, err := aspCoordinator.AggregateNonces()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// coordinator sends the combined nonce to all signers
|
||||||
|
|
||||||
|
err = aliceSession.SetKeys(
|
||||||
|
cosigners,
|
||||||
|
aggregatedNonce,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = bobSession.SetKeys(
|
||||||
|
cosigners,
|
||||||
|
aggregatedNonce,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = aspSession.SetKeys(
|
||||||
|
cosigners,
|
||||||
|
aggregatedNonce,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
aliceSig, err := aliceSession.Sign(alice)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bobSig, err := bobSession.Sign(bob)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
aspSig, err := aspSession.Sign(asp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// coordinator receives the signatures and combines them
|
||||||
|
err = aspCoordinator.AddSig(alice.PubKey(), aliceSig)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = aspCoordinator.AddSig(bob.PubKey(), bobSig)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = aspCoordinator.AddSig(asp.PubKey(), aspSig)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signedTree, err := aspCoordinator.SignTree()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// verify the tree
|
||||||
|
aggregatedKey, err := bitcointree.AggregateKeys(cosigners, root.CloneBytes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = bitcointree.ValidateTreeSigs(
|
||||||
|
minRelayFee,
|
||||||
|
root.CloneBytes(),
|
||||||
|
aggregatedKey.FinalKey,
|
||||||
|
signedTree,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fixture struct {
|
||||||
|
Valid []struct {
|
||||||
|
Receivers []bitcointree.Receiver `json:"receivers"`
|
||||||
|
} `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFixtures(t *testing.T) fixture {
|
||||||
|
file, err := os.ReadFile("testdata/musig2.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
err = json.Unmarshal(file, &v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
vv := v["treeSignature"].(map[string]interface{})
|
||||||
|
file, _ = json.Marshal(vv)
|
||||||
|
var fixtures fixture
|
||||||
|
err = json.Unmarshal(file, &fixtures)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return fixtures
|
||||||
|
}
|
||||||
197
common/bitcointree/script.go
Normal file
197
common/bitcointree/script.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package bitcointree
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Closure interface {
|
||||||
|
Leaf() (*txscript.TapLeaf, error)
|
||||||
|
Decode(script []byte) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CSVSigClosure struct {
|
||||||
|
Pubkey *secp256k1.PublicKey
|
||||||
|
Seconds uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForfeitClosure struct {
|
||||||
|
Pubkey *secp256k1.PublicKey
|
||||||
|
AspPubkey *secp256k1.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeClosure(script []byte) (Closure, error) {
|
||||||
|
var closure Closure
|
||||||
|
|
||||||
|
closure = &CSVSigClosure{}
|
||||||
|
if valid, err := closure.Decode(script); err == nil && valid {
|
||||||
|
return closure, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
closure = &ForfeitClosure{}
|
||||||
|
if valid, err := closure.Decode(script); err == nil && valid {
|
||||||
|
return closure, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("invalid closure script")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ForfeitClosure) Leaf() (*txscript.TapLeaf, error) {
|
||||||
|
aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey)
|
||||||
|
userKeyBytes := schnorr.SerializePubKey(f.Pubkey)
|
||||||
|
|
||||||
|
script, err := txscript.NewScriptBuilder().AddData(aspKeyBytes).
|
||||||
|
AddOp(txscript.OP_CHECKSIGVERIFY).AddData(userKeyBytes).
|
||||||
|
AddOp(txscript.OP_CHECKSIG).Script()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tapLeaf := txscript.NewBaseTapLeaf(script)
|
||||||
|
return &tapLeaf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ForfeitClosure) Decode(script []byte) (bool, error) {
|
||||||
|
valid, aspPubKey, err := decodeChecksigScript(script)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
valid, pubkey, err := decodeChecksigScript(script[33:])
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Pubkey = pubkey
|
||||||
|
f.AspPubkey = aspPubKey
|
||||||
|
|
||||||
|
rebuilt, err := f.Leaf()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(rebuilt.Script, script) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CSVSigClosure) Leaf() (*txscript.TapLeaf, error) {
|
||||||
|
script, err := encodeCsvWithChecksigScript(d.Pubkey, d.Seconds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tapLeaf := txscript.NewBaseTapLeaf(script)
|
||||||
|
return &tapLeaf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
|
||||||
|
csvIndex := bytes.Index(
|
||||||
|
script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP},
|
||||||
|
)
|
||||||
|
if csvIndex == -1 || csvIndex == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence := script[1:csvIndex]
|
||||||
|
|
||||||
|
seconds, err := common.BIP68Decode(sequence)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
checksigScript := script[csvIndex+2:]
|
||||||
|
valid, pubkey, err := decodeChecksigScript(checksigScript)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuilt, err := encodeCsvWithChecksigScript(pubkey, seconds)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(rebuilt, script) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Pubkey = pubkey
|
||||||
|
d.Seconds = seconds
|
||||||
|
|
||||||
|
return valid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) {
|
||||||
|
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
|
||||||
|
if data32Index == -1 {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := script[data32Index+1 : data32Index+33]
|
||||||
|
if len(key) != 32 {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err := schnorr.ParsePubKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, pubkey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSequenceVerifyScript without checksig
|
||||||
|
func encodeCsvScript(seconds uint) ([]byte, error) {
|
||||||
|
sequence, err := common.BIP68Encode(seconds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return txscript.NewScriptBuilder().AddData(sequence).AddOps([]byte{
|
||||||
|
txscript.OP_CHECKSEQUENCEVERIFY,
|
||||||
|
txscript.OP_DROP,
|
||||||
|
}).Script()
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSequenceVerifyScript + checksig
|
||||||
|
func encodeCsvWithChecksigScript(
|
||||||
|
pubkey *secp256k1.PublicKey, seconds uint,
|
||||||
|
) ([]byte, error) {
|
||||||
|
script, err := encodeChecksigScript(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
csvScript, err := encodeCsvScript(seconds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(csvScript, script...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeChecksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
|
||||||
|
key := schnorr.SerializePubKey(pubkey)
|
||||||
|
return txscript.NewScriptBuilder().AddData(key).
|
||||||
|
AddOp(txscript.OP_CHECKSIG).Script()
|
||||||
|
}
|
||||||
50
common/bitcointree/testdata/musig2.json
vendored
Normal file
50
common/bitcointree/testdata/musig2.json
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"treeSignature": {
|
||||||
|
"valid": [
|
||||||
|
{
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 8000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
6
common/bitcointree/type.go
Normal file
6
common/bitcointree/type.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package bitcointree
|
||||||
|
|
||||||
|
type Receiver struct {
|
||||||
|
Pubkey string
|
||||||
|
Amount uint64
|
||||||
|
}
|
||||||
255
common/bitcointree/validation.go
Normal file
255
common/bitcointree/validation.go
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
package bitcointree
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common/tree"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidPoolTransaction = errors.New("invalid pool transaction")
|
||||||
|
ErrInvalidPoolTransactionOutputs = errors.New("invalid number of outputs in pool transaction")
|
||||||
|
ErrEmptyTree = errors.New("empty congestion tree")
|
||||||
|
ErrInvalidRootLevel = errors.New("root level must have only one node")
|
||||||
|
ErrNoLeaves = errors.New("no leaves in the tree")
|
||||||
|
ErrNodeTransactionEmpty = errors.New("node transaction is empty")
|
||||||
|
ErrNodeTxidEmpty = errors.New("node txid is empty")
|
||||||
|
ErrNodeParentTxidEmpty = errors.New("node parent txid is empty")
|
||||||
|
ErrNodeTxidDifferent = errors.New("node txid differs from node transaction")
|
||||||
|
ErrNumberOfInputs = errors.New("node transaction should have only one input")
|
||||||
|
ErrNumberOfOutputs = errors.New("node transaction should have only three or two outputs")
|
||||||
|
ErrParentTxidInput = errors.New("parent txid should be the input of the node transaction")
|
||||||
|
ErrNumberOfChildren = errors.New("node branch transaction should have two children")
|
||||||
|
ErrLeafChildren = errors.New("leaf node should have max 1 child")
|
||||||
|
ErrInvalidChildTxid = errors.New("invalid child txid")
|
||||||
|
ErrNumberOfTapscripts = errors.New("input should have 1 tapscript leaf")
|
||||||
|
ErrInternalKey = errors.New("invalid taproot internal key")
|
||||||
|
ErrInvalidTaprootScript = errors.New("invalid taproot script")
|
||||||
|
ErrInvalidControlBlock = errors.New("invalid control block")
|
||||||
|
ErrInvalidTaprootScriptLen = errors.New("invalid taproot script length (expected 32 bytes)")
|
||||||
|
ErrInvalidLeafTaprootScript = errors.New("invalid leaf taproot script")
|
||||||
|
ErrInvalidAmount = errors.New("children amount is different from parent amount")
|
||||||
|
ErrInvalidSweepSequence = errors.New("invalid sweep sequence")
|
||||||
|
ErrInvalidASP = errors.New("invalid ASP")
|
||||||
|
ErrMissingFeeOutput = errors.New("missing fee output")
|
||||||
|
ErrInvalidLeftOutput = errors.New("invalid left output")
|
||||||
|
ErrInvalidRightOutput = errors.New("invalid right output")
|
||||||
|
ErrMissingSweepTapscript = errors.New("missing sweep tapscript")
|
||||||
|
ErrInvalidLeaf = errors.New("leaf node shouldn't have children")
|
||||||
|
ErrWrongPoolTxID = errors.New("root input should be the pool tx outpoint")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
|
||||||
|
var unspendablePoint = []byte{
|
||||||
|
0x02, 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a,
|
||||||
|
0x5e, 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sharedOutputIndex = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func UnspendableKey() *secp256k1.PublicKey {
|
||||||
|
key, _ := secp256k1.ParsePubKey(unspendablePoint)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCongestionTree checks if the given congestion tree is valid
|
||||||
|
// poolTxID & poolTxIndex & poolTxAmount are used to validate the root input outpoint
|
||||||
|
// aspPublicKey & roundLifetime are used to validate the sweep tapscript leaves
|
||||||
|
// besides that, the function validates:
|
||||||
|
// - the number of nodes
|
||||||
|
// - the number of leaves
|
||||||
|
// - children coherence with parent
|
||||||
|
// - every control block and taproot output scripts
|
||||||
|
// - input and output amounts
|
||||||
|
func ValidateCongestionTree(
|
||||||
|
tree tree.CongestionTree, poolTx string, aspPublicKey *secp256k1.PublicKey,
|
||||||
|
roundLifetime int64, cosigners []*secp256k1.PublicKey, minRelayFee int64,
|
||||||
|
) error {
|
||||||
|
poolTransaction, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
|
||||||
|
if err != nil {
|
||||||
|
return ErrInvalidPoolTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(poolTransaction.Outputs) < sharedOutputIndex+1 {
|
||||||
|
return ErrInvalidPoolTransactionOutputs
|
||||||
|
}
|
||||||
|
|
||||||
|
poolTxAmount := poolTransaction.UnsignedTx.TxOut[sharedOutputIndex].Value
|
||||||
|
|
||||||
|
nbNodes := tree.NumberOfNodes()
|
||||||
|
if nbNodes == 0 {
|
||||||
|
return ErrEmptyTree
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tree[0]) != 1 {
|
||||||
|
return ErrInvalidRootLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that root input is connected to the pool tx
|
||||||
|
rootPsetB64 := tree[0][0].Tx
|
||||||
|
rootPset, err := psbt.NewFromRawBytes(strings.NewReader(rootPsetB64), true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid root transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rootPset.Inputs) != 1 {
|
||||||
|
return ErrNumberOfInputs
|
||||||
|
}
|
||||||
|
|
||||||
|
rootInput := rootPset.UnsignedTx.TxIn[0]
|
||||||
|
if chainhash.Hash(rootInput.PreviousOutPoint.Hash).String() != poolTransaction.UnsignedTx.TxHash().String() ||
|
||||||
|
rootInput.PreviousOutPoint.Index != sharedOutputIndex {
|
||||||
|
return ErrWrongPoolTxID
|
||||||
|
}
|
||||||
|
|
||||||
|
sumRootValue := minRelayFee
|
||||||
|
for _, output := range rootPset.UnsignedTx.TxOut {
|
||||||
|
sumRootValue += output.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if sumRootValue != poolTxAmount {
|
||||||
|
return ErrInvalidAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tree.Leaves()) == 0 {
|
||||||
|
return ErrNoLeaves
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepClosure := &CSVSigClosure{
|
||||||
|
Seconds: uint(roundLifetime),
|
||||||
|
Pubkey: aspPublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepLeaf, err := sweepClosure.Leaf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tapTree := txscript.AssembleTaprootScriptTree(*sweepLeaf)
|
||||||
|
root := tapTree.RootNode.TapHash()
|
||||||
|
|
||||||
|
signers := append(cosigners, aspPublicKey)
|
||||||
|
aggregatedKey, err := AggregateKeys(signers, root[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterates over all the nodes of the tree
|
||||||
|
for _, level := range tree {
|
||||||
|
for _, node := range level {
|
||||||
|
if err := validateNodeTransaction(
|
||||||
|
node, tree, aggregatedKey, minRelayFee,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNodeTransaction(
|
||||||
|
node tree.Node, tree tree.CongestionTree,
|
||||||
|
expectedAggregatedKey *musig2.AggregateKey, minRelayFee int64,
|
||||||
|
) error {
|
||||||
|
if node.Tx == "" {
|
||||||
|
return ErrNodeTransactionEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Txid == "" {
|
||||||
|
return ErrNodeTxidEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.ParentTxid == "" {
|
||||||
|
return ErrNodeParentTxidEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedPset, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid node transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decodedPset.UnsignedTx.TxHash().String() != node.Txid {
|
||||||
|
return ErrNodeTxidDifferent
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decodedPset.Inputs) != 1 {
|
||||||
|
return ErrNumberOfInputs
|
||||||
|
}
|
||||||
|
|
||||||
|
input := decodedPset.Inputs[0]
|
||||||
|
if len(input.TaprootLeafScript) != 1 {
|
||||||
|
return ErrNumberOfTapscripts
|
||||||
|
}
|
||||||
|
|
||||||
|
prevTxid := decodedPset.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
|
||||||
|
if prevTxid != node.ParentTxid {
|
||||||
|
return ErrParentTxidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
children := tree.Children(node.Txid)
|
||||||
|
|
||||||
|
if node.Leaf && len(children) >= 1 {
|
||||||
|
return ErrLeafChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
for childIndex, child := range children {
|
||||||
|
childTx, err := psbt.NewFromRawBytes(strings.NewReader(child.Tx), true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid child transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentOutput := decodedPset.UnsignedTx.TxOut[childIndex]
|
||||||
|
previousScriptKey := parentOutput.PkScript[2:]
|
||||||
|
if len(previousScriptKey) != 32 {
|
||||||
|
return ErrInvalidTaprootScript
|
||||||
|
}
|
||||||
|
|
||||||
|
inputData := decodedPset.Inputs[0]
|
||||||
|
|
||||||
|
inputTapInternalKey, err := schnorr.ParsePubKey(inputData.TaprootInternalKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid internal key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(inputData.TaprootInternalKey, schnorr.SerializePubKey(expectedAggregatedKey.PreTweakedKey)) {
|
||||||
|
return ErrInternalKey
|
||||||
|
}
|
||||||
|
|
||||||
|
inputTapLeaf := inputData.TaprootLeafScript[0]
|
||||||
|
|
||||||
|
ctrlBlock, err := txscript.ParseControlBlock(inputTapLeaf.ControlBlock)
|
||||||
|
if err != nil {
|
||||||
|
return ErrInvalidControlBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
rootHash := ctrlBlock.RootHash(inputTapLeaf.Script)
|
||||||
|
tapKey := txscript.ComputeTaprootOutputKey(inputTapInternalKey, rootHash)
|
||||||
|
|
||||||
|
if !bytes.Equal(schnorr.SerializePubKey(tapKey), schnorr.SerializePubKey(expectedAggregatedKey.FinalKey)) {
|
||||||
|
return ErrInvalidTaprootScript
|
||||||
|
}
|
||||||
|
|
||||||
|
sumChildAmount := minRelayFee
|
||||||
|
for _, output := range childTx.UnsignedTx.TxOut {
|
||||||
|
sumChildAmount += output.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if sumChildAmount != parentOutput.Value {
|
||||||
|
return ErrInvalidAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ require (
|
|||||||
github.com/btcsuite/btcd v0.23.1
|
github.com/btcsuite/btcd v0.23.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||||
|
github.com/btcsuite/btcd/btcutil/psbt v1.1.4
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.4 // indirect
|
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||||
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
|
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ service ArkService {
|
|||||||
body: "*"
|
body: "*"
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// TODO BTC: signTree rpc
|
||||||
rpc GetRound(GetRoundRequest) returns (GetRoundResponse) {
|
rpc GetRound(GetRoundRequest) returns (GetRoundResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/v1/round/{txid}"
|
get: "/v1/round/{txid}"
|
||||||
@@ -94,6 +95,7 @@ message GetRoundResponse {
|
|||||||
message GetEventStreamRequest {}
|
message GetEventStreamRequest {}
|
||||||
message GetEventStreamResponse {
|
message GetEventStreamResponse {
|
||||||
oneof event {
|
oneof event {
|
||||||
|
// TODO: BTC add "signTree" event
|
||||||
RoundFinalizationEvent round_finalization = 1;
|
RoundFinalizationEvent round_finalization = 1;
|
||||||
RoundFinalizedEvent round_finalized = 2;
|
RoundFinalizedEvent round_finalized = 2;
|
||||||
RoundFailed round_failed = 3;
|
RoundFailed round_failed = 3;
|
||||||
@@ -104,6 +106,7 @@ message PingRequest {
|
|||||||
string payment_id = 1;
|
string payment_id = 1;
|
||||||
}
|
}
|
||||||
message PingResponse {
|
message PingResponse {
|
||||||
|
// TODO: improve this response (returns oneof the round event)
|
||||||
repeated string forfeit_txs = 1;
|
repeated string forfeit_txs = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/timshannon/badgerhold/v4 v4.0.3
|
github.com/timshannon/badgerhold/v4 v4.0.3
|
||||||
github.com/vulpemventures/go-elements v0.5.3
|
github.com/vulpemventures/go-elements v0.5.3
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157
|
||||||
google.golang.org/grpc v1.64.0
|
google.golang.org/grpc v1.64.0
|
||||||
google.golang.org/protobuf v1.34.1
|
google.golang.org/protobuf v1.34.1
|
||||||
)
|
)
|
||||||
@@ -34,7 +34,7 @@ require (
|
|||||||
github.com/btcsuite/btcd v0.24.0
|
github.com/btcsuite/btcd v0.24.0
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.5
|
github.com/btcsuite/btcd/btcutil v1.1.5
|
||||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9 // indirect
|
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
@@ -69,11 +69,11 @@ require (
|
|||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/net v0.25.0
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
@@ -379,10 +379,10 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn
|
|||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
|||||||
@@ -395,9 +395,10 @@ func (s *service) startFinalization() {
|
|||||||
log.WithError(err).Warn("failed to create pool tx")
|
log.WithError(err).Warn("failed to create pool tx")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("pool tx created for round %s", round.Id)
|
log.Debugf("pool tx created for round %s", round.Id)
|
||||||
|
|
||||||
|
// TODO BTC make the senders sign the tree
|
||||||
|
|
||||||
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
|
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
|
changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import (
|
|||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/ark-network/ark/internal/core/domain"
|
"github.com/ark-network/ark/internal/core/domain"
|
||||||
"github.com/ark-network/ark/internal/core/ports"
|
"github.com/ark-network/ark/internal/core/ports"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/vulpemventures/go-elements/psetv2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// sweeper is an unexported service running while the main application service is started
|
// sweeper is an unexported service running while the main application service is started
|
||||||
@@ -158,8 +156,8 @@ func (s *sweeper) createTask(
|
|||||||
ctx,
|
ctx,
|
||||||
[]domain.VtxoKey{
|
[]domain.VtxoKey{
|
||||||
{
|
{
|
||||||
Txid: input.InputArgs.Txid,
|
Txid: input.GetHash().String(),
|
||||||
VOut: input.InputArgs.TxIndex,
|
VOut: input.GetIndex(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -169,20 +167,14 @@ func (s *sweeper) createTask(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if it's not a vtxo, find all the vtxos leaves reachable from that input
|
// if it's not a vtxo, find all the vtxos leaves reachable from that input
|
||||||
vtxosLeaves, err := congestionTree.FindLeaves(input.InputArgs.Txid, input.InputArgs.TxIndex)
|
vtxosLeaves, err := congestionTree.FindLeaves(input.GetHash().String(), input.GetIndex())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("error while finding vtxos leaves")
|
log.WithError(err).Error("error while finding vtxos leaves")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, leaf := range vtxosLeaves {
|
for _, leaf := range vtxosLeaves {
|
||||||
pset, err := psetv2.NewPsetFromBase64(leaf.Tx)
|
vtxo, err := extractVtxoOutpoint(leaf)
|
||||||
if err != nil {
|
|
||||||
log.Error(fmt.Errorf("error while decoding pset: %w", err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
vtxo, err := extractVtxoOutpoint(pset)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
continue
|
continue
|
||||||
@@ -308,7 +300,7 @@ func (s *sweeper) findSweepableOutputs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expirationTime int64
|
var expirationTime int64
|
||||||
var sweepInputs []ports.SweepInput
|
var sweepInput ports.SweepInput
|
||||||
|
|
||||||
if !isConfirmed {
|
if !isConfirmed {
|
||||||
if _, ok := blocktimeCache[node.ParentTxid]; !ok {
|
if _, ok := blocktimeCache[node.ParentTxid]; !ok {
|
||||||
@@ -320,7 +312,7 @@ func (s *sweeper) findSweepableOutputs(
|
|||||||
blocktimeCache[node.ParentTxid] = blocktime
|
blocktimeCache[node.ParentTxid] = blocktime
|
||||||
}
|
}
|
||||||
|
|
||||||
expirationTime, sweepInputs, err = s.nodeToSweepInputs(blocktimeCache[node.ParentTxid], node)
|
expirationTime, sweepInput, err = s.builder.GetSweepInput(blocktimeCache[node.ParentTxid], node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -341,7 +333,7 @@ func (s *sweeper) findSweepableOutputs(
|
|||||||
if _, ok := sweepableOutputs[expirationTime]; !ok {
|
if _, ok := sweepableOutputs[expirationTime]; !ok {
|
||||||
sweepableOutputs[expirationTime] = make([]ports.SweepInput, 0)
|
sweepableOutputs[expirationTime] = make([]ports.SweepInput, 0)
|
||||||
}
|
}
|
||||||
sweepableOutputs[expirationTime] = append(sweepableOutputs[expirationTime], sweepInputs...)
|
sweepableOutputs[expirationTime] = append(sweepableOutputs[expirationTime], sweepInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
nodesToCheck = newNodesToCheck
|
nodesToCheck = newNodesToCheck
|
||||||
@@ -350,47 +342,6 @@ func (s *sweeper) findSweepableOutputs(
|
|||||||
return sweepableOutputs, nil
|
return sweepableOutputs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sweeper) nodeToSweepInputs(parentBlocktime int64, node tree.Node) (int64, []ports.SweepInput, error) {
|
|
||||||
pset, err := psetv2.NewPsetFromBase64(node.Tx)
|
|
||||||
if err != nil {
|
|
||||||
return -1, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pset.Inputs) != 1 {
|
|
||||||
return -1, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(pset.Inputs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the tx is not onchain, it means that the input is an existing shared output
|
|
||||||
input := pset.Inputs[0]
|
|
||||||
txid := chainhash.Hash(input.PreviousTxid).String()
|
|
||||||
index := input.PreviousTxIndex
|
|
||||||
|
|
||||||
sweepLeaf, lifetime, err := extractSweepLeaf(input)
|
|
||||||
if err != nil {
|
|
||||||
return -1, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
expirationTime := parentBlocktime + lifetime
|
|
||||||
|
|
||||||
amount := uint64(0)
|
|
||||||
for _, out := range pset.Outputs {
|
|
||||||
amount += out.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
sweepInputs := []ports.SweepInput{
|
|
||||||
{
|
|
||||||
InputArgs: psetv2.InputArgs{
|
|
||||||
Txid: txid,
|
|
||||||
TxIndex: index,
|
|
||||||
},
|
|
||||||
SweepLeaf: *sweepLeaf,
|
|
||||||
Amount: amount,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return expirationTime, sweepInputs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sweeper) updateVtxoExpirationTime(
|
func (s *sweeper) updateVtxoExpirationTime(
|
||||||
tree tree.CongestionTree,
|
tree tree.CongestionTree,
|
||||||
expirationTime int64,
|
expirationTime int64,
|
||||||
@@ -399,12 +350,7 @@ func (s *sweeper) updateVtxoExpirationTime(
|
|||||||
vtxos := make([]domain.VtxoKey, 0)
|
vtxos := make([]domain.VtxoKey, 0)
|
||||||
|
|
||||||
for _, leaf := range leaves {
|
for _, leaf := range leaves {
|
||||||
pset, err := psetv2.NewPsetFromBase64(leaf.Tx)
|
vtxo, err := extractVtxoOutpoint(leaf)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
vtxo, err := extractVtxoOutpoint(pset)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -421,7 +367,7 @@ func computeSubTrees(congestionTree tree.CongestionTree, inputs []ports.SweepInp
|
|||||||
// for each sweepable input, create a sub congestion tree
|
// for each sweepable input, create a sub congestion tree
|
||||||
// it allows to skip the part of the tree that has been broadcasted in the next task
|
// it allows to skip the part of the tree that has been broadcasted in the next task
|
||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
subTree, err := computeSubTree(congestionTree, input.InputArgs.Txid)
|
subTree, err := computeSubTree(congestionTree, input.GetHash().String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("error while finding sub tree")
|
log.WithError(err).Error("error while finding sub tree")
|
||||||
continue
|
continue
|
||||||
@@ -507,40 +453,13 @@ func containsTree(tr0 tree.CongestionTree, tr1 tree.CongestionTree) (bool, error
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a congestion tree input, searches and returns the sweep leaf and its lifetime in seconds
|
|
||||||
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
|
|
||||||
for _, leaf := range input.TapLeafScript {
|
|
||||||
closure := &tree.CSVSigClosure{}
|
|
||||||
valid, err := closure.Decode(leaf.Script)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if valid && closure.Seconds > uint(lifetime) {
|
|
||||||
sweepLeaf = &leaf
|
|
||||||
lifetime = int64(closure.Seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sweepLeaf == nil {
|
|
||||||
return nil, 0, fmt.Errorf("sweep leaf not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sweepLeaf, lifetime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// assuming the pset is a leaf in the congestion tree, returns the vtxo outpoint
|
// assuming the pset is a leaf in the congestion tree, returns the vtxo outpoint
|
||||||
func extractVtxoOutpoint(pset *psetv2.Pset) (*domain.VtxoKey, error) {
|
func extractVtxoOutpoint(leaf tree.Node) (*domain.VtxoKey, error) {
|
||||||
if len(pset.Outputs) != 2 {
|
if !leaf.Leaf {
|
||||||
return nil, fmt.Errorf("invalid leaf pset, expect 2 outputs, got %d", len(pset.Outputs))
|
return nil, fmt.Errorf("node is not a leaf")
|
||||||
}
|
}
|
||||||
|
|
||||||
utx, err := pset.UnsignedTx()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &domain.VtxoKey{
|
return &domain.VtxoKey{
|
||||||
Txid: utx.TxHash().String(),
|
Txid: leaf.Txid,
|
||||||
VOut: 0,
|
VOut: 0,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type RoundStarted struct {
|
|||||||
|
|
||||||
type RoundFinalizationStarted struct {
|
type RoundFinalizationStarted struct {
|
||||||
Id string
|
Id string
|
||||||
CongestionTree tree.CongestionTree
|
CongestionTree tree.CongestionTree // BTC: signed
|
||||||
Connectors []string
|
Connectors []string
|
||||||
ConnectorAddress string
|
ConnectorAddress string
|
||||||
UnsignedForfeitTxs []string
|
UnsignedForfeitTxs []string
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/ark-network/ark/internal/core/domain"
|
"github.com/ark-network/ark/internal/core/domain"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/vulpemventures/go-elements/psetv2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SweepInput struct {
|
type SweepInput interface {
|
||||||
InputArgs psetv2.InputArgs
|
GetAmount() uint64
|
||||||
SweepLeaf psetv2.TapLeafScript
|
GetHash() chainhash.Hash
|
||||||
Amount uint64
|
GetIndex() uint32
|
||||||
|
GetLeafScript() []byte
|
||||||
|
GetControlBlock() []byte
|
||||||
|
GetInternalKey() *secp256k1.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type TxBuilder interface {
|
type TxBuilder interface {
|
||||||
@@ -18,4 +21,5 @@ type TxBuilder interface {
|
|||||||
BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64) (connectors []string, forfeitTxs []string, err error)
|
BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64) (connectors []string, forfeitTxs []string, err error)
|
||||||
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
|
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
|
||||||
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
|
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
|
||||||
|
GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/ark-network/ark/internal/core/domain"
|
"github.com/ark-network/ark/internal/core/domain"
|
||||||
"github.com/ark-network/ark/internal/core/ports"
|
"github.com/ark-network/ark/internal/core/ports"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/vulpemventures/go-elements/address"
|
"github.com/vulpemventures/go-elements/address"
|
||||||
"github.com/vulpemventures/go-elements/network"
|
"github.com/vulpemventures/go-elements/network"
|
||||||
@@ -171,6 +172,45 @@ func (b *txBuilder) BuildPoolTx(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput ports.SweepInput, err error) {
|
||||||
|
pset, err := psetv2.NewPsetFromBase64(node.Tx)
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pset.Inputs) != 1 {
|
||||||
|
return -1, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(pset.Inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the tx is not onchain, it means that the input is an existing shared output
|
||||||
|
input := pset.Inputs[0]
|
||||||
|
txid := chainhash.Hash(input.PreviousTxid).String()
|
||||||
|
index := input.PreviousTxIndex
|
||||||
|
|
||||||
|
sweepLeaf, lifetime, err := extractSweepLeaf(input)
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationTime := parentblocktime + lifetime
|
||||||
|
|
||||||
|
amount := uint64(0)
|
||||||
|
for _, out := range pset.Outputs {
|
||||||
|
amount += out.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepInput = &sweepLiquidInput{
|
||||||
|
inputArgs: psetv2.InputArgs{
|
||||||
|
Txid: txid,
|
||||||
|
TxIndex: index,
|
||||||
|
},
|
||||||
|
sweepLeaf: sweepLeaf,
|
||||||
|
amount: amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
return expirationTime, sweepInput, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *txBuilder) getLeafScriptAndTree(
|
func (b *txBuilder) getLeafScriptAndTree(
|
||||||
userPubkey, aspPubkey *secp256k1.PublicKey,
|
userPubkey, aspPubkey *secp256k1.PublicKey,
|
||||||
) ([]byte, *taproot.IndexedElementsTapScriptTree, error) {
|
) ([]byte, *taproot.IndexedElementsTapScriptTree, error) {
|
||||||
@@ -534,3 +574,55 @@ func (b *txBuilder) getConnectorAddress(poolTx string) (string, error) {
|
|||||||
|
|
||||||
return pay.WitnessPubKeyHash()
|
return pay.WitnessPubKeyHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
|
||||||
|
for _, leaf := range input.TapLeafScript {
|
||||||
|
closure := &tree.CSVSigClosure{}
|
||||||
|
valid, err := closure.Decode(leaf.Script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if valid && closure.Seconds > uint(lifetime) {
|
||||||
|
sweepLeaf = &leaf
|
||||||
|
lifetime = int64(closure.Seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sweepLeaf == nil {
|
||||||
|
return nil, 0, fmt.Errorf("sweep leaf not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sweepLeaf, lifetime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sweepLiquidInput struct {
|
||||||
|
inputArgs psetv2.InputArgs
|
||||||
|
sweepLeaf *psetv2.TapLeafScript
|
||||||
|
amount uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepLiquidInput) GetAmount() uint64 {
|
||||||
|
return s.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepLiquidInput) GetControlBlock() []byte {
|
||||||
|
ctrlBlock, _ := s.sweepLeaf.ControlBlock.ToBytes()
|
||||||
|
return ctrlBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepLiquidInput) GetHash() chainhash.Hash {
|
||||||
|
h, _ := chainhash.NewHashFromStr(s.inputArgs.Txid)
|
||||||
|
return *h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepLiquidInput) GetIndex() uint32 {
|
||||||
|
return s.inputArgs.TxIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepLiquidInput) GetInternalKey() *secp256k1.PublicKey {
|
||||||
|
return s.sweepLeaf.ControlBlock.InternalKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepLiquidInput) GetLeafScript() []byte {
|
||||||
|
return s.sweepLeaf.Script
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ func sweepTransaction(
|
|||||||
amount := uint64(0)
|
amount := uint64(0)
|
||||||
|
|
||||||
for i, input := range sweepInputs {
|
for i, input := range sweepInputs {
|
||||||
leaf := input.SweepLeaf
|
|
||||||
sweepClosure := &tree.CSVSigClosure{}
|
sweepClosure := &tree.CSVSigClosure{}
|
||||||
isSweep, err := sweepClosure.Decode(leaf.Script)
|
isSweep, err := sweepClosure.Decode(input.GetLeafScript())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -43,12 +42,27 @@ func sweepTransaction(
|
|||||||
return nil, fmt.Errorf("invalid sweep script")
|
return nil, fmt.Errorf("invalid sweep script")
|
||||||
}
|
}
|
||||||
|
|
||||||
amount += input.Amount
|
amount += input.GetAmount()
|
||||||
|
|
||||||
if err := updater.AddInputs([]psetv2.InputArgs{input.InputArgs}); err != nil {
|
if err := updater.AddInputs([]psetv2.InputArgs{
|
||||||
|
{
|
||||||
|
Txid: input.GetHash().String(),
|
||||||
|
TxIndex: input.GetIndex(),
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctrlBlock, err := taproot.ParseControlBlock(input.GetControlBlock())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
leaf := psetv2.TapLeafScript{
|
||||||
|
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(input.GetLeafScript()),
|
||||||
|
ControlBlock: *ctrlBlock,
|
||||||
|
}
|
||||||
|
|
||||||
if err := updater.AddInTapLeafScript(i, leaf); err != nil {
|
if err := updater.AddInTapLeafScript(i, leaf); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -58,7 +72,7 @@ func sweepTransaction(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := elementsutil.ValueToBytes(input.Amount)
|
value, err := elementsutil.ValueToBytes(input.GetAmount())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,735 @@
|
|||||||
|
package txbuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common/bitcointree"
|
||||||
|
"github.com/ark-network/ark/common/tree"
|
||||||
|
"github.com/ark-network/ark/internal/core/domain"
|
||||||
|
"github.com/ark-network/ark/internal/core/ports"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
connectorAmount = uint64(1000)
|
||||||
|
dustLimit = uint64(1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
type txBuilder struct {
|
||||||
|
wallet ports.WalletService
|
||||||
|
net *chaincfg.Params
|
||||||
|
roundLifetime int64 // in seconds
|
||||||
|
exitDelay int64 // in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTxBuilder(
|
||||||
|
wallet ports.WalletService, net *chaincfg.Params, roundLifetime int64, exitDelay int64,
|
||||||
|
) ports.TxBuilder {
|
||||||
|
return &txBuilder{wallet, net, roundLifetime, exitDelay}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
|
||||||
|
outputScript, _, err := b.getLeafScriptAndTree(userPubkey, aspPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return outputScript, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx string, err error) {
|
||||||
|
sweepPsbt, err := sweepTransaction(
|
||||||
|
b.wallet,
|
||||||
|
inputs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepPsbtBase64, err := sweepPsbt.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
signedSweepPsbtB64, err := b.wallet.SignPsetWithKey(ctx, sweepPsbtBase64, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPsbt, err := psbt.NewFromRawBytes(strings.NewReader(signedSweepPsbtB64), true)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range inputs {
|
||||||
|
if err := psbt.Finalize(signedPsbt, i); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := psbt.Extract(signedPsbt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if err := tx.Serialize(buf); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(buf.Bytes()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) BuildForfeitTxs(
|
||||||
|
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64,
|
||||||
|
) (connectors []string, forfeitTxs []string, err error) {
|
||||||
|
connectorPkScript, err := b.getConnectorPkScript(poolTx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorTxs, err := b.createConnectors(poolTx, payments, connectorPkScript, minRelayFee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, minRelayFee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tx := range connectorTxs {
|
||||||
|
buf, _ := tx.B64Encode()
|
||||||
|
connectors = append(connectors, buf)
|
||||||
|
}
|
||||||
|
return connectors, forfeitTxs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) BuildPoolTx(
|
||||||
|
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
|
||||||
|
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
|
||||||
|
var sharedOutputScript []byte
|
||||||
|
var sharedOutputAmount int64
|
||||||
|
|
||||||
|
var senders []*secp256k1.PublicKey
|
||||||
|
senders, err = getCosigners(payments)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cosigners := append(senders, aspPubkey)
|
||||||
|
receivers := getOffchainReceivers(payments)
|
||||||
|
|
||||||
|
if !isOnchainOnly(payments) {
|
||||||
|
sharedOutputScript, sharedOutputAmount, err = bitcointree.CraftSharedOutput(
|
||||||
|
cosigners, aspPubkey, receivers, minRelayFee, b.roundLifetime, b.exitDelay,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorAddress, err = b.wallet.DeriveConnectorAddress(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ptx, err := b.createPoolTx(
|
||||||
|
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
poolTx, err = ptx.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isOnchainOnly(payments) {
|
||||||
|
initialOutpoint := &wire.OutPoint{
|
||||||
|
Hash: ptx.UnsignedTx.TxHash(),
|
||||||
|
Index: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
congestionTree, err = bitcointree.CraftCongestionTree(
|
||||||
|
initialOutpoint, cosigners, aspPubkey, receivers, minRelayFee, b.roundLifetime, b.exitDelay,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput ports.SweepInput, err error) {
|
||||||
|
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(partialTx.Inputs) != 1 {
|
||||||
|
return -1, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(partialTx.Inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
input := partialTx.UnsignedTx.TxIn[0]
|
||||||
|
txid := input.PreviousOutPoint.Hash
|
||||||
|
index := input.PreviousOutPoint.Index
|
||||||
|
|
||||||
|
sweepLeaf, internalKey, lifetime, err := extractSweepLeaf(partialTx.Inputs[0])
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationTime := parentblocktime + lifetime
|
||||||
|
|
||||||
|
amount := int64(0)
|
||||||
|
for _, out := range partialTx.UnsignedTx.TxOut {
|
||||||
|
amount += out.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepInput = &sweepBitcoinInput{
|
||||||
|
inputArgs: wire.OutPoint{
|
||||||
|
Hash: txid,
|
||||||
|
Index: index,
|
||||||
|
},
|
||||||
|
internalPubkey: internalKey,
|
||||||
|
sweepLeaf: sweepLeaf,
|
||||||
|
amount: amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
return expirationTime, sweepInput, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) getLeafScriptAndTree(
|
||||||
|
userPubkey, aspPubkey *secp256k1.PublicKey,
|
||||||
|
) ([]byte, *txscript.IndexedTapScriptTree, error) {
|
||||||
|
redeemClosure := &bitcointree.CSVSigClosure{
|
||||||
|
Pubkey: userPubkey,
|
||||||
|
Seconds: uint(b.exitDelay),
|
||||||
|
}
|
||||||
|
|
||||||
|
redeemLeaf, err := redeemClosure.Leaf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitClosure := &bitcointree.ForfeitClosure{
|
||||||
|
Pubkey: userPubkey,
|
||||||
|
AspPubkey: aspPubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
taprootTree := txscript.AssembleTaprootScriptTree(
|
||||||
|
*redeemLeaf, *forfeitLeaf,
|
||||||
|
)
|
||||||
|
|
||||||
|
root := taprootTree.RootNode.TapHash()
|
||||||
|
unspendableKey := tree.UnspendableKey()
|
||||||
|
taprootKey := txscript.ComputeTaprootOutputKey(unspendableKey, root[:])
|
||||||
|
|
||||||
|
outputScript, err := taprootOutputScript(taprootKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputScript, taprootTree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) createPoolTx(
|
||||||
|
sharedOutputAmount int64, sharedOutputScript []byte,
|
||||||
|
payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
|
||||||
|
sweptRounds []domain.Round,
|
||||||
|
) (*psbt.Packet, error) {
|
||||||
|
aspScript, err := p2trScript(aspPubKey, b.net)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorAddr, err := btcutil.DecodeAddress(connectorAddress, b.net)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorScript, err := txscript.PayToAddrScript(connectorAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
receivers := getOnchainReceivers(payments)
|
||||||
|
nbOfInputs := countSpentVtxos(payments)
|
||||||
|
connectorsAmount := (connectorAmount + minRelayFee) * nbOfInputs
|
||||||
|
if nbOfInputs > 1 {
|
||||||
|
connectorsAmount -= minRelayFee
|
||||||
|
}
|
||||||
|
targetAmount := connectorsAmount
|
||||||
|
|
||||||
|
outputs := make([]*wire.TxOut, 0)
|
||||||
|
|
||||||
|
if sharedOutputScript != nil && sharedOutputAmount > 0 {
|
||||||
|
targetAmount += uint64(sharedOutputAmount)
|
||||||
|
|
||||||
|
outputs = append(outputs, &wire.TxOut{
|
||||||
|
Value: sharedOutputAmount,
|
||||||
|
PkScript: sharedOutputScript,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs = append(outputs, &wire.TxOut{
|
||||||
|
Value: int64(connectorAmount),
|
||||||
|
PkScript: connectorScript,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, receiver := range receivers {
|
||||||
|
targetAmount += receiver.Amount
|
||||||
|
|
||||||
|
receiverAddr, err := btcutil.DecodeAddress(receiver.OnchainAddress, b.net)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverScript, err := txscript.PayToAddrScript(receiverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs = append(outputs, &wire.TxOut{
|
||||||
|
Value: int64(receiver.Amount),
|
||||||
|
PkScript: receiverScript,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dust uint64
|
||||||
|
if change > 0 {
|
||||||
|
if change < dustLimit {
|
||||||
|
dust = change
|
||||||
|
change = 0
|
||||||
|
} else {
|
||||||
|
outputs = append(outputs, &wire.TxOut{
|
||||||
|
Value: int64(change),
|
||||||
|
PkScript: aspScript,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ins := make([]*wire.OutPoint, 0)
|
||||||
|
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
txhash, err := chainhash.NewHashFromStr(utxo.GetTxid())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ins = append(ins, &wire.OutPoint{
|
||||||
|
Hash: *txhash,
|
||||||
|
Index: utxo.GetIndex(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ptx, err := psbt.New(ins, outputs, 2, 0, []uint32{wire.MaxTxInSequenceNum})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updater, err := psbt.NewUpdater(ptx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
script, err := hex.DecodeString(utxo.GetScript())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInWitnessUtxo(&wire.TxOut{
|
||||||
|
Value: int64(utxo.GetValue()),
|
||||||
|
PkScript: script,
|
||||||
|
}, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b64, err := ptx.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
feeAmount, err := b.wallet.EstimateFees(ctx, b64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dust > feeAmount {
|
||||||
|
feeAmount = dust
|
||||||
|
} else {
|
||||||
|
feeAmount += dust
|
||||||
|
}
|
||||||
|
|
||||||
|
if dust == 0 {
|
||||||
|
if feeAmount == change {
|
||||||
|
// fees = change, remove change output
|
||||||
|
ptx.UnsignedTx.TxOut = ptx.UnsignedTx.TxOut[:len(ptx.UnsignedTx.TxOut)-1]
|
||||||
|
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
|
||||||
|
} else if feeAmount < change {
|
||||||
|
// change covers the fees, reduce change amount
|
||||||
|
ptx.UnsignedTx.TxOut[len(ptx.Outputs)-1].Value = int64(change - feeAmount)
|
||||||
|
} else {
|
||||||
|
// change is not enough to cover fees, re-select utxos
|
||||||
|
if change > 0 {
|
||||||
|
// remove change output if present
|
||||||
|
ptx.UnsignedTx.TxOut = ptx.UnsignedTx.TxOut[:len(ptx.UnsignedTx.TxOut)-1]
|
||||||
|
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
|
||||||
|
}
|
||||||
|
newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if change > 0 {
|
||||||
|
ptx.UnsignedTx.AddTxOut(&wire.TxOut{
|
||||||
|
Value: int64(change),
|
||||||
|
PkScript: aspScript,
|
||||||
|
})
|
||||||
|
ptx.Outputs = append(ptx.Outputs, psbt.POutput{})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, utxo := range newUtxos {
|
||||||
|
txhash, err := chainhash.NewHashFromStr(utxo.GetTxid())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outpoint := &wire.OutPoint{
|
||||||
|
Hash: *txhash,
|
||||||
|
Index: utxo.GetIndex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ptx.UnsignedTx.AddTxIn(wire.NewTxIn(outpoint, nil, nil))
|
||||||
|
ptx.Inputs = append(ptx.Inputs, psbt.PInput{})
|
||||||
|
|
||||||
|
scriptBytes, err := hex.DecodeString(utxo.GetScript())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInWitnessUtxo(
|
||||||
|
&wire.TxOut{
|
||||||
|
Value: int64(utxo.GetValue()),
|
||||||
|
PkScript: scriptBytes,
|
||||||
|
},
|
||||||
|
len(ptx.UnsignedTx.TxIn)-1,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else if feeAmount-dust > 0 {
|
||||||
|
newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-dust)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if change > 0 {
|
||||||
|
if change > dustLimit {
|
||||||
|
ptx.UnsignedTx.AddTxOut(&wire.TxOut{
|
||||||
|
Value: int64(change),
|
||||||
|
PkScript: aspScript,
|
||||||
|
})
|
||||||
|
ptx.Outputs = append(ptx.Outputs, psbt.POutput{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, utxo := range newUtxos {
|
||||||
|
txhash, err := chainhash.NewHashFromStr(utxo.GetTxid())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outpoint := &wire.OutPoint{
|
||||||
|
Hash: *txhash,
|
||||||
|
Index: utxo.GetIndex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ptx.UnsignedTx.AddTxIn(wire.NewTxIn(outpoint, nil, nil))
|
||||||
|
ptx.Inputs = append(ptx.Inputs, psbt.PInput{})
|
||||||
|
|
||||||
|
scriptBytes, err := hex.DecodeString(utxo.GetScript())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInWitnessUtxo(
|
||||||
|
&wire.TxOut{
|
||||||
|
Value: int64(utxo.GetValue()),
|
||||||
|
PkScript: scriptBytes,
|
||||||
|
},
|
||||||
|
len(ptx.UnsignedTx.TxIn)-1,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) createConnectors(
|
||||||
|
poolTx string, payments []domain.Payment, connectorScript []byte, minRelayFee uint64,
|
||||||
|
) ([]*psbt.Packet, error) {
|
||||||
|
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorOutput := &wire.TxOut{
|
||||||
|
PkScript: connectorScript,
|
||||||
|
Value: int64(connectorAmount),
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfConnectors := countSpentVtxos(payments)
|
||||||
|
|
||||||
|
previousInput := &wire.OutPoint{
|
||||||
|
Hash: partialTx.UnsignedTx.TxHash(),
|
||||||
|
Index: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if numberOfConnectors == 1 {
|
||||||
|
outputs := []*wire.TxOut{connectorOutput}
|
||||||
|
connectorTx, err := craftConnectorTx(previousInput, connectorScript, outputs, minRelayFee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*psbt.Packet{connectorTx}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalConnectorAmount := (connectorAmount + minRelayFee) * numberOfConnectors
|
||||||
|
if numberOfConnectors > 1 {
|
||||||
|
totalConnectorAmount -= minRelayFee
|
||||||
|
}
|
||||||
|
|
||||||
|
connectors := make([]*psbt.Packet, 0, numberOfConnectors-1)
|
||||||
|
for i := uint64(0); i < numberOfConnectors-1; i++ {
|
||||||
|
outputs := []*wire.TxOut{connectorOutput}
|
||||||
|
totalConnectorAmount -= connectorAmount
|
||||||
|
totalConnectorAmount -= minRelayFee
|
||||||
|
if totalConnectorAmount > 0 {
|
||||||
|
outputs = append(outputs, &wire.TxOut{
|
||||||
|
PkScript: connectorScript,
|
||||||
|
Value: int64(totalConnectorAmount),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
connectorTx, err := craftConnectorTx(previousInput, connectorScript, outputs, minRelayFee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
previousInput = &wire.OutPoint{
|
||||||
|
Hash: connectorTx.UnsignedTx.TxHash(),
|
||||||
|
Index: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
connectors = append(connectors, connectorTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) createForfeitTxs(
|
||||||
|
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psbt.Packet, minRelayFee uint64,
|
||||||
|
) ([]string, error) {
|
||||||
|
aspScript, err := p2trScript(aspPubkey, b.net)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxs := make([]string, 0)
|
||||||
|
for _, payment := range payments {
|
||||||
|
for _, vtxo := range payment.Inputs {
|
||||||
|
pubkeyBytes, err := hex.DecodeString(vtxo.Pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode pubkey: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vtxoPubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vtxoScript, vtxoTaprootTree, err := b.getLeafScriptAndTree(vtxoPubkey, aspPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var forfeitProof *txscript.TapscriptProof
|
||||||
|
|
||||||
|
for _, proof := range vtxoTaprootTree.LeafMerkleProofs {
|
||||||
|
isForfeit, err := (&bitcointree.ForfeitClosure{}).Decode(proof.Script)
|
||||||
|
if !isForfeit || err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitProof = &proof
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if forfeitProof == nil {
|
||||||
|
return nil, fmt.Errorf("forfeit proof not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, connector := range connectors {
|
||||||
|
txs, err := craftForfeitTxs(
|
||||||
|
connector, vtxo, vtxoScript, aspScript, minRelayFee,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxs = append(forfeitTxs, txs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return forfeitTxs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) getConnectorPkScript(poolTx string) ([]byte, error) {
|
||||||
|
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(partialTx.Outputs) < 1 {
|
||||||
|
return nil, fmt.Errorf("connector output not found in pool tx")
|
||||||
|
}
|
||||||
|
|
||||||
|
return partialTx.UnsignedTx.TxOut[0].PkScript, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) {
|
||||||
|
selectedConnectorsUtxos := make([]ports.TxInput, 0)
|
||||||
|
selectedConnectorsAmount := uint64(0)
|
||||||
|
|
||||||
|
for _, round := range sweptRounds {
|
||||||
|
if selectedConnectorsAmount >= amount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
connectors, err := b.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, connector := range connectors {
|
||||||
|
if selectedConnectorsAmount >= amount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedConnectorsUtxos = append(selectedConnectorsUtxos, connector)
|
||||||
|
selectedConnectorsAmount += connector.GetValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(selectedConnectorsUtxos) > 0 {
|
||||||
|
if err := b.wallet.LockConnectorUtxos(ctx, castToOutpoints(selectedConnectorsUtxos)); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedConnectorsAmount >= amount {
|
||||||
|
return selectedConnectorsUtxos, selectedConnectorsAmount - amount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utxos, change, err := b.wallet.SelectUtxos(ctx, "", amount-selectedConnectorsAmount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(selectedConnectorsUtxos, utxos...), change, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
|
||||||
|
outpoints := make([]ports.TxOutpoint, 0, len(inputs))
|
||||||
|
for _, input := range inputs {
|
||||||
|
outpoints = append(outpoints, input)
|
||||||
|
}
|
||||||
|
return outpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSweepLeaf(input psbt.PInput) (sweepLeaf *psbt.TaprootTapLeafScript, internalKey *secp256k1.PublicKey, lifetime int64, err error) {
|
||||||
|
for _, leaf := range input.TaprootLeafScript {
|
||||||
|
closure := &bitcointree.CSVSigClosure{}
|
||||||
|
valid, err := closure.Decode(leaf.Script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
if valid && closure.Seconds > 0 {
|
||||||
|
sweepLeaf = leaf
|
||||||
|
lifetime = int64(closure.Seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internalKey, err = schnorr.ParsePubKey(input.TaprootInternalKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if sweepLeaf == nil {
|
||||||
|
return nil, nil, 0, fmt.Errorf("sweep leaf not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sweepLeaf, internalKey, lifetime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sweepBitcoinInput struct {
|
||||||
|
inputArgs wire.OutPoint
|
||||||
|
sweepLeaf *psbt.TaprootTapLeafScript
|
||||||
|
internalPubkey *secp256k1.PublicKey
|
||||||
|
amount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepBitcoinInput) GetAmount() uint64 {
|
||||||
|
return uint64(s.amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepBitcoinInput) GetControlBlock() []byte {
|
||||||
|
return s.sweepLeaf.ControlBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepBitcoinInput) GetHash() chainhash.Hash {
|
||||||
|
return s.inputArgs.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepBitcoinInput) GetIndex() uint32 {
|
||||||
|
return s.inputArgs.Index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepBitcoinInput) GetInternalKey() *secp256k1.PublicKey {
|
||||||
|
return s.internalPubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sweepBitcoinInput) GetLeafScript() []byte {
|
||||||
|
return s.sweepLeaf.Script
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package txbuilder_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common/bitcointree"
|
||||||
|
"github.com/ark-network/ark/internal/core/domain"
|
||||||
|
"github.com/ark-network/ark/internal/core/ports"
|
||||||
|
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenantless"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
|
||||||
|
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
||||||
|
minRelayFee = uint64(30)
|
||||||
|
roundLifetime = int64(1209344)
|
||||||
|
unilateralExitDelay = int64(512)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
wallet *mockedWallet
|
||||||
|
pubkey *secp256k1.PublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
wallet = &mockedWallet{}
|
||||||
|
wallet.On("EstimateFees", mock.Anything, mock.Anything).
|
||||||
|
Return(uint64(100), nil)
|
||||||
|
wallet.On("SelectUtxos", mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(randomInput, uint64(0), nil)
|
||||||
|
wallet.On("DeriveConnectorAddress", mock.Anything).
|
||||||
|
Return(connectorAddress, nil)
|
||||||
|
|
||||||
|
pubkeyBytes, _ := hex.DecodeString(testingKey)
|
||||||
|
pubkey, _ = secp256k1.ParsePubKey(pubkeyBytes)
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildPoolTx(t *testing.T) {
|
||||||
|
builder := txbuilder.NewTxBuilder(
|
||||||
|
wallet, &chaincfg.MainNetParams, roundLifetime, unilateralExitDelay,
|
||||||
|
)
|
||||||
|
|
||||||
|
fixtures, err := parsePoolTxFixtures()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, fixtures)
|
||||||
|
|
||||||
|
if len(fixtures.Valid) > 0 {
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
for _, f := range fixtures.Valid {
|
||||||
|
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
|
||||||
|
pubkey, f.Payments, minRelayFee, []domain.Round{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, poolTx)
|
||||||
|
require.NotEmpty(t, congestionTree)
|
||||||
|
require.Equal(t, connectorAddress, connAddr)
|
||||||
|
require.Equal(t, f.ExpectedNumOfNodes, congestionTree.NumberOfNodes())
|
||||||
|
require.Len(t, congestionTree.Leaves(), f.ExpectedNumOfLeaves)
|
||||||
|
|
||||||
|
cosigners := make([]*secp256k1.PublicKey, 0)
|
||||||
|
for _, payment := range f.Payments {
|
||||||
|
for _, input := range payment.Inputs {
|
||||||
|
pubkeyBytes, err := hex.DecodeString(input.Pubkey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cosigners = append(cosigners, pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bitcointree.ValidateCongestionTree(
|
||||||
|
congestionTree, poolTx, pubkey, roundLifetime, cosigners, int64(minRelayFee),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fixtures.Invalid) > 0 {
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
for _, f := range fixtures.Invalid {
|
||||||
|
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
|
||||||
|
pubkey, f.Payments, minRelayFee, []domain.Round{},
|
||||||
|
)
|
||||||
|
require.EqualError(t, err, f.ExpectedErr)
|
||||||
|
require.Empty(t, poolTx)
|
||||||
|
require.Empty(t, connAddr)
|
||||||
|
require.Empty(t, congestionTree)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildForfeitTxs(t *testing.T) {
|
||||||
|
builder := txbuilder.NewTxBuilder(
|
||||||
|
wallet, &chaincfg.MainNetParams, 1209344, unilateralExitDelay,
|
||||||
|
)
|
||||||
|
|
||||||
|
fixtures, err := parseForfeitTxsFixtures()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, fixtures)
|
||||||
|
|
||||||
|
if len(fixtures.Valid) > 0 {
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
for _, f := range fixtures.Valid {
|
||||||
|
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
|
||||||
|
pubkey, f.PoolTx, f.Payments, minRelayFee,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, connectors, f.ExpectedNumOfConnectors)
|
||||||
|
require.Len(t, forfeitTxs, f.ExpectedNumOfForfeitTxs)
|
||||||
|
|
||||||
|
expectedInputTxid := f.PoolTxid
|
||||||
|
// Verify the chain of connectors
|
||||||
|
for _, connector := range connectors {
|
||||||
|
tx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, tx)
|
||||||
|
|
||||||
|
require.Len(t, tx.Inputs, 1)
|
||||||
|
require.Len(t, tx.Outputs, 2)
|
||||||
|
|
||||||
|
inputTxid := tx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
|
||||||
|
require.Equal(t, expectedInputTxid, inputTxid)
|
||||||
|
require.Equal(t, 1, int(tx.UnsignedTx.TxIn[0].PreviousOutPoint.Index))
|
||||||
|
|
||||||
|
expectedInputTxid = tx.UnsignedTx.TxHash().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode and check forfeit txs
|
||||||
|
for _, forfeitTx := range forfeitTxs {
|
||||||
|
tx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, tx.Inputs, 2)
|
||||||
|
require.Len(t, tx.Outputs, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fixtures.Invalid) > 0 {
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
for _, f := range fixtures.Invalid {
|
||||||
|
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
|
||||||
|
pubkey, f.PoolTx, f.Payments, minRelayFee,
|
||||||
|
)
|
||||||
|
require.EqualError(t, err, f.ExpectedErr)
|
||||||
|
require.Empty(t, connectors)
|
||||||
|
require.Empty(t, forfeitTxs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomInput() []ports.TxInput {
|
||||||
|
txid := randomHex(32)
|
||||||
|
input := &mockedInput{}
|
||||||
|
input.On("GetAsset").Return("5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225")
|
||||||
|
input.On("GetValue").Return(uint64(1000))
|
||||||
|
input.On("GetScript").Return("a914ea9f486e82efb3dd83a69fd96e3f0113757da03c87")
|
||||||
|
input.On("GetTxid").Return(txid)
|
||||||
|
input.On("GetIndex").Return(uint32(0))
|
||||||
|
|
||||||
|
return []ports.TxInput{input}
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomHex(len int) string {
|
||||||
|
buf := make([]byte, len)
|
||||||
|
// nolint
|
||||||
|
rand.Read(buf)
|
||||||
|
return hex.EncodeToString(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
type poolTxFixtures struct {
|
||||||
|
Valid []struct {
|
||||||
|
Payments []domain.Payment
|
||||||
|
ExpectedNumOfNodes int
|
||||||
|
ExpectedNumOfLeaves int
|
||||||
|
}
|
||||||
|
Invalid []struct {
|
||||||
|
Payments []domain.Payment
|
||||||
|
ExpectedErr string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePoolTxFixtures() (*poolTxFixtures, error) {
|
||||||
|
file, err := os.ReadFile("testdata/fixtures.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
if err := json.Unmarshal(file, &v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vv := v["buildPoolTx"].(map[string]interface{})
|
||||||
|
file, _ = json.Marshal(vv)
|
||||||
|
var fixtures poolTxFixtures
|
||||||
|
if err := json.Unmarshal(file, &fixtures); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fixtures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type forfeitTxsFixtures struct {
|
||||||
|
Valid []struct {
|
||||||
|
Payments []domain.Payment
|
||||||
|
ExpectedNumOfConnectors int
|
||||||
|
ExpectedNumOfForfeitTxs int
|
||||||
|
PoolTx string
|
||||||
|
PoolTxid string
|
||||||
|
}
|
||||||
|
Invalid []struct {
|
||||||
|
Payments []domain.Payment
|
||||||
|
ExpectedErr string
|
||||||
|
PoolTx string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) {
|
||||||
|
file, err := os.ReadFile("testdata/fixtures.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
if err := json.Unmarshal(file, &v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vv := v["buildForfeitTxs"].(map[string]interface{})
|
||||||
|
file, _ = json.Marshal(vv)
|
||||||
|
var fixtures forfeitTxsFixtures
|
||||||
|
if err := json.Unmarshal(file, &fixtures); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fixtures, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package txbuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func craftConnectorTx(
|
||||||
|
input *wire.OutPoint, inputScript []byte, outputs []*wire.TxOut, feeAmount uint64,
|
||||||
|
) (*psbt.Packet, error) {
|
||||||
|
ptx, err := psbt.New(
|
||||||
|
[]*wire.OutPoint{input},
|
||||||
|
outputs,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
[]uint32{wire.MaxTxInSequenceNum},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updater, err := psbt.NewUpdater(ptx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputAmount := int64(feeAmount)
|
||||||
|
for _, output := range outputs {
|
||||||
|
inputAmount += output.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInWitnessUtxo(&wire.TxOut{
|
||||||
|
Value: inputAmount,
|
||||||
|
PkScript: inputScript,
|
||||||
|
}, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConnectorInputs(partialTx *psbt.Packet) ([]*wire.OutPoint, []*wire.TxOut) {
|
||||||
|
inputs := make([]*wire.OutPoint, 0)
|
||||||
|
witnessUtxos := make([]*wire.TxOut, 0)
|
||||||
|
|
||||||
|
for i, output := range partialTx.UnsignedTx.TxOut {
|
||||||
|
if output.Value == int64(connectorAmount) {
|
||||||
|
inputs = append(inputs, &wire.OutPoint{
|
||||||
|
Hash: partialTx.UnsignedTx.TxHash(),
|
||||||
|
Index: uint32(i),
|
||||||
|
})
|
||||||
|
witnessUtxos = append(witnessUtxos, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs, witnessUtxos
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package txbuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ark-network/ark/internal/core/domain"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func craftForfeitTxs(
|
||||||
|
connectorTx *psbt.Packet,
|
||||||
|
vtxo domain.Vtxo,
|
||||||
|
vtxoScript, aspScript []byte,
|
||||||
|
minRelayFee uint64,
|
||||||
|
) (forfeitTxs []string, err error) {
|
||||||
|
connectors, prevouts := getConnectorInputs(connectorTx)
|
||||||
|
|
||||||
|
for i, connectorInput := range connectors {
|
||||||
|
connectorPrevout := prevouts[i]
|
||||||
|
|
||||||
|
vtxoHash, err := chainhash.NewHashFromStr(vtxo.Txid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vtxoInput := &wire.OutPoint{
|
||||||
|
Hash: *vtxoHash,
|
||||||
|
Index: vtxo.VOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
partialTx, err := psbt.New(
|
||||||
|
[]*wire.OutPoint{connectorInput, vtxoInput},
|
||||||
|
[]*wire.TxOut{{
|
||||||
|
Value: int64(vtxo.Amount) + int64(connectorAmount) - int64(minRelayFee),
|
||||||
|
PkScript: aspScript,
|
||||||
|
}},
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
[]uint32{wire.MaxTxInSequenceNum, wire.MaxTxInSequenceNum},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updater, err := psbt.NewUpdater(partialTx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInWitnessUtxo(connectorPrevout, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInWitnessUtxo(&wire.TxOut{
|
||||||
|
Value: int64(vtxo.Amount),
|
||||||
|
PkScript: vtxoScript,
|
||||||
|
}, 1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInSighashType(txscript.SigHashDefault, 1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := partialTx.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxs = append(forfeitTxs, tx)
|
||||||
|
}
|
||||||
|
return forfeitTxs, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package txbuilder_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/internal/core/ports"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockedWallet struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// BroadcastTransaction implements ports.WalletService.
|
||||||
|
func (m *mockedWallet) BroadcastTransaction(ctx context.Context, txHex string) (string, error) {
|
||||||
|
args := m.Called(ctx, txHex)
|
||||||
|
|
||||||
|
var res string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(string)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements ports.WalletService.
|
||||||
|
func (m *mockedWallet) Close() {
|
||||||
|
m.Called()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveAddresses implements ports.WalletService.
|
||||||
|
func (m *mockedWallet) DeriveAddresses(ctx context.Context, num int) ([]string, error) {
|
||||||
|
args := m.Called(ctx, num)
|
||||||
|
|
||||||
|
var res []string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.([]string)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveConnectorAddress implements ports.WalletService.
|
||||||
|
func (m *mockedWallet) DeriveConnectorAddress(ctx context.Context) (string, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
|
||||||
|
var res string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(string)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPubkey implements ports.WalletService.
|
||||||
|
func (m *mockedWallet) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
|
||||||
|
var res *secp256k1.PublicKey
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(*secp256k1.PublicKey)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignPset implements ports.WalletService.
|
||||||
|
func (m *mockedWallet) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) {
|
||||||
|
args := m.Called(ctx, pset, extractRawTx)
|
||||||
|
|
||||||
|
var res string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(string)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status implements ports.WalletService.
|
||||||
|
func (m *mockedWallet) Status(ctx context.Context) (ports.WalletStatus, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
|
||||||
|
var res ports.WalletStatus
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(ports.WalletStatus)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) {
|
||||||
|
args := m.Called(ctx, asset, amount)
|
||||||
|
|
||||||
|
var res0 func() []ports.TxInput
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res0 = a.(func() []ports.TxInput)
|
||||||
|
}
|
||||||
|
var res1 uint64
|
||||||
|
if a := args.Get(1); a != nil {
|
||||||
|
res1 = a.(uint64)
|
||||||
|
}
|
||||||
|
return res0(), res1, args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) EstimateFees(ctx context.Context, pset string) (uint64, error) {
|
||||||
|
args := m.Called(ctx, pset)
|
||||||
|
|
||||||
|
var res uint64
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(uint64)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) IsTransactionConfirmed(ctx context.Context, txid string) (bool, int64, error) {
|
||||||
|
args := m.Called(ctx, txid)
|
||||||
|
|
||||||
|
var res bool
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocktime int64
|
||||||
|
if b := args.Get(1); b != nil {
|
||||||
|
blocktime = b.(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, blocktime, args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) SignPsetWithKey(ctx context.Context, pset string, inputIndexes []int) (string, error) {
|
||||||
|
args := m.Called(ctx, pset, inputIndexes)
|
||||||
|
|
||||||
|
var res string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(string)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) WatchScripts(
|
||||||
|
ctx context.Context, scripts []string,
|
||||||
|
) error {
|
||||||
|
args := m.Called(ctx, scripts)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) UnwatchScripts(
|
||||||
|
ctx context.Context, scripts []string,
|
||||||
|
) error {
|
||||||
|
args := m.Called(ctx, scripts)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string]ports.VtxoWithValue {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
|
||||||
|
var res <-chan map[string]ports.VtxoWithValue
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(<-chan map[string]ports.VtxoWithValue)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) ListConnectorUtxos(ctx context.Context, addr string) ([]ports.TxInput, error) {
|
||||||
|
args := m.Called(ctx, addr)
|
||||||
|
|
||||||
|
var res []ports.TxInput
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.([]ports.TxInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoint) error {
|
||||||
|
args := m.Called(ctx, utxos)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) WaitForSync(ctx context.Context, txid string) error {
|
||||||
|
args := m.Called(ctx, txid)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockedInput struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedInput) GetTxid() string {
|
||||||
|
args := m.Called()
|
||||||
|
|
||||||
|
var res string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedInput) GetIndex() uint32 {
|
||||||
|
args := m.Called()
|
||||||
|
|
||||||
|
var res uint32
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(uint32)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedInput) GetScript() string {
|
||||||
|
args := m.Called()
|
||||||
|
|
||||||
|
var res string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(string)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedInput) GetAsset() string {
|
||||||
|
args := m.Called()
|
||||||
|
|
||||||
|
var res string
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(string)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedInput) GetValue() uint64 {
|
||||||
|
args := m.Called()
|
||||||
|
|
||||||
|
var res uint64
|
||||||
|
if a := args.Get(0); a != nil {
|
||||||
|
res = a.(uint64)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
146
server/internal/infrastructure/tx-builder/covenantless/sweep.go
Normal file
146
server/internal/infrastructure/tx-builder/covenantless/sweep.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package txbuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
|
"github.com/ark-network/ark/common/bitcointree"
|
||||||
|
"github.com/ark-network/ark/internal/core/ports"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sweepTransaction(
|
||||||
|
wallet ports.WalletService,
|
||||||
|
sweepInputs []ports.SweepInput,
|
||||||
|
) (*psbt.Packet, error) {
|
||||||
|
ins := make([]*wire.OutPoint, 0)
|
||||||
|
sequences := make([]uint32, 0)
|
||||||
|
|
||||||
|
for _, input := range sweepInputs {
|
||||||
|
ins = append(ins, &wire.OutPoint{
|
||||||
|
Hash: input.GetHash(),
|
||||||
|
Index: input.GetIndex(),
|
||||||
|
})
|
||||||
|
|
||||||
|
sweepClosure := bitcointree.CSVSigClosure{}
|
||||||
|
valid, err := sweepClosure.Decode(input.GetLeafScript())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return nil, fmt.Errorf("invalid csv script")
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence, err := common.BIP68EncodeAsNumber(sweepClosure.Seconds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sequences = append(sequences, sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepPartialTx, err := psbt.New(
|
||||||
|
ins,
|
||||||
|
nil,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
sequences,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updater, err := psbt.NewUpdater(sweepPartialTx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amount := int64(0)
|
||||||
|
|
||||||
|
for i, sweepInput := range sweepInputs {
|
||||||
|
sweepPartialTx.Inputs[i].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
|
||||||
|
{
|
||||||
|
ControlBlock: sweepInput.GetControlBlock(),
|
||||||
|
Script: sweepInput.GetLeafScript(),
|
||||||
|
LeafVersion: txscript.BaseLeafVersion,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepPartialTx.Inputs[i].TaprootInternalKey = schnorr.SerializePubKey(sweepInput.GetInternalKey())
|
||||||
|
|
||||||
|
amount += int64(sweepInput.GetAmount())
|
||||||
|
|
||||||
|
ctrlBlock, err := txscript.ParseControlBlock(sweepInput.GetControlBlock())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
root := ctrlBlock.RootHash(sweepInput.GetLeafScript())
|
||||||
|
|
||||||
|
prevoutTaprootKey := txscript.ComputeTaprootOutputKey(
|
||||||
|
sweepInput.GetInternalKey(),
|
||||||
|
root,
|
||||||
|
)
|
||||||
|
|
||||||
|
script, err := taprootOutputScript(prevoutTaprootKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevout := &wire.TxOut{
|
||||||
|
Value: int64(sweepInput.GetAmount()),
|
||||||
|
PkScript: script,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updater.AddInWitnessUtxo(prevout, i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sweepAddress, err := wallet.DeriveAddresses(ctx, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := btcutil.DecodeAddress(sweepAddress[0], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := txscript.PayToAddrScript(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepPartialTx.UnsignedTx.AddTxOut(&wire.TxOut{
|
||||||
|
Value: amount,
|
||||||
|
PkScript: script,
|
||||||
|
})
|
||||||
|
sweepPartialTx.Outputs = append(sweepPartialTx.Outputs, psbt.POutput{})
|
||||||
|
|
||||||
|
b64, err := sweepPartialTx.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fees, err := wallet.EstimateFees(ctx, b64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount < int64(fees) {
|
||||||
|
return nil, fmt.Errorf("insufficient funds (%d) to cover fees (%d) for sweep transaction", amount, fees)
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepPartialTx.UnsignedTx.TxOut[0].Value = amount - int64(fees)
|
||||||
|
|
||||||
|
return sweepPartialTx, nil
|
||||||
|
}
|
||||||
259
server/internal/infrastructure/tx-builder/covenantless/testdata/fixtures.json
vendored
Normal file
259
server/internal/infrastructure/tx-builder/covenantless/testdata/fixtures.json
vendored
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
{
|
||||||
|
"buildPoolTx": {
|
||||||
|
"valid": [
|
||||||
|
{
|
||||||
|
"payments": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expectedNumOfNodes": 1,
|
||||||
|
"expectedNumOfLeaves": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"payments": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expectedNumOfNodes": 3,
|
||||||
|
"expectedNumOfLeaves": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"payments": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 1100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expectedNumOfNodes": 11,
|
||||||
|
"expectedNumOfLeaves": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"payments": [
|
||||||
|
{
|
||||||
|
"id": "a242cdd8-f3d5-46c0-ae98-94135a2bee3f",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
|
||||||
|
"vout": 1,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||||
|
"vout": 1,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expectedNumOfNodes": 9,
|
||||||
|
"expectedNumOfLeaves": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"invalid": []
|
||||||
|
},
|
||||||
|
"buildForfeitTxs": {
|
||||||
|
"valid": [
|
||||||
|
{
|
||||||
|
"payments": [
|
||||||
|
{
|
||||||
|
"id": "a242cdd8-f3d5-46c0-ae98-94135a2bee3f",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
|
||||||
|
"vout": 1,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||||
|
"vout": 0,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||||
|
"vout": 1,
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"poolTx": "cHNidP8BALICAAAAAnonOnsJBkHUUaKf/7fdS0/sVyBCgDPusYzGSZZiXPbtAAAAAAD/////VLtr81ZII3QJnXgrIwgcnbsq3aa4L3qdHOAn2evlFtEAAAAAAP////8CohIAAAAAAAAiUSBZarBUuSIHnlkuIoel9MmvexqTGK8jCZaRjt8L+Pb3s+gDAAAAAAAAIlEgI95L4kHEn2fAA+vysD+RIR4eD3AIQwc+FyCInJ8HivYAAAAAAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAAA",
|
||||||
|
"poolTxid": "7c0c10756cdb9ab8e605f1c82e25989761308cf4c60e6a6f42b72d46144c4ce0",
|
||||||
|
"expectedNumOfForfeitTxs": 25,
|
||||||
|
"expectedNumOfConnectors": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"invalid": []
|
||||||
|
}
|
||||||
|
}
|
||||||
106
server/internal/infrastructure/tx-builder/covenantless/utils.go
Normal file
106
server/internal/infrastructure/tx-builder/covenantless/utils.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package txbuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common/bitcointree"
|
||||||
|
"github.com/ark-network/ark/internal/core/domain"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func p2trScript(publicKey *secp256k1.PublicKey, net *chaincfg.Params) ([]byte, error) {
|
||||||
|
tapKey := txscript.ComputeTaprootKeyNoScript(publicKey)
|
||||||
|
|
||||||
|
payment, err := btcutil.NewAddressTaproot(
|
||||||
|
schnorr.SerializePubKey(tapKey),
|
||||||
|
net,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return payment.ScriptAddress(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOnchainReceivers(
|
||||||
|
payments []domain.Payment,
|
||||||
|
) []domain.Receiver {
|
||||||
|
receivers := make([]domain.Receiver, 0)
|
||||||
|
for _, payment := range payments {
|
||||||
|
for _, receiver := range payment.Receivers {
|
||||||
|
if receiver.IsOnchain() {
|
||||||
|
receivers = append(receivers, receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return receivers
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use ephemeral keys ?
|
||||||
|
func getCosigners(
|
||||||
|
payments []domain.Payment,
|
||||||
|
) ([]*secp256k1.PublicKey, error) {
|
||||||
|
cosigners := make([]*secp256k1.PublicKey, 0)
|
||||||
|
|
||||||
|
for _, payment := range payments {
|
||||||
|
for _, input := range payment.Inputs {
|
||||||
|
pubkeyBytes, err := hex.DecodeString(input.Pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cosigners = append(cosigners, pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cosigners, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOffchainReceivers(
|
||||||
|
payments []domain.Payment,
|
||||||
|
) []bitcointree.Receiver {
|
||||||
|
receivers := make([]bitcointree.Receiver, 0)
|
||||||
|
for _, payment := range payments {
|
||||||
|
for _, receiver := range payment.Receivers {
|
||||||
|
if !receiver.IsOnchain() {
|
||||||
|
receivers = append(receivers, bitcointree.Receiver{
|
||||||
|
Pubkey: receiver.Pubkey,
|
||||||
|
Amount: receiver.Amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return receivers
|
||||||
|
}
|
||||||
|
|
||||||
|
func countSpentVtxos(payments []domain.Payment) uint64 {
|
||||||
|
var sum uint64
|
||||||
|
for _, payment := range payments {
|
||||||
|
sum += uint64(len(payment.Inputs))
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
|
||||||
|
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOnchainOnly(payments []domain.Payment) bool {
|
||||||
|
for _, p := range payments {
|
||||||
|
for _, r := range p.Receivers {
|
||||||
|
if !r.IsOnchain() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user