mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 12:14:21 +01:00
* scaffolding wallet * remove wallet db, add loader instead * wip * implement some wallet methods * signing and utxos * renaming * fee estimator * chain source options * config * application service * clark docker-compose * CLI refactor * v0 clark * v0.1 clark * fix SignTapscriptInput (btcwallet) * wallet.Broadcast, send via explora * fix ASP pubkey * Use lnd's btcwallet & Add rpc to get wallet staus * wip * unilateral exit * Fixes on watching for notifications and cli init * handle non-final BIP68 errors * Fixes * Fixes * Fix * a * fix onboard cosigners + revert tree validation * fix covenant e2e tests * fix covenantless e2e tests * fix container naming * fix lint error * update REAME.md * Add env var for wallet password --------- Co-authored-by: altafan <18440657+altafan@users.noreply.github.com>
387 lines
8.9 KiB
Go
387 lines
8.9 KiB
Go
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, cosigners, 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, cosigners, 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, cosigners)
|
|
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
|
|
cosigners []*secp256k1.PublicKey
|
|
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,
|
|
cosigners []*secp256k1.PublicKey,
|
|
) (tree.Node, error) {
|
|
partialTx, err := getTx(n, input, inputTapInternalKey, inputSweepTapLeaf, cosigners)
|
|
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,
|
|
cosigners []*secp256k1.PublicKey,
|
|
) (*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}
|
|
|
|
for _, cosigner := range cosigners {
|
|
if err := AddCosignerKey(0, tx, cosigner); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return tx, nil
|
|
}
|
|
|
|
func createRootNode(
|
|
aggregatedKey *musig2.AggregateKey, cosigners []*secp256k1.PublicKey,
|
|
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, cosigners, 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, cosigners []*secp256k1.PublicKey, feeAmount int64) ([]node, error) {
|
|
if len(nodes)%2 != 0 {
|
|
last := nodes[len(nodes)-1]
|
|
pairs, err := createUpperLevel(nodes[:len(nodes)-1], aggregatedKey, cosigners, 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,
|
|
cosigners: cosigners,
|
|
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()
|
|
}
|