mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-31 17:14:46 +01:00
Congestion tree validation (#84)
* add common/pkg/tree validation * update noah go.mod * cleaning and fixes * fix builder_test.go * Fix deferred func * fix even number of vtxos in congestion tree --------- Co-authored-by: altafan <18440657+altafan@users.noreply.github.com>
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
func checksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
|
||||
key := schnorr.SerializePubKey(pubkey)
|
||||
return txscript.NewScriptBuilder().AddData(key).AddOp(txscript.OP_CHECKSIG).Script()
|
||||
}
|
||||
|
||||
func VtxoScript(pubkey *secp256k1.PublicKey) (*taproot.TapElementsLeaf, error) {
|
||||
script, err := checksigScript(pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tapLeaf := taproot.NewBaseTapElementsLeaf(script)
|
||||
return &tapLeaf, nil
|
||||
}
|
||||
97
common/tree/congestion_tree.go
Normal file
97
common/tree/congestion_tree.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package tree
|
||||
|
||||
import "errors"
|
||||
|
||||
// Node is a struct embedding the transaction and the parent txid of a congestion tree node
|
||||
type Node struct {
|
||||
Txid string
|
||||
Tx string
|
||||
ParentTxid string
|
||||
Leaf bool
|
||||
}
|
||||
|
||||
var (
|
||||
ErrParentNotFound = errors.New("parent not found")
|
||||
ErrLeafNotFound = errors.New("leaf not found in congestion tree")
|
||||
)
|
||||
|
||||
// CongestionTree is reprensented as a matrix of TreeNode struct
|
||||
// the first level of the matrix is the root of the tree
|
||||
type CongestionTree [][]Node
|
||||
|
||||
// Leaves returns the leaves of the congestion tree (the vtxos txs)
|
||||
func (c CongestionTree) Leaves() []Node {
|
||||
leaves := c[len(c)-1]
|
||||
for _, level := range c[:len(c)-1] {
|
||||
for _, node := range level {
|
||||
if node.Leaf {
|
||||
leaves = append(leaves, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return leaves
|
||||
}
|
||||
|
||||
// Children returns all the nodes that have the given node as parent
|
||||
func (c CongestionTree) Children(nodeTxid string) []Node {
|
||||
var children []Node
|
||||
for _, level := range c {
|
||||
for _, node := range level {
|
||||
if node.ParentTxid == nodeTxid {
|
||||
children = append(children, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
func (c CongestionTree) NumberOfNodes() int {
|
||||
var count int
|
||||
for _, level := range c {
|
||||
count += len(level)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (c CongestionTree) Branch(vtxoTxid string) ([]Node, error) {
|
||||
branch := make([]Node, 0)
|
||||
|
||||
leaves := c.Leaves()
|
||||
// check if the vtxo is a leaf
|
||||
found := false
|
||||
for _, leaf := range leaves {
|
||||
if leaf.Txid == vtxoTxid {
|
||||
found = true
|
||||
branch = append(branch, leaf)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, ErrLeafNotFound
|
||||
}
|
||||
|
||||
rootTxid := c[0][0].Txid
|
||||
|
||||
for branch[0].Txid != rootTxid {
|
||||
parent, err := branch[0].findParent(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
branch = append([]Node{parent}, branch...)
|
||||
}
|
||||
|
||||
return branch, nil
|
||||
}
|
||||
|
||||
func (n Node) findParent(tree CongestionTree) (Node, error) {
|
||||
for _, level := range tree {
|
||||
for _, node := range level {
|
||||
if node.Txid == n.ParentTxid {
|
||||
return node, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return Node{}, ErrParentNotFound
|
||||
}
|
||||
260
common/tree/script.go
Normal file
260
common/tree/script.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
|
||||
"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"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
const (
|
||||
OP_INSPECTOUTPUTSCRIPTPUBKEY = 0xd1
|
||||
OP_INSPECTOUTPUTVALUE = 0xcf
|
||||
OP_PUSHCURRENTINPUTINDEX = 0xcd
|
||||
)
|
||||
|
||||
// VtxoScript returns a simple checksig script for a given pubkey
|
||||
func VtxoScript(pubkey *secp256k1.PublicKey) (*taproot.TapElementsLeaf, error) {
|
||||
script, err := checksigScript(pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tapLeaf := taproot.NewBaseTapElementsLeaf(script)
|
||||
return &tapLeaf, nil
|
||||
}
|
||||
|
||||
// SweepScript returns a taproot leaf letting the owner of the key to spend the output after a given timeDelta
|
||||
func SweepScript(sweepKey *secp256k1.PublicKey, seconds uint) (*taproot.TapElementsLeaf, error) {
|
||||
sweepScript, err := csvChecksigScript(sweepKey, seconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tapLeaf := taproot.NewBaseTapElementsLeaf(sweepScript)
|
||||
return &tapLeaf, nil
|
||||
}
|
||||
|
||||
// BranchScript returns a taproot leaf that will split the coin in two outputs
|
||||
// each output (left and right) will have the given amount and the given taproot key as witness program
|
||||
func BranchScript(
|
||||
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 decodeBranchScript(script []byte) (valid bool, leftKey, rightKey *secp256k1.PublicKey, leftAmount, rightAmount uint64, err error) {
|
||||
if len(script) != 52 && len(script) != 104 {
|
||||
return false, nil, nil, 0, 0, nil
|
||||
}
|
||||
|
||||
isLeftOnly := len(script) == 52
|
||||
|
||||
validLeft, leftKey, leftAmount, err := decodeWithOutputScript(script[:52], txscript.OP_0, !isLeftOnly)
|
||||
if err != nil {
|
||||
return false, nil, nil, 0, 0, err
|
||||
}
|
||||
|
||||
if !validLeft {
|
||||
return false, nil, nil, 0, 0, nil
|
||||
}
|
||||
|
||||
if isLeftOnly {
|
||||
return true, leftKey, nil, leftAmount, 0, nil
|
||||
}
|
||||
|
||||
validRight, rightKey, rightAmount, err := decodeWithOutputScript(script[52:], txscript.OP_1, false)
|
||||
if err != nil {
|
||||
return false, nil, nil, 0, 0, err
|
||||
}
|
||||
|
||||
if !validRight {
|
||||
return false, nil, nil, 0, 0, nil
|
||||
}
|
||||
|
||||
rebuilt := BranchScript(leftKey, rightKey, leftAmount, rightAmount)
|
||||
|
||||
if !bytes.Equal(rebuilt.Script, script) {
|
||||
return false, nil, nil, 0, 0, nil
|
||||
}
|
||||
|
||||
return true, leftKey, rightKey, leftAmount, rightAmount, nil
|
||||
}
|
||||
|
||||
func decodeWithOutputScript(script []byte, expectedIndex byte, isVerify bool) (valid bool, pubkey *secp256k1.PublicKey, amount uint64, err error) {
|
||||
if len(script) != 52 {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
if script[0] != expectedIndex {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
// 32 bytes for the witness program
|
||||
pubkey, err = schnorr.ParsePubKey(script[5 : 5+32])
|
||||
if err != nil {
|
||||
return false, nil, 0, err
|
||||
}
|
||||
|
||||
inspectOutputValueIndex := bytes.IndexByte(script, OP_INSPECTOUTPUTVALUE)
|
||||
if inspectOutputValueIndex == -1 {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
if script[inspectOutputValueIndex-1] != expectedIndex {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
// 8 bytes for the amount
|
||||
amountBytes := script[len(script)-9 : len(script)-1]
|
||||
amount = binary.LittleEndian.Uint64(amountBytes)
|
||||
|
||||
rebuilt := withOutput(expectedIndex, schnorr.SerializePubKey(pubkey), amount, isVerify)
|
||||
if !bytes.Equal(rebuilt, script) {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
return true, pubkey, amount, nil
|
||||
}
|
||||
|
||||
func decodeChecksigScript(script []byte) (valid bool, pubkey *secp256k1.PublicKey, err error) {
|
||||
checksigIndex := bytes.Index(script, []byte{txscript.OP_CHECKSIG})
|
||||
if checksigIndex == -1 || checksigIndex == 0 {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
key := script[1:checksigIndex]
|
||||
if len(key) != 32 {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
pubkey, err = schnorr.ParsePubKey(key)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
rebuilt, err := checksigScript(pubkey)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(rebuilt, script) {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, pubkey, nil
|
||||
}
|
||||
|
||||
func decodeSweepScript(script []byte) (valid bool, aspPubKey *secp256k1.PublicKey, seconds uint, err error) {
|
||||
csvIndex := bytes.Index(script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP})
|
||||
if csvIndex == -1 || csvIndex == 0 {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
sequence := script[:csvIndex]
|
||||
|
||||
seconds, err = common.BIP68Decode(sequence)
|
||||
if err != nil {
|
||||
return false, nil, 0, err
|
||||
}
|
||||
|
||||
checksigScript := script[csvIndex+2:]
|
||||
valid, aspPubKey, err = decodeChecksigScript(checksigScript)
|
||||
if err != nil {
|
||||
return false, nil, 0, err
|
||||
}
|
||||
|
||||
rebuilt, err := csvChecksigScript(aspPubKey, seconds)
|
||||
if err != nil {
|
||||
return false, nil, 0, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(rebuilt, script) {
|
||||
return false, nil, 0, nil
|
||||
}
|
||||
|
||||
return valid, aspPubKey, seconds, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func checksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
|
||||
key := schnorr.SerializePubKey(pubkey)
|
||||
return txscript.NewScriptBuilder().AddData(key).AddOp(txscript.OP_CHECKSIG).Script()
|
||||
}
|
||||
|
||||
// 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
|
||||
// length = 52 bytes
|
||||
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
|
||||
}
|
||||
306
common/tree/validation.go
Normal file
306
common/tree/validation.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidPoolTransaction = errors.New("invalid 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 two tapscripts leaves")
|
||||
ErrInternalKey = errors.New("taproot internal key is not unspendable")
|
||||
ErrInvalidTaprootScript = errors.New("invalid taproot script")
|
||||
ErrInvalidLeafTaprootScript = errors.New("invalid leaf taproot script")
|
||||
ErrInvalidAmount = errors.New("children amount is different from parent amount")
|
||||
ErrInvalidAsset = errors.New("invalid output asset")
|
||||
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")
|
||||
ErrMissingBranchTapscript = errors.New("missing branch tapscript")
|
||||
ErrInvalidLeaf = errors.New("leaf node shouldn't have children")
|
||||
ErrWrongPoolTxID = errors.New("root input should be the pool tx outpoint")
|
||||
)
|
||||
|
||||
const (
|
||||
UnspendablePoint = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
|
||||
sharedOutputIndex = 0
|
||||
)
|
||||
|
||||
// ValidateCongestionTree checks if the given congestion tree is valid
|
||||
// poolTxID & poolTxIndex & poolTxAmount are used to validate the root input outpoint
|
||||
// aspPublicKey & roundLifetimeSeconds 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 CongestionTree,
|
||||
poolTxHex string,
|
||||
aspPublicKey *secp256k1.PublicKey,
|
||||
roundLifetimeSeconds uint,
|
||||
) error {
|
||||
unspendableKeyBytes, _ := hex.DecodeString(UnspendablePoint)
|
||||
unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes)
|
||||
|
||||
poolTransaction, err := transaction.NewTxFromHex(poolTxHex)
|
||||
if err != nil {
|
||||
return ErrInvalidPoolTransaction
|
||||
}
|
||||
|
||||
poolTxAmount, err := elementsutil.ValueFromBytes(poolTransaction.Outputs[sharedOutputIndex].Value)
|
||||
if err != nil {
|
||||
return ErrInvalidPoolTransaction
|
||||
}
|
||||
|
||||
poolTxID := poolTransaction.TxHash().String()
|
||||
|
||||
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 := psetv2.NewPsetFromBase64(rootPsetB64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid root transaction: %w", err)
|
||||
}
|
||||
|
||||
if len(rootPset.Inputs) != 1 {
|
||||
return ErrNumberOfInputs
|
||||
}
|
||||
|
||||
rootInput := rootPset.Inputs[0]
|
||||
if chainhash.Hash(rootInput.PreviousTxid).String() != poolTxID || rootInput.PreviousTxIndex != sharedOutputIndex {
|
||||
return ErrWrongPoolTxID
|
||||
}
|
||||
|
||||
sumRootValue := uint64(0)
|
||||
for _, output := range rootPset.Outputs {
|
||||
sumRootValue += output.Value
|
||||
}
|
||||
|
||||
if sumRootValue != poolTxAmount {
|
||||
return ErrInvalidAmount
|
||||
}
|
||||
|
||||
if len(tree.Leaves()) == 0 {
|
||||
return ErrNoLeaves
|
||||
}
|
||||
|
||||
// iterates over all the nodes of the tree
|
||||
for _, level := range tree {
|
||||
for _, node := range level {
|
||||
if err := validateNodeTransaction(node, tree, unspendableKey, aspPublicKey, roundLifetimeSeconds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNodeTransaction(
|
||||
node Node,
|
||||
tree CongestionTree,
|
||||
expectedInternalKey,
|
||||
expectedPublicKeyASP *secp256k1.PublicKey,
|
||||
expectedSequenceSeconds uint,
|
||||
) error {
|
||||
if node.Tx == "" {
|
||||
return ErrNodeTransactionEmpty
|
||||
}
|
||||
|
||||
if node.Txid == "" {
|
||||
return ErrNodeTxidEmpty
|
||||
}
|
||||
|
||||
if node.ParentTxid == "" {
|
||||
return ErrNodeParentTxidEmpty
|
||||
}
|
||||
|
||||
decodedPset, err := psetv2.NewPsetFromBase64(node.Tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid node transaction: %w", err)
|
||||
}
|
||||
|
||||
utx, err := decodedPset.UnsignedTx()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid node transaction: %w", err)
|
||||
}
|
||||
|
||||
if utx.TxHash().String() != node.Txid {
|
||||
return ErrNodeTxidDifferent
|
||||
}
|
||||
|
||||
if len(decodedPset.Inputs) != 1 {
|
||||
return ErrNumberOfInputs
|
||||
}
|
||||
|
||||
input := decodedPset.Inputs[0]
|
||||
if len(input.TapLeafScript) != 2 {
|
||||
return ErrNumberOfTapscripts
|
||||
}
|
||||
|
||||
if chainhash.Hash(decodedPset.Inputs[0].PreviousTxid).String() != node.ParentTxid {
|
||||
return ErrParentTxidInput
|
||||
}
|
||||
|
||||
feeOutput := decodedPset.Outputs[len(decodedPset.Outputs)-1]
|
||||
if len(feeOutput.Script) != 0 {
|
||||
return ErrMissingFeeOutput
|
||||
}
|
||||
|
||||
children := tree.Children(node.Txid)
|
||||
|
||||
if node.Leaf && len(children) > 1 {
|
||||
return ErrLeafChildren
|
||||
}
|
||||
|
||||
for childIndex, child := range children {
|
||||
childTx, err := psetv2.NewPsetFromBase64(child.Tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid child transaction: %w", err)
|
||||
}
|
||||
|
||||
parentOutput := decodedPset.Outputs[childIndex]
|
||||
previousScriptKey := parentOutput.Script[2:]
|
||||
if len(previousScriptKey) != 32 {
|
||||
return ErrInvalidTaprootScript
|
||||
}
|
||||
|
||||
sweepLeafFound := false
|
||||
branchLeafFound := false
|
||||
|
||||
for _, tapLeaf := range childTx.Inputs[0].TapLeafScript {
|
||||
key := tapLeaf.ControlBlock.InternalKey
|
||||
if !key.IsEqual(expectedInternalKey) {
|
||||
return ErrInternalKey
|
||||
}
|
||||
|
||||
rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script)
|
||||
outputScript := taproot.ComputeTaprootOutputKey(key, rootHash)
|
||||
|
||||
if !bytes.Equal(schnorr.SerializePubKey(outputScript), previousScriptKey) {
|
||||
return ErrInvalidTaprootScript
|
||||
}
|
||||
|
||||
isSweepLeaf, aspKey, seconds, err := decodeSweepScript(tapLeaf.Script)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sweep script: %w", err)
|
||||
}
|
||||
|
||||
if isSweepLeaf {
|
||||
if !aspKey.IsEqual(aspKey) {
|
||||
return ErrInvalidASP
|
||||
}
|
||||
|
||||
if seconds != expectedSequenceSeconds {
|
||||
return ErrInvalidSweepSequence
|
||||
}
|
||||
|
||||
sweepLeafFound = true
|
||||
continue
|
||||
}
|
||||
|
||||
isBranchLeaf, leftKey, rightKey, leftAmount, rightAmount, err := decodeBranchScript(tapLeaf.Script)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid vtxo script: %w", err)
|
||||
}
|
||||
|
||||
if isBranchLeaf {
|
||||
branchLeafFound = true
|
||||
|
||||
// check outputs
|
||||
nbOuts := len(childTx.Outputs)
|
||||
if leftKey != nil && rightKey != nil {
|
||||
if nbOuts != 3 {
|
||||
return ErrNumberOfOutputs
|
||||
}
|
||||
} else {
|
||||
if nbOuts != 2 {
|
||||
return ErrNumberOfOutputs
|
||||
}
|
||||
}
|
||||
|
||||
leftWitnessProgram := childTx.Outputs[0].Script[2:]
|
||||
leftOutputAmount := childTx.Outputs[0].Value
|
||||
|
||||
if !bytes.Equal(leftWitnessProgram, schnorr.SerializePubKey(leftKey)) {
|
||||
return ErrInvalidLeftOutput
|
||||
}
|
||||
|
||||
if leftAmount != leftOutputAmount {
|
||||
return ErrInvalidLeftOutput
|
||||
}
|
||||
|
||||
if rightKey != nil {
|
||||
rightWitnessProgram := childTx.Outputs[1].Script[2:]
|
||||
rightOutputAmount := childTx.Outputs[1].Value
|
||||
|
||||
if !bytes.Equal(rightWitnessProgram, schnorr.SerializePubKey(rightKey)) {
|
||||
return ErrInvalidRightOutput
|
||||
}
|
||||
|
||||
if rightAmount != rightOutputAmount {
|
||||
return ErrInvalidRightOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !sweepLeafFound {
|
||||
return ErrMissingSweepTapscript
|
||||
}
|
||||
|
||||
if !branchLeafFound {
|
||||
return ErrMissingBranchTapscript
|
||||
}
|
||||
|
||||
sumChildAmount := uint64(0)
|
||||
for _, output := range childTx.Outputs {
|
||||
sumChildAmount += output.Value
|
||||
if !bytes.Equal(output.Asset, parentOutput.Asset) {
|
||||
return ErrInvalidAsset
|
||||
}
|
||||
}
|
||||
|
||||
if sumChildAmount != parentOutput.Value {
|
||||
return ErrInvalidAmount
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user