mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 04:04: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/sys v0.20.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/rpc 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-20240528184218-531527333157 // indirect
|
||||
google.golang.org/grpc v1.64.0
|
||||
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/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=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||
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-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||
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/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
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/btcec/v2 v2.3.2
|
||||
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/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.4 // indirect
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
|
||||
|
||||
@@ -23,6 +23,7 @@ service ArkService {
|
||||
body: "*"
|
||||
};
|
||||
};
|
||||
// TODO BTC: signTree rpc
|
||||
rpc GetRound(GetRoundRequest) returns (GetRoundResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v1/round/{txid}"
|
||||
@@ -94,6 +95,7 @@ message GetRoundResponse {
|
||||
message GetEventStreamRequest {}
|
||||
message GetEventStreamResponse {
|
||||
oneof event {
|
||||
// TODO: BTC add "signTree" event
|
||||
RoundFinalizationEvent round_finalization = 1;
|
||||
RoundFinalizedEvent round_finalized = 2;
|
||||
RoundFailed round_failed = 3;
|
||||
@@ -104,6 +106,7 @@ message PingRequest {
|
||||
string payment_id = 1;
|
||||
}
|
||||
message PingResponse {
|
||||
// TODO: improve this response (returns oneof the round event)
|
||||
repeated string forfeit_txs = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/timshannon/badgerhold/v4 v4.0.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/protobuf v1.34.1
|
||||
)
|
||||
@@ -34,7 +34,7 @@ require (
|
||||
github.com/btcsuite/btcd v0.24.0
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||
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/btclog v0.0.0-20170628155309-84c8d2346e9f // 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/multierr v1.11.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/sys v0.20.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/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/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-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck=
|
||||
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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
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-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||
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-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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/internal/core/domain"
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
// sweeper is an unexported service running while the main application service is started
|
||||
@@ -158,8 +156,8 @@ func (s *sweeper) createTask(
|
||||
ctx,
|
||||
[]domain.VtxoKey{
|
||||
{
|
||||
Txid: input.InputArgs.Txid,
|
||||
VOut: input.InputArgs.TxIndex,
|
||||
Txid: input.GetHash().String(),
|
||||
VOut: input.GetIndex(),
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -169,20 +167,14 @@ func (s *sweeper) createTask(
|
||||
}
|
||||
} else {
|
||||
// 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 {
|
||||
log.WithError(err).Error("error while finding vtxos leaves")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, leaf := range vtxosLeaves {
|
||||
pset, err := psetv2.NewPsetFromBase64(leaf.Tx)
|
||||
if err != nil {
|
||||
log.Error(fmt.Errorf("error while decoding pset: %w", err))
|
||||
continue
|
||||
}
|
||||
|
||||
vtxo, err := extractVtxoOutpoint(pset)
|
||||
vtxo, err := extractVtxoOutpoint(leaf)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
@@ -308,7 +300,7 @@ func (s *sweeper) findSweepableOutputs(
|
||||
}
|
||||
|
||||
var expirationTime int64
|
||||
var sweepInputs []ports.SweepInput
|
||||
var sweepInput ports.SweepInput
|
||||
|
||||
if !isConfirmed {
|
||||
if _, ok := blocktimeCache[node.ParentTxid]; !ok {
|
||||
@@ -320,7 +312,7 @@ func (s *sweeper) findSweepableOutputs(
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -341,7 +333,7 @@ func (s *sweeper) findSweepableOutputs(
|
||||
if _, ok := sweepableOutputs[expirationTime]; !ok {
|
||||
sweepableOutputs[expirationTime] = make([]ports.SweepInput, 0)
|
||||
}
|
||||
sweepableOutputs[expirationTime] = append(sweepableOutputs[expirationTime], sweepInputs...)
|
||||
sweepableOutputs[expirationTime] = append(sweepableOutputs[expirationTime], sweepInput)
|
||||
}
|
||||
|
||||
nodesToCheck = newNodesToCheck
|
||||
@@ -350,47 +342,6 @@ func (s *sweeper) findSweepableOutputs(
|
||||
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(
|
||||
tree tree.CongestionTree,
|
||||
expirationTime int64,
|
||||
@@ -399,12 +350,7 @@ func (s *sweeper) updateVtxoExpirationTime(
|
||||
vtxos := make([]domain.VtxoKey, 0)
|
||||
|
||||
for _, leaf := range leaves {
|
||||
pset, err := psetv2.NewPsetFromBase64(leaf.Tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtxo, err := extractVtxoOutpoint(pset)
|
||||
vtxo, err := extractVtxoOutpoint(leaf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -421,7 +367,7 @@ func computeSubTrees(congestionTree tree.CongestionTree, inputs []ports.SweepInp
|
||||
// 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
|
||||
for _, input := range inputs {
|
||||
subTree, err := computeSubTree(congestionTree, input.InputArgs.Txid)
|
||||
subTree, err := computeSubTree(congestionTree, input.GetHash().String())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while finding sub tree")
|
||||
continue
|
||||
@@ -507,40 +453,13 @@ func containsTree(tr0 tree.CongestionTree, tr1 tree.CongestionTree) (bool, error
|
||||
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
|
||||
func extractVtxoOutpoint(pset *psetv2.Pset) (*domain.VtxoKey, error) {
|
||||
if len(pset.Outputs) != 2 {
|
||||
return nil, fmt.Errorf("invalid leaf pset, expect 2 outputs, got %d", len(pset.Outputs))
|
||||
func extractVtxoOutpoint(leaf tree.Node) (*domain.VtxoKey, error) {
|
||||
if !leaf.Leaf {
|
||||
return nil, fmt.Errorf("node is not a leaf")
|
||||
}
|
||||
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.VtxoKey{
|
||||
Txid: utx.TxHash().String(),
|
||||
Txid: leaf.Txid,
|
||||
VOut: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type RoundStarted struct {
|
||||
|
||||
type RoundFinalizationStarted struct {
|
||||
Id string
|
||||
CongestionTree tree.CongestionTree
|
||||
CongestionTree tree.CongestionTree // BTC: signed
|
||||
Connectors []string
|
||||
ConnectorAddress string
|
||||
UnsignedForfeitTxs []string
|
||||
|
||||
@@ -3,14 +3,17 @@ package ports
|
||||
import (
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
type SweepInput struct {
|
||||
InputArgs psetv2.InputArgs
|
||||
SweepLeaf psetv2.TapLeafScript
|
||||
Amount uint64
|
||||
type SweepInput interface {
|
||||
GetAmount() uint64
|
||||
GetHash() chainhash.Hash
|
||||
GetIndex() uint32
|
||||
GetLeafScript() []byte
|
||||
GetControlBlock() []byte
|
||||
GetInternalKey() *secp256k1.PublicKey
|
||||
}
|
||||
|
||||
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)
|
||||
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err 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/internal/core/domain"
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
@@ -171,6 +172,45 @@ func (b *txBuilder) BuildPoolTx(
|
||||
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(
|
||||
userPubkey, aspPubkey *secp256k1.PublicKey,
|
||||
) ([]byte, *taproot.IndexedElementsTapScriptTree, error) {
|
||||
@@ -534,3 +574,55 @@ func (b *txBuilder) getConnectorAddress(poolTx string) (string, error) {
|
||||
|
||||
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)
|
||||
|
||||
for i, input := range sweepInputs {
|
||||
leaf := input.SweepLeaf
|
||||
sweepClosure := &tree.CSVSigClosure{}
|
||||
isSweep, err := sweepClosure.Decode(leaf.Script)
|
||||
isSweep, err := sweepClosure.Decode(input.GetLeafScript())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -43,12 +42,27 @@ func sweepTransaction(
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -58,7 +72,7 @@ func sweepTransaction(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := elementsutil.ValueToBytes(input.Amount)
|
||||
value, err := elementsutil.ValueToBytes(input.GetAmount())
|
||||
if err != nil {
|
||||
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