mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 04:34:19 +01:00
194 lines
4.3 KiB
Go
194 lines
4.3 KiB
Go
package redemption
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ark-network/ark/common/bitcointree"
|
|
"github.com/ark-network/ark/common/tree"
|
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
)
|
|
|
|
type CovenantlessRedeemBranch struct {
|
|
vtxo client.Vtxo
|
|
branch []*psbt.Packet
|
|
lifetime time.Duration
|
|
explorer explorer.Explorer
|
|
}
|
|
|
|
func NewCovenantlessRedeemBranch(
|
|
explorer explorer.Explorer,
|
|
congestionTree tree.CongestionTree, vtxo client.Vtxo,
|
|
) (*CovenantlessRedeemBranch, error) {
|
|
_, seconds, err := findCovenantlessSweepClosure(congestionTree)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodes, err := congestionTree.Branch(vtxo.Txid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
branch := make([]*psbt.Packet, 0, len(nodes))
|
|
for _, node := range nodes {
|
|
ptx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
branch = append(branch, ptx)
|
|
}
|
|
|
|
return &CovenantlessRedeemBranch{
|
|
vtxo: vtxo,
|
|
branch: branch,
|
|
lifetime: lifetime,
|
|
explorer: explorer,
|
|
}, nil
|
|
}
|
|
|
|
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
|
|
func (r *CovenantlessRedeemBranch) RedeemPath() ([]string, error) {
|
|
transactions := make([]string, 0, len(r.branch))
|
|
|
|
offchainPath, err := r.OffchainPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ptx := range offchainPath {
|
|
firstInput := ptx.Inputs[0]
|
|
if len(firstInput.TaprootKeySpendSig) == 0 {
|
|
return nil, fmt.Errorf("missing taproot key spend signature")
|
|
}
|
|
|
|
var witness bytes.Buffer
|
|
|
|
if err := psbt.WriteTxWitness(&witness, [][]byte{firstInput.TaprootKeySpendSig}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ptx.Inputs[0].FinalScriptWitness = witness.Bytes()
|
|
|
|
extracted, err := psbt.Extract(ptx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var txBytes bytes.Buffer
|
|
|
|
if err := extracted.Serialize(&txBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
transactions = append(transactions, hex.EncodeToString(txBytes.Bytes()))
|
|
}
|
|
|
|
return transactions, nil
|
|
}
|
|
|
|
func (r *CovenantlessRedeemBranch) ExpiresAt() (*time.Time, error) {
|
|
lastKnownBlocktime := int64(0)
|
|
|
|
confirmed, blocktime, _ := r.explorer.GetTxBlockTime(r.vtxo.RoundTxid)
|
|
|
|
if confirmed {
|
|
lastKnownBlocktime = blocktime
|
|
} else {
|
|
expirationFromNow := time.Now().Add(time.Minute).Add(r.lifetime)
|
|
return &expirationFromNow, nil
|
|
}
|
|
|
|
for _, ptx := range r.branch {
|
|
txid := ptx.UnsignedTx.TxHash().String()
|
|
|
|
confirmed, blocktime, err := r.explorer.GetTxBlockTime(txid)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
if confirmed {
|
|
lastKnownBlocktime = blocktime
|
|
continue
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
t := time.Unix(lastKnownBlocktime, 0).Add(r.lifetime)
|
|
return &t, nil
|
|
}
|
|
|
|
// offchainPath checks for transactions of the branch onchain and returns only the offchain part
|
|
func (r *CovenantlessRedeemBranch) OffchainPath() ([]*psbt.Packet, error) {
|
|
offchainPath := append([]*psbt.Packet{}, r.branch...)
|
|
|
|
for i := len(r.branch) - 1; i >= 0; i-- {
|
|
ptx := r.branch[i]
|
|
txHash := ptx.UnsignedTx.TxHash().String()
|
|
|
|
if _, err := r.explorer.GetTxHex(txHash); err != nil {
|
|
continue
|
|
}
|
|
|
|
// if no error, the tx exists onchain, so we can remove it (+ the parents) from the branch
|
|
if i == len(r.branch)-1 {
|
|
offchainPath = []*psbt.Packet{}
|
|
} else {
|
|
offchainPath = r.branch[i+1:]
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
return offchainPath, nil
|
|
}
|
|
|
|
func findCovenantlessSweepClosure(
|
|
congestionTree tree.CongestionTree,
|
|
) (*txscript.TapLeaf, uint, error) {
|
|
root, err := congestionTree.Root()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// find the sweep closure
|
|
tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var seconds uint
|
|
var sweepClosure *txscript.TapLeaf
|
|
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
|
|
closure := &bitcointree.CSVSigClosure{}
|
|
valid, err := closure.Decode(tapLeaf.Script)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if valid && closure.Seconds > seconds {
|
|
seconds = closure.Seconds
|
|
leaf := txscript.NewBaseTapLeaf(tapLeaf.Script)
|
|
sweepClosure = &leaf
|
|
}
|
|
}
|
|
|
|
if sweepClosure == nil {
|
|
return nil, 0, fmt.Errorf("sweep closure not found")
|
|
}
|
|
|
|
return sweepClosure, seconds, nil
|
|
}
|