Files
ark/asp/internal/infrastructure/tx-builder/covenant/tree.go
Louis Singer cf78fc1ab3 Add support for unilateral exit (#79)
* v0 unilateral redemption

* add fee outputs to congestion tree

* unilateral exit

* rework unilateral exit verbosity

* substract fee from vtxo amount

* remove unused functions and variables

* fix after reviews

* Update noah/explorer.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* remove bufferutils

---------

Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>
Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
2024-01-16 14:13:47 +01:00

620 lines
15 KiB
Go

package txbuilder
import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/internal/core/domain"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
)
const (
OP_INSPECTOUTPUTSCRIPTPUBKEY = 0xd1
OP_INSPECTOUTPUTVALUE = 0xcf
OP_PUSHCURRENTINPUTINDEX = 0xcd
unspendablePoint = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
timeDelta = 60 * 60 * 24 * 14 // 14 days in seconds
)
// the private method buildCongestionTree returns a function letting to plug in the pool transaction output as input of the tree's root node
type pluggableCongestionTree func(outpoint psetv2.InputArgs) (domain.CongestionTree, error)
// withOutput returns an introspection script that checks the script and the amount of the output at the given index
// verify will add an OP_EQUALVERIFY at the end of the script, otherwise it will add an OP_EQUAL
func withOutput(index byte, taprootWitnessProgram []byte, amount uint64, verify bool) []byte {
amountBuffer := make([]byte, 8)
binary.LittleEndian.PutUint64(amountBuffer, amount)
script := []byte{
index,
OP_INSPECTOUTPUTSCRIPTPUBKEY,
txscript.OP_1,
txscript.OP_EQUALVERIFY,
txscript.OP_DATA_32,
}
script = append(script, taprootWitnessProgram...)
script = append(script, []byte{
txscript.OP_EQUALVERIFY,
}...)
script = append(script, index)
script = append(script, []byte{
OP_INSPECTOUTPUTVALUE,
txscript.OP_1,
txscript.OP_EQUALVERIFY,
txscript.OP_DATA_8,
}...)
script = append(script, amountBuffer...)
if verify {
script = append(script, []byte{
txscript.OP_EQUALVERIFY,
}...)
} else {
script = append(script, []byte{
txscript.OP_EQUAL,
}...)
}
return script
}
func checksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
key := schnorr.SerializePubKey(pubkey)
return txscript.NewScriptBuilder().AddData(key).AddOp(txscript.OP_CHECKSIG).Script()
}
// checkSequenceVerifyScript without checksig
func checkSequenceVerifyScript(seconds uint) ([]byte, error) {
sequence, err := common.BIP68Encode(seconds)
if err != nil {
return nil, err
}
return append(sequence, []byte{
txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP,
}...), nil
}
// checkSequenceVerifyScript + checksig
func csvChecksigScript(pubkey *secp256k1.PublicKey, seconds uint) ([]byte, error) {
script, err := checksigScript(pubkey)
if err != nil {
return nil, err
}
csvScript, err := checkSequenceVerifyScript(seconds)
if err != nil {
return nil, err
}
return append(csvScript, script...), nil
}
// sweepTapLeaf returns a taproot leaf letting the owner of the key to spend the output after a given timeDelta
func sweepTapLeaf(sweepKey *secp256k1.PublicKey) (*taproot.TapElementsLeaf, error) {
sweepScript, err := csvChecksigScript(sweepKey, timeDelta)
if err != nil {
return nil, err
}
tapLeaf := taproot.NewBaseTapElementsLeaf(sweepScript)
return &tapLeaf, nil
}
// forceSplitCoinTapLeaf returns a taproot leaf that enforces a split into two outputs
// each output (left and right) will have the given amount and the given taproot key as witness program
func forceSplitCoinTapLeaf(
leftKey, rightKey *secp256k1.PublicKey, leftAmount, rightAmount uint64,
) taproot.TapElementsLeaf {
nextScriptLeft := withOutput(txscript.OP_0, schnorr.SerializePubKey(leftKey), leftAmount, rightKey != nil)
branchScript := append([]byte{}, nextScriptLeft...)
if rightKey != nil {
nextScriptRight := withOutput(txscript.OP_1, schnorr.SerializePubKey(rightKey), rightAmount, false)
branchScript = append(branchScript, nextScriptRight...)
}
return taproot.NewBaseTapElementsLeaf(branchScript)
}
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
}
// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script
func addTaprootInput(
updater *psetv2.Updater,
input psetv2.InputArgs,
internalTaprootKey *secp256k1.PublicKey,
taprootTree *taproot.IndexedElementsTapScriptTree,
) error {
if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil {
return err
}
if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil {
return err
}
for _, proof := range taprootTree.LeafMerkleProofs {
controlBlock := proof.ToControlBlock(internalTaprootKey)
if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script),
ControlBlock: controlBlock,
}); err != nil {
return err
}
}
return nil
}
// buildCongestionTree builder iteratively creates a binary tree of Pset from a set of receivers
// it returns a factory function creating a CongestionTree and the associated output script to be used in the pool transaction
func buildCongestionTree(
net *network.Network,
aspPublicKey *secp256k1.PublicKey,
receivers []domain.Receiver,
feeSatsPerNode uint64,
) (pluggableTree pluggableCongestionTree, sharedOutputScript []byte, sharedOutputAmount uint64, err error) {
unspendableKeyBytes, err := hex.DecodeString(unspendablePoint)
if err != nil {
return nil, nil, 0, err
}
unspendableKey, err := secp256k1.ParsePubKey(unspendableKeyBytes)
if err != nil {
return nil, nil, 0, err
}
var nodes []*node
for _, r := range receivers {
nodes = append(nodes, newLeaf(net, unspendableKey, aspPublicKey, r, feeSatsPerNode))
}
for len(nodes) > 1 {
nodes, err = createTreeLevel(nodes)
if err != nil {
return nil, nil, 0, err
}
}
psets, err := nodes[0].psets(nil, 0)
if err != nil {
return nil, nil, 0, err
}
// find the root
var rootPset *psetv2.Pset
for _, psetWithLevel := range psets {
if psetWithLevel.level == 0 {
rootPset = psetWithLevel.pset
break
}
}
// compute the shared output script
sweepLeaf, err := sweepTapLeaf(aspPublicKey)
if err != nil {
return nil, nil, 0, err
}
leftOutput := rootPset.Outputs[0]
leftWitnessProgram := leftOutput.Script[2:]
leftKey, err := schnorr.ParsePubKey(leftWitnessProgram)
if err != nil {
return nil, nil, 0, err
}
var rightAmount uint64
var rightKey *secp256k1.PublicKey
if len(rootPset.Outputs) > 1 {
rightAmount = rootPset.Outputs[1].Value
rightKey, err = schnorr.ParsePubKey(rootPset.Outputs[1].Script[2:])
if err != nil {
return nil, nil, 0, err
}
}
goToTreeScript := forceSplitCoinTapLeaf(
leftKey, rightKey, leftOutput.Value, rightAmount,
)
taprootTree := taproot.AssembleTaprootScriptTree(goToTreeScript, *sweepLeaf)
root := taprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:])
outputScript, err := taprootOutputScript(taprootKey)
if err != nil {
return nil, nil, 0, err
}
return func(outpoint psetv2.InputArgs) (domain.CongestionTree, error) {
psets, err := nodes[0].psets(&psetArgs{
input: outpoint,
taprootTree: taprootTree,
}, 0)
if err != nil {
return nil, err
}
maxLevel := 0
for _, p := range psets {
if p.level > maxLevel {
maxLevel = p.level
}
}
tree := make(domain.CongestionTree, maxLevel+1)
for _, psetWithLevel := range psets {
utx, err := psetWithLevel.pset.UnsignedTx()
if err != nil {
return nil, err
}
txid := utx.TxHash().String()
psetB64, err := psetWithLevel.pset.ToBase64()
if err != nil {
return nil, err
}
parentTxid := chainhash.Hash(psetWithLevel.pset.Inputs[0].PreviousTxid).String()
tree[psetWithLevel.level] = append(tree[psetWithLevel.level], domain.Node{
Txid: txid,
Tx: psetB64,
ParentTxid: parentTxid,
Leaf: psetWithLevel.leaf,
})
}
return tree, nil
}, outputScript, uint64(rightAmount) + leftOutput.Value + uint64(feeSatsPerNode), nil
}
func createTreeLevel(nodes []*node) ([]*node, error) {
if len(nodes)%2 != 0 {
last := nodes[len(nodes)-1]
pairs, err := createTreeLevel(nodes[:len(nodes)-1])
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 {
pairs = append(pairs, newBranch(nodes[i], nodes[i+1]))
}
return pairs, nil
}
// internal struct to build a binary tree of Pset
type node struct {
internalTaprootKey *secp256k1.PublicKey
sweepKey *secp256k1.PublicKey
receivers []domain.Receiver
left *node
right *node
network *network.Network
feeSats uint64
// cached values
_taprootKey *secp256k1.PublicKey
_taprootTree *taproot.IndexedElementsTapScriptTree
}
// create a node from a single receiver
func newLeaf(
network *network.Network,
internalKey *secp256k1.PublicKey,
sweepKey *secp256k1.PublicKey,
receiver domain.Receiver,
feeSats uint64,
) *node {
return &node{
sweepKey: sweepKey,
internalTaprootKey: internalKey,
receivers: []domain.Receiver{receiver},
network: network,
feeSats: feeSats,
}
}
// aggregate two nodes into a branch node
func newBranch(
left *node,
right *node,
) *node {
return &node{
internalTaprootKey: left.internalTaprootKey,
sweepKey: left.sweepKey,
receivers: append(left.receivers, right.receivers...),
left: left,
right: right,
network: left.network,
feeSats: left.feeSats,
}
}
func (n *node) isLeaf() bool {
return n.left.isEmpty() && (n.right == nil || n.right.isEmpty())
}
// is it the final node of the tree
func (n *node) isEmpty() bool {
return n.left == nil && n.right == nil
}
func (n *node) countChildren() int {
if n.isEmpty() {
return 0
}
result := 0
if n.left != nil && !n.left.isEmpty() {
result++
result += n.left.countChildren()
}
if n.right != nil && !n.right.isEmpty() {
result++
result += n.right.countChildren()
}
return result
}
// compute the output amount of a node
func (n *node) amount() uint64 {
var amount uint64
for _, r := range n.receivers {
amount += r.Amount
}
if n.isEmpty() {
return amount
}
nb := uint64(n.countChildren())
return amount + (nb+1)*n.feeSats
}
func (n *node) taprootKey() (*secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error) {
if n._taprootKey != nil && n._taprootTree != nil {
return n._taprootKey, n._taprootTree, nil
}
sweepTaprootLeaf, err := sweepTapLeaf(n.sweepKey)
if err != nil {
return nil, nil, err
}
if n.isEmpty() {
key, err := hex.DecodeString(n.receivers[0].Pubkey)
if err != nil {
return nil, nil, err
}
pubkey, err := secp256k1.ParsePubKey(key)
if err != nil {
return nil, nil, err
}
vtxoLeaf, err := common.VtxoScript(pubkey)
if err != nil {
return nil, nil, err
}
leafTaprootTree := taproot.AssembleTaprootScriptTree(*vtxoLeaf, *sweepTaprootLeaf)
root := leafTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
n.internalTaprootKey,
root[:],
)
n._taprootKey = taprootKey
n._taprootTree = leafTaprootTree
return taprootKey, leafTaprootTree, nil
}
leftKey, _, err := n.left.taprootKey()
if err != nil {
return nil, nil, err
}
rightKey, _, err := n.right.taprootKey()
if err != nil {
return nil, nil, err
}
branchTaprootLeaf := forceSplitCoinTapLeaf(
leftKey, rightKey, n.left.amount(), n.right.amount(),
)
branchTaprootTree := taproot.AssembleTaprootScriptTree(branchTaprootLeaf, *sweepTaprootLeaf)
root := branchTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
n.internalTaprootKey,
root[:],
)
n._taprootKey = taprootKey
n._taprootTree = branchTaprootTree
return taprootKey, branchTaprootTree, nil
}
// compute the output script of a node
func (n *node) script() ([]byte, error) {
taprootKey, _, err := n.taprootKey()
if err != nil {
return nil, err
}
return taprootOutputScript(taprootKey)
}
// use script & amount() to create OutputArgs
func (n *node) output() (*psetv2.OutputArgs, error) {
script, err := n.script()
if err != nil {
return nil, err
}
return &psetv2.OutputArgs{
Asset: n.network.AssetID,
Amount: uint64(n.amount()),
Script: script,
}, nil
}
type psetArgs struct {
input psetv2.InputArgs
taprootTree *taproot.IndexedElementsTapScriptTree
}
// create the node Pset from the previous node Pset represented by input arg
// if node is a branch, it adds two outputs to the Pset, one for the left branch and one for the right branch
// if node is a leaf, it only adds one output to the Pset (the node output)
func (n *node) pset(args *psetArgs) (*psetv2.Pset, error) {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return nil, err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return nil, err
}
if args != nil {
if err := addTaprootInput(updater, args.input, n.internalTaprootKey, args.taprootTree); err != nil {
return nil, err
}
}
feeOutput := psetv2.OutputArgs{
Amount: uint64(n.feeSats),
Asset: n.network.AssetID,
}
if n.isEmpty() {
output, err := n.output()
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{*output, feeOutput})
if err != nil {
return nil, err
}
return pset, nil
}
outputLeft, err := n.left.output()
if err != nil {
return nil, err
}
outputRight, err := n.right.output()
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{*outputLeft, *outputRight, feeOutput})
if err != nil {
return nil, err
}
return pset, nil
}
type psetWithLevel struct {
pset *psetv2.Pset
level int
leaf bool
}
// create the node pset and all the psets of its children recursively, updating the input arg at each step
// the function stops when it reaches a leaf node
func (n *node) psets(inputArgs *psetArgs, level int) ([]psetWithLevel, error) {
if inputArgs == nil && level != 0 {
return nil, fmt.Errorf("only the first level must be pluggable")
}
pset, err := n.pset(inputArgs)
if err != nil {
return nil, err
}
nodeResult := []psetWithLevel{
{pset, level, n.isLeaf()},
}
if n.left.isEmpty() && (n.right == nil || n.right.isEmpty()) {
return nodeResult, nil
}
if n.isEmpty() {
return nodeResult, nil
}
unsignedTx, err := pset.UnsignedTx()
if err != nil {
return nil, err
}
txID := unsignedTx.TxHash().String()
_, leftTaprootTree, err := n.left.taprootKey()
if err != nil {
return nil, err
}
psetsLeft, err := n.left.psets(&psetArgs{
input: psetv2.InputArgs{
Txid: txID,
TxIndex: 0,
},
taprootTree: leftTaprootTree,
}, level+1)
if err != nil {
return nil, err
}
_, rightTaprootTree, err := n.right.taprootKey()
if err != nil {
return nil, err
}
psetsRight, err := n.right.psets(&psetArgs{
input: psetv2.InputArgs{
Txid: txID,
TxIndex: 1,
},
taprootTree: rightTaprootTree,
}, level+1)
if err != nil {
return nil, err
}
return append(nodeResult, append(psetsLeft, psetsRight...)...), nil
}