Support forfeit with CHECKLOCKTIMEVERIFY (#389)

* explicit Timelock struct

* support & test CLTV forfeit path

* fix wasm pkg

* fix wasm

* fix liquid GetCurrentBlockTime

* cleaning

* move esplora URL check
This commit is contained in:
Louis Singer
2024-11-28 14:51:06 +01:00
committed by GitHub
parent a4ae439341
commit 02542c3634
51 changed files with 1007 additions and 257 deletions

View File

@@ -14,31 +14,68 @@ const (
SECONDS_MOD = 1 << SEQUENCE_LOCKTIME_GRANULARITY SECONDS_MOD = 1 << SEQUENCE_LOCKTIME_GRANULARITY
SECONDS_MAX = SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY SECONDS_MAX = SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31 SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31
SECONDS_PER_BLOCK = 10 * 60 // 10 minutes
) )
func closerToModulo512(x uint) uint { type LocktimeType uint
return x - (x % 512)
const (
LocktimeTypeSecond LocktimeType = iota
LocktimeTypeBlock
)
// Locktime represents a BIP68 relative timelock value.
// This struct is comparable and can be used as a map key.
type Locktime struct {
Type LocktimeType
Value uint32
} }
func BIP68Sequence(locktime uint) (uint32, error) { func (l Locktime) Seconds() int64 {
isSeconds := locktime >= 512 if l.Type == LocktimeTypeBlock {
return int64(l.Value) * SECONDS_PER_BLOCK
}
return int64(l.Value)
}
func (l Locktime) Compare(other Locktime) int {
val := l.Seconds()
otherVal := other.Seconds()
if val == otherVal {
return 0
}
if val < otherVal {
return -1
}
return 1
}
// LessThan returns true if this locktime is less than the other locktime
func (l Locktime) LessThan(other Locktime) bool {
return l.Compare(other) < 0
}
func BIP68Sequence(locktime Locktime) (uint32, error) {
value := locktime.Value
isSeconds := locktime.Type == LocktimeTypeSecond
if isSeconds { if isSeconds {
locktime = closerToModulo512(locktime) if value > SECONDS_MAX {
if locktime > SECONDS_MAX {
return 0, fmt.Errorf("seconds too large, max is %d", SECONDS_MAX) return 0, fmt.Errorf("seconds too large, max is %d", SECONDS_MAX)
} }
if locktime%SECONDS_MOD != 0 { if value%SECONDS_MOD != 0 {
return 0, fmt.Errorf("seconds must be a multiple of %d", SECONDS_MOD) return 0, fmt.Errorf("seconds must be a multiple of %d", SECONDS_MOD)
} }
} }
return blockchain.LockTimeToSequence(isSeconds, uint32(locktime)), nil return blockchain.LockTimeToSequence(isSeconds, value), nil
} }
func BIP68DecodeSequence(sequence []byte) (uint, error) { func BIP68DecodeSequence(sequence []byte) (*Locktime, error) {
scriptNumber, err := txscript.MakeScriptNum(sequence, true, len(sequence)) scriptNumber, err := txscript.MakeScriptNum(sequence, true, len(sequence))
if err != nil { if err != nil {
return 0, err return nil, err
} }
if scriptNumber >= txscript.OP_1 && scriptNumber <= txscript.OP_16 { if scriptNumber >= txscript.OP_1 && scriptNumber <= txscript.OP_16 {
@@ -48,12 +85,12 @@ func BIP68DecodeSequence(sequence []byte) (uint, error) {
asNumber := int64(scriptNumber) asNumber := int64(scriptNumber)
if asNumber&SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 { if asNumber&SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
return 0, fmt.Errorf("sequence is disabled") return nil, fmt.Errorf("sequence is disabled")
} }
if asNumber&SEQUENCE_LOCKTIME_TYPE_FLAG != 0 { if asNumber&SEQUENCE_LOCKTIME_TYPE_FLAG != 0 {
seconds := asNumber & SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY seconds := asNumber & SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
return uint(seconds), nil return &Locktime{Type: LocktimeTypeSecond, Value: uint32(seconds)}, nil
} }
return uint(asNumber), nil return &Locktime{Type: LocktimeTypeBlock, Value: uint32(asNumber)}, nil
} }

View File

@@ -18,7 +18,7 @@ import (
// CraftSharedOutput returns the taproot script and the amount of the initial root output // CraftSharedOutput returns the taproot script and the amount of the initial root output
func CraftSharedOutput( func CraftSharedOutput(
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime common.Locktime,
) ([]byte, int64, error) { ) ([]byte, int64, error) {
aggregatedKey, _, err := createAggregatedKeyWithSweep( aggregatedKey, _, err := createAggregatedKeyWithSweep(
cosigners, server, roundLifetime, cosigners, server, roundLifetime,
@@ -45,7 +45,7 @@ func CraftSharedOutput(
// BuildVtxoTree creates all the tree's transactions // BuildVtxoTree creates all the tree's transactions
func BuildVtxoTree( func BuildVtxoTree(
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf, initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime common.Locktime,
) (tree.VtxoTree, error) { ) (tree.VtxoTree, error) {
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep( aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
cosigners, server, roundLifetime, cosigners, server, roundLifetime,
@@ -280,11 +280,11 @@ func createRootNode(
} }
func createAggregatedKeyWithSweep( func createAggregatedKeyWithSweep(
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime int64, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime common.Locktime,
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) { ) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
sweepClosure := &tree.CSVSigClosure{ sweepClosure := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server}}, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server}},
Seconds: uint(roundLifetime), Locktime: roundLifetime,
} }
sweepScript, err := sweepClosure.Script() sweepScript, err := sweepClosure.Script()

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@@ -18,9 +19,10 @@ import (
const ( const (
minRelayFee = 1000 minRelayFee = 1000
exitDelay = 512 exitDelay = 512
lifetime = 1024
) )
var lifetime = common.Locktime{Type: common.LocktimeTypeBlock, Value: 144}
var testTxid, _ = chainhash.NewHashFromStr("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12") var testTxid, _ = chainhash.NewHashFromStr("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12")
func TestRoundTripSignTree(t *testing.T) { func TestRoundTripSignTree(t *testing.T) {
@@ -63,7 +65,7 @@ func TestRoundTripSignTree(t *testing.T) {
sweepClosure := &tree.CSVSigClosure{ sweepClosure := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server.PubKey()}}, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server.PubKey()}},
Seconds: uint(lifetime), Locktime: lifetime,
} }
sweepScript, err := sweepClosure.Script() sweepScript, err := sweepClosure.Script()

View File

@@ -12,7 +12,6 @@ import (
func BuildRedeemTx( func BuildRedeemTx(
vtxos []common.VtxoInput, vtxos []common.VtxoInput,
outputs []*wire.TxOut, outputs []*wire.TxOut,
feeAmount int64,
) (string, error) { ) (string, error) {
if len(vtxos) <= 0 { if len(vtxos) <= 0 {
return "", fmt.Errorf("missing vtxos") return "", fmt.Errorf("missing vtxos")
@@ -50,10 +49,6 @@ func BuildRedeemTx(
ins = append(ins, vtxo.Outpoint) ins = append(ins, vtxo.Outpoint)
} }
if feeAmount >= outputs[len(outputs)-1].Value {
return "", fmt.Errorf("redeem tx fee is higher than the amount of the change receiver")
}
sequences := make([]uint32, len(ins)) sequences := make([]uint32, len(ins))
for i := range sequences { for i := range sequences {
sequences[i] = wire.MaxTxInSequenceNum sequences[i] = wire.MaxTxInSequenceNum

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
@@ -71,7 +72,7 @@ func UnspendableKey() *secp256k1.PublicKey {
// - every control block and taproot output scripts // - every control block and taproot output scripts
// - input and output amounts // - input and output amounts
func ValidateVtxoTree( func ValidateVtxoTree(
vtxoTree tree.VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey, roundLifetime int64, vtxoTree tree.VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey, roundLifetime common.Locktime,
) error { ) error {
roundTransaction, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true) roundTransaction, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
if err != nil { if err != nil {
@@ -125,7 +126,7 @@ func ValidateVtxoTree(
sweepClosure := &tree.CSVSigClosure{ sweepClosure := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{serverPubkey}}, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{serverPubkey}},
Seconds: uint(roundLifetime), Locktime: roundLifetime,
} }
sweepScript, err := sweepClosure.Script() sweepScript, err := sweepClosure.Script()

View File

@@ -10,7 +10,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
type VtxoScript common.VtxoScript[bitcoinTapTree, *tree.MultisigClosure, *tree.CSVSigClosure] type VtxoScript common.VtxoScript[bitcoinTapTree, tree.Closure]
func ParseVtxoScript(scripts []string) (VtxoScript, error) { func ParseVtxoScript(scripts []string) (VtxoScript, error) {
types := []VtxoScript{ types := []VtxoScript{
@@ -26,7 +26,7 @@ func ParseVtxoScript(scripts []string) (VtxoScript, error) {
return nil, fmt.Errorf("invalid vtxo scripts: %s", scripts) return nil, fmt.Errorf("invalid vtxo scripts: %s", scripts)
} }
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay uint) VtxoScript { func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay common.Locktime) VtxoScript {
base := tree.NewDefaultVtxoScript(owner, server, exitDelay) base := tree.NewDefaultVtxoScript(owner, server, exitDelay)
return &TapscriptsVtxoScript{*base} return &TapscriptsVtxoScript{*base}

View File

@@ -51,7 +51,7 @@ func ParseDefaultVtxoDescriptor(
if first, ok := andLeaf.First.(*Older); ok { if first, ok := andLeaf.First.(*Older); ok {
if second, ok := andLeaf.Second.(*PK); ok { if second, ok := andLeaf.Second.(*PK); ok {
timeout = first.Timeout timeout = uint(first.Locktime.Value)
keyBytes, err := hex.DecodeString(second.Key.Hex) keyBytes, err := hex.DecodeString(second.Key.Hex)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err

View File

@@ -97,11 +97,11 @@ func (e *PK) Script(verify bool) (string, error) {
} }
type Older struct { type Older struct {
Timeout uint Locktime common.Locktime
} }
func (e *Older) String() string { func (e *Older) String() string {
return fmt.Sprintf("older(%d)", e.Timeout) return fmt.Sprintf("older(%d)", e.Locktime.Value)
} }
func (e *Older) Parse(policy string) error { func (e *Older) Parse(policy string) error {
@@ -124,13 +124,13 @@ func (e *Older) Parse(policy string) error {
return ErrInvalidOlderPolicy return ErrInvalidOlderPolicy
} }
e.Timeout = uint(timeout) e.Locktime = common.Locktime{Type: common.LocktimeTypeBlock, Value: uint32(timeout)}
return nil return nil
} }
func (e *Older) Script(bool) (string, error) { func (e *Older) Script(bool) (string, error) {
sequence, err := common.BIP68Sequence(e.Timeout) sequence, err := common.BIP68Sequence(e.Locktime)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -3,6 +3,7 @@ package descriptor_test
import ( import (
"testing" "testing"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor" "github.com/ark-network/ark/common/descriptor"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -53,7 +54,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
}, },
Second: &descriptor.Older{ Second: &descriptor.Older{
Timeout: 144, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 144},
}, },
}, },
}, },
@@ -91,7 +92,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
}, },
First: &descriptor.Older{ First: &descriptor.Older{
Timeout: 604672, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 604672},
}, },
}, },
}, },
@@ -156,7 +157,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
}, },
First: &descriptor.Older{ First: &descriptor.Older{
Timeout: 604672, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 604672},
}, },
}, },
&descriptor.And{ &descriptor.And{
@@ -232,7 +233,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
&descriptor.And{ &descriptor.And{
First: &descriptor.Older{ First: &descriptor.Older{
Timeout: 512, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
}, },
Second: &descriptor.And{ Second: &descriptor.And{
First: &descriptor.PK{ First: &descriptor.PK{
@@ -278,7 +279,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
&descriptor.And{ &descriptor.And{
First: &descriptor.Older{ First: &descriptor.Older{
Timeout: 1024, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
}, },
Second: &descriptor.And{ Second: &descriptor.And{
First: &descriptor.PK{ First: &descriptor.PK{
@@ -299,7 +300,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
&descriptor.And{ &descriptor.And{
First: &descriptor.Older{ First: &descriptor.Older{
Timeout: 512, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
}, },
Second: &descriptor.PK{ Second: &descriptor.PK{
Key: descriptor.XOnlyKey{ Key: descriptor.XOnlyKey{
@@ -311,7 +312,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
&descriptor.And{ &descriptor.And{
First: &descriptor.Older{ First: &descriptor.Older{
Timeout: 512, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
}, },
Second: &descriptor.And{ Second: &descriptor.And{
First: &descriptor.PK{ First: &descriptor.PK{
@@ -393,7 +394,7 @@ func TestCompileDescriptor(t *testing.T) {
}, },
}, },
Second: &descriptor.Older{ Second: &descriptor.Older{
Timeout: 1024, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
}, },
}, },
}, },
@@ -464,14 +465,14 @@ func TestParseOlder(t *testing.T) {
policy: "older(512)", policy: "older(512)",
expectedScript: "03010040b275", expectedScript: "03010040b275",
expected: descriptor.Older{ expected: descriptor.Older{
Timeout: uint(512), Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
}, },
}, },
{ {
policy: "older(1024)", policy: "older(1024)",
expectedScript: "03020040b275", expectedScript: "03020040b275",
expected: descriptor.Older{ expected: descriptor.Older{
Timeout: uint(1024), Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
}, },
}, },
} }
@@ -506,7 +507,7 @@ func TestParseAnd(t *testing.T) {
}, },
}, },
Second: &descriptor.Older{ Second: &descriptor.Older{
Timeout: 512, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
}, },
}, },
}, },
@@ -522,7 +523,7 @@ func TestParseAnd(t *testing.T) {
}, },
}, },
First: &descriptor.Older{ First: &descriptor.Older{
Timeout: 512, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
}, },
}, },
}, },

View File

@@ -4,6 +4,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@@ -14,7 +15,7 @@ import (
func BuildVtxoTree( func BuildVtxoTree(
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf, asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime common.Locktime,
) ( ) (
factoryFn TreeFactory, factoryFn TreeFactory,
sharedOutputScript []byte, sharedOutputAmount uint64, err error, sharedOutputScript []byte, sharedOutputAmount uint64, err error,
@@ -53,7 +54,7 @@ type node struct {
right *node right *node
asset string asset string
feeSats uint64 feeSats uint64
roundLifetime int64 roundLifetime common.Locktime
_inputTaprootKey *secp256k1.PublicKey _inputTaprootKey *secp256k1.PublicKey
_inputTaprootTree *taproot.IndexedElementsTapScriptTree _inputTaprootTree *taproot.IndexedElementsTapScriptTree
@@ -164,7 +165,7 @@ func (n *node) getWitnessData() (
sweepClosure := &CSVSigClosure{ sweepClosure := &CSVSigClosure{
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{n.sweepKey}}, MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{n.sweepKey}},
Seconds: uint(n.roundLifetime), Locktime: n.roundLifetime,
} }
sweepLeaf, err := sweepClosure.Script() sweepLeaf, err := sweepClosure.Script()
@@ -380,7 +381,7 @@ func (n *node) buildVtxoTree() TreeFactory {
func buildTreeNodes( func buildTreeNodes(
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf, asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64, feeSatsPerNode uint64, roundLifetime common.Locktime,
) (root *node, err error) { ) (root *node, err error) {
if len(receivers) == 0 { if len(receivers) == 0 {
return nil, fmt.Errorf("no receivers provided") return nil, fmt.Errorf("no receivers provided")

View File

@@ -53,16 +53,25 @@ type MultisigClosure struct {
} }
// CSVSigClosure is a closure that contains a list of public keys and a // CSVSigClosure is a closure that contains a list of public keys and a
// CHECKSEQUENCEVERIFY + DROP. The witness size is 64 bytes per key, admitting // CHECKSEQUENCEVERIFY. The witness size is 64 bytes per key, admitting
// the sighash type is SIGHASH_DEFAULT. // the sighash type is SIGHASH_DEFAULT.
type CSVSigClosure struct { type CSVSigClosure struct {
MultisigClosure MultisigClosure
Seconds uint Locktime common.Locktime
}
// CLTVMultisigClosure is a closure that contains a list of public keys and a
// CHECKLOCKTIMEVERIFY. The witness size is 64 bytes per key, admitting
// the sighash type is SIGHASH_DEFAULT.
type CLTVMultisigClosure struct {
MultisigClosure
Locktime common.Locktime
} }
func DecodeClosure(script []byte) (Closure, error) { func DecodeClosure(script []byte) (Closure, error) {
types := []Closure{ types := []Closure{
&CSVSigClosure{}, &CSVSigClosure{},
&CLTVMultisigClosure{},
&MultisigClosure{}, &MultisigClosure{},
&UnrollClosure{}, &UnrollClosure{},
} }
@@ -324,8 +333,13 @@ func (f *CSVSigClosure) WitnessSize() int {
} }
func (d *CSVSigClosure) Script() ([]byte, error) { func (d *CSVSigClosure) Script() ([]byte, error) {
sequence, err := common.BIP68Sequence(d.Locktime)
if err != nil {
return nil, err
}
csvScript, err := txscript.NewScriptBuilder(). csvScript, err := txscript.NewScriptBuilder().
AddInt64(int64(d.Seconds)). AddInt64(int64(sequence)).
AddOps([]byte{ AddOps([]byte{
txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP, txscript.OP_DROP,
@@ -360,7 +374,7 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
sequence = sequence[1:] sequence = sequence[1:]
} }
seconds, err := common.BIP68DecodeSequence(sequence) locktime, err := common.BIP68DecodeSequence(sequence)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -375,7 +389,7 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
return false, nil return false, nil
} }
d.Seconds = seconds d.Locktime = *locktime
d.MultisigClosure = *multisigClosure d.MultisigClosure = *multisigClosure
return valid, nil return valid, nil
@@ -666,3 +680,87 @@ func (c *UnrollClosure) Witness(controlBlock []byte, _ map[string][]byte) (wire.
// UnrollClosure only needs script and control block // UnrollClosure only needs script and control block
return wire.TxWitness{script, controlBlock}, nil return wire.TxWitness{script, controlBlock}, nil
} }
func (f *CLTVMultisigClosure) Witness(controlBlock []byte, signatures map[string][]byte) (wire.TxWitness, error) {
multisigWitness, err := f.MultisigClosure.Witness(controlBlock, signatures)
if err != nil {
return nil, err
}
script, err := f.Script()
if err != nil {
return nil, fmt.Errorf("failed to generate script: %w", err)
}
// replace script with cltv script
multisigWitness[len(multisigWitness)-2] = script
return multisigWitness, nil
}
func (f *CLTVMultisigClosure) WitnessSize() int {
return f.MultisigClosure.WitnessSize()
}
func (d *CLTVMultisigClosure) Script() ([]byte, error) {
locktime, err := common.BIP68Sequence(d.Locktime)
if err != nil {
return nil, err
}
cltvScript, err := txscript.NewScriptBuilder().
AddInt64(int64(locktime)).
AddOps([]byte{
txscript.OP_CHECKLOCKTIMEVERIFY,
txscript.OP_DROP,
}).
Script()
if err != nil {
return nil, err
}
multisigScript, err := d.MultisigClosure.Script()
if err != nil {
return nil, err
}
return append(cltvScript, multisigScript...), nil
}
func (d *CLTVMultisigClosure) Decode(script []byte) (bool, error) {
if len(script) == 0 {
return false, fmt.Errorf("empty script")
}
cltvIndex := bytes.Index(
script, []byte{txscript.OP_CHECKLOCKTIMEVERIFY, txscript.OP_DROP},
)
if cltvIndex == -1 || cltvIndex == 0 {
return false, nil
}
locktime := script[:cltvIndex]
if len(locktime) > 1 {
locktime = locktime[1:]
}
locktimeValue, err := common.BIP68DecodeSequence(locktime)
if err != nil {
return false, err
}
multisigClosure := &MultisigClosure{}
valid, err := multisigClosure.Decode(script[cltvIndex+2:])
if err != nil {
return false, err
}
if !valid {
return false, nil
}
d.Locktime = *locktimeValue
d.MultisigClosure = *multisigClosure
return valid, nil
}

View File

@@ -1,9 +1,11 @@
package tree_test package tree_test
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"testing" "testing"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@@ -19,7 +21,7 @@ func TestRoundTripCSV(t *testing.T) {
MultisigClosure: tree.MultisigClosure{ MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{seckey.PubKey()}, PubKeys: []*secp256k1.PublicKey{seckey.PubKey()},
}, },
Seconds: 1024, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
} }
leaf, err := csvSig.Script() leaf, err := csvSig.Script()
@@ -31,7 +33,7 @@ func TestRoundTripCSV(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, valid) require.True(t, valid)
require.Equal(t, csvSig.Seconds, cl.Seconds) require.Equal(t, csvSig.Locktime.Value, cl.Locktime.Value)
} }
func TestMultisigClosure(t *testing.T) { func TestMultisigClosure(t *testing.T) {
@@ -194,7 +196,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{ MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1}, PubKeys: []*secp256k1.PublicKey{pubkey1},
}, },
Seconds: 1024, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
} }
script, err := csvSig.Script() script, err := csvSig.Script()
@@ -204,7 +206,7 @@ func TestCSVSigClosure(t *testing.T) {
valid, err := decodedCSV.Decode(script) valid, err := decodedCSV.Decode(script)
require.NoError(t, err) require.NoError(t, err)
require.True(t, valid) require.True(t, valid)
require.Equal(t, uint32(1024), uint32(decodedCSV.Seconds)) require.Equal(t, uint32(1024), uint32(decodedCSV.Locktime.Value))
require.Equal(t, 1, len(decodedCSV.PubKeys)) require.Equal(t, 1, len(decodedCSV.PubKeys))
require.Equal(t, require.Equal(t,
schnorr.SerializePubKey(pubkey1), schnorr.SerializePubKey(pubkey1),
@@ -217,7 +219,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{ MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2}, PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
}, },
Seconds: 2016, // ~2 weeks Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512 * 4}, // ~2 weeks
} }
script, err := csvSig.Script() script, err := csvSig.Script()
@@ -227,7 +229,7 @@ func TestCSVSigClosure(t *testing.T) {
valid, err := decodedCSV.Decode(script) valid, err := decodedCSV.Decode(script)
require.NoError(t, err) require.NoError(t, err)
require.True(t, valid) require.True(t, valid)
require.Equal(t, uint32(2016), uint32(decodedCSV.Seconds)) require.Equal(t, uint32(512*4), uint32(decodedCSV.Locktime.Value))
require.Equal(t, 2, len(decodedCSV.PubKeys)) require.Equal(t, 2, len(decodedCSV.PubKeys))
require.Equal(t, require.Equal(t,
schnorr.SerializePubKey(pubkey1), schnorr.SerializePubKey(pubkey1),
@@ -265,7 +267,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{ MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2}, PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
}, },
Seconds: 1024, Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
} }
// Should be same as multisig witness size (64 bytes per signature) // Should be same as multisig witness size (64 bytes per signature)
require.Equal(t, 128, csvSig.WitnessSize()) require.Equal(t, 128, csvSig.WitnessSize())
@@ -276,7 +278,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{ MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1}, PubKeys: []*secp256k1.PublicKey{pubkey1},
}, },
Seconds: 65535, // Maximum allowed value Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: common.SECONDS_MAX}, // Maximum allowed value
} }
script, err := csvSig.Script() script, err := csvSig.Script()
@@ -286,7 +288,7 @@ func TestCSVSigClosure(t *testing.T) {
valid, err := decodedCSV.Decode(script) valid, err := decodedCSV.Decode(script)
require.NoError(t, err) require.NoError(t, err)
require.True(t, valid) require.True(t, valid)
require.Equal(t, uint32(65535), uint32(decodedCSV.Seconds)) require.Equal(t, uint32(common.SECONDS_MAX), decodedCSV.Locktime.Value)
}) })
} }
@@ -416,7 +418,7 @@ func TestCSVSigClosureWitness(t *testing.T) {
MultisigClosure: tree.MultisigClosure{ MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pub1}, PubKeys: []*secp256k1.PublicKey{pub1},
}, },
Seconds: 144, Locktime: common.Locktime{Type: common.LocktimeTypeBlock, Value: 144},
} }
witness, err := closure.Witness(controlBlock, signatures) witness, err := closure.Witness(controlBlock, signatures)
@@ -469,3 +471,156 @@ func TestDecodeChecksigAdd(t *testing.T) {
require.Equal(t, tree.MultisigTypeChecksigAdd, multisigClosure.Type, "expected MultisigTypeChecksigAdd") require.Equal(t, tree.MultisigTypeChecksigAdd, multisigClosure.Type, "expected MultisigTypeChecksigAdd")
require.Equal(t, 3, len(multisigClosure.PubKeys), "expected 3 public keys") require.Equal(t, 3, len(multisigClosure.PubKeys), "expected 3 public keys")
} }
func TestCLTVMultisigClosure(t *testing.T) {
// Generate test keys
privkey1, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubkey1 := privkey1.PubKey()
privkey2, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubkey2 := privkey2.PubKey()
locktime := common.Locktime{
Type: common.LocktimeTypeBlock,
Value: 100,
}
t.Run("valid single key with CLTV", func(t *testing.T) {
closure := &tree.CLTVMultisigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1},
Type: tree.MultisigTypeChecksig,
},
Locktime: locktime,
}
script, err := closure.Script()
require.NoError(t, err)
decodedClosure := &tree.CLTVMultisigClosure{}
valid, err := decodedClosure.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, closure.Locktime, decodedClosure.Locktime)
require.Equal(t, 1, len(decodedClosure.PubKeys))
require.True(t, closure.PubKeys[0].IsEqual(decodedClosure.PubKeys[0]))
})
t.Run("valid two keys with CLTV", func(t *testing.T) {
closure := &tree.CLTVMultisigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
Type: tree.MultisigTypeChecksig,
},
Locktime: locktime,
}
script, err := closure.Script()
require.NoError(t, err)
decodedClosure := &tree.CLTVMultisigClosure{}
valid, err := decodedClosure.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, closure.Locktime, decodedClosure.Locktime)
require.Equal(t, 2, len(decodedClosure.PubKeys))
})
t.Run("valid two keys with CLTV using checksigadd", func(t *testing.T) {
closure := &tree.CLTVMultisigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
Type: tree.MultisigTypeChecksigAdd,
},
Locktime: locktime,
}
script, err := closure.Script()
require.NoError(t, err)
decodedClosure := &tree.CLTVMultisigClosure{}
valid, err := decodedClosure.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, closure.Locktime, decodedClosure.Locktime)
require.Equal(t, closure.Type, decodedClosure.Type)
require.Equal(t, 2, len(decodedClosure.PubKeys))
})
t.Run("witness generation", func(t *testing.T) {
closure := &tree.CLTVMultisigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
Type: tree.MultisigTypeChecksig,
},
Locktime: locktime,
}
controlBlock := bytes.Repeat([]byte{0x00}, 32)
signatures := map[string][]byte{
hex.EncodeToString(schnorr.SerializePubKey(pubkey1)): bytes.Repeat([]byte{0x01}, 64),
hex.EncodeToString(schnorr.SerializePubKey(pubkey2)): bytes.Repeat([]byte{0x01}, 64),
}
witness, err := closure.Witness(controlBlock, signatures)
require.NoError(t, err)
require.Equal(t, 4, len(witness)) // 2 sigs + script + control block
script, err := closure.Script()
require.NoError(t, err)
require.Equal(t, script, witness[2])
require.Equal(t, controlBlock, witness[3])
})
t.Run("invalid cases", func(t *testing.T) {
validClosure := &tree.CLTVMultisigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1},
Type: tree.MultisigTypeChecksig,
},
Locktime: locktime,
}
script, err := validClosure.Script()
require.NoError(t, err)
emptyScriptErr := "empty script"
testCases := []struct {
name string
script []byte
err *string
}{
{
name: "empty script",
script: []byte{},
err: &emptyScriptErr,
},
{
name: "invalid CLTV index",
script: append([]byte{txscript.OP_CHECKLOCKTIMEVERIFY, txscript.OP_DROP}, script...),
},
{
name: "missing CLTV",
script: script[5:],
},
{
name: "invalid multisig after CLTV",
script: append(script[:len(script)-1], txscript.OP_CHECKSIGVERIFY),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
closure := &tree.CLTVMultisigClosure{}
valid, err := closure.Decode(tc.script)
require.False(t, valid)
if tc.err != nil {
require.Contains(t, err.Error(), *tc.err)
} else {
require.NoError(t, err)
}
})
}
})
}

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -72,7 +73,7 @@ func UnspendableKey() *secp256k1.PublicKey {
// - input and output amounts // - input and output amounts
func ValidateVtxoTree( func ValidateVtxoTree(
tree VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey, tree VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey,
roundLifetime int64, roundLifetime common.Locktime,
) error { ) error {
roundTransaction, err := psetv2.NewPsetFromBase64(roundTx) roundTransaction, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil { if err != nil {
@@ -148,7 +149,7 @@ func ValidateVtxoTree(
func validateNodeTransaction( func validateNodeTransaction(
node Node, tree VtxoTree, node Node, tree VtxoTree,
expectedInternalKey, expectedServerPubkey *secp256k1.PublicKey, expectedInternalKey, expectedServerPubkey *secp256k1.PublicKey,
expectedSequence int64, expectedLifetime common.Locktime,
) error { ) error {
if node.Tx == "" { if node.Tx == "" {
return ErrNodeTxEmpty return ErrNodeTxEmpty
@@ -242,7 +243,8 @@ func validateNodeTransaction(
schnorr.SerializePubKey(c.MultisigClosure.PubKeys[0]), schnorr.SerializePubKey(c.MultisigClosure.PubKeys[0]),
schnorr.SerializePubKey(expectedServerPubkey), schnorr.SerializePubKey(expectedServerPubkey),
) )
isSweepDelay := int64(c.Seconds) == expectedSequence
isSweepDelay := c.Locktime == expectedLifetime
if isServer && !isSweepDelay { if isServer && !isSweepDelay {
return ErrInvalidSweepSequence return ErrInvalidSweepSequence

View File

@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -17,7 +16,7 @@ var (
ErrNoExitLeaf = fmt.Errorf("no exit leaf") ErrNoExitLeaf = fmt.Errorf("no exit leaf")
) )
type VtxoScript common.VtxoScript[elementsTapTree, *MultisigClosure, *CSVSigClosure] type VtxoScript common.VtxoScript[elementsTapTree, Closure]
func ParseVtxoScript(scripts []string) (VtxoScript, error) { func ParseVtxoScript(scripts []string) (VtxoScript, error) {
v := &TapscriptsVtxoScript{} v := &TapscriptsVtxoScript{}
@@ -26,12 +25,12 @@ func ParseVtxoScript(scripts []string) (VtxoScript, error) {
return v, err return v, err
} }
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay uint) *TapscriptsVtxoScript { func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay common.Locktime) *TapscriptsVtxoScript {
return &TapscriptsVtxoScript{ return &TapscriptsVtxoScript{
[]Closure{ []Closure{
&CSVSigClosure{ &CSVSigClosure{
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner}}, MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner}},
Seconds: exitDelay, Locktime: exitDelay,
}, },
&MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner, server}}, &MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner, server}},
}, },
@@ -73,12 +72,17 @@ func (v *TapscriptsVtxoScript) Decode(scripts []string) error {
return nil return nil
} }
func (v *TapscriptsVtxoScript) Validate(server *secp256k1.PublicKey, minExitDelay uint) error { func (v *TapscriptsVtxoScript) Validate(server *secp256k1.PublicKey, minLocktime common.Locktime) error {
serverXonly := schnorr.SerializePubKey(server) serverXonly := schnorr.SerializePubKey(server)
for _, forfeit := range v.ForfeitClosures() { for _, forfeit := range v.ForfeitClosures() {
multisigClosure, ok := forfeit.(*MultisigClosure)
if !ok {
return fmt.Errorf("invalid forfeit closure, expected MultisigClosure")
}
// must contain server pubkey // must contain server pubkey
found := false found := false
for _, pubkey := range forfeit.PubKeys { for _, pubkey := range multisigClosure.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(pubkey), serverXonly) { if bytes.Equal(schnorr.SerializePubKey(pubkey), serverXonly) {
found = true found = true
break break
@@ -97,46 +101,48 @@ func (v *TapscriptsVtxoScript) Validate(server *secp256k1.PublicKey, minExitDela
return err return err
} }
if smallestExit < minExitDelay { if smallestExit.LessThan(minLocktime) {
return fmt.Errorf("exit delay is too short") return fmt.Errorf("exit delay is too short")
} }
return nil return nil
} }
func (v *TapscriptsVtxoScript) SmallestExitDelay() (uint, error) { func (v *TapscriptsVtxoScript) SmallestExitDelay() (*common.Locktime, error) {
smallest := uint(math.MaxUint32) var smallest *common.Locktime
for _, closure := range v.Closures { for _, closure := range v.Closures {
if csvClosure, ok := closure.(*CSVSigClosure); ok { if csvClosure, ok := closure.(*CSVSigClosure); ok {
if csvClosure.Seconds < smallest { if smallest == nil || csvClosure.Locktime.LessThan(*smallest) {
smallest = csvClosure.Seconds smallest = &csvClosure.Locktime
} }
} }
} }
if smallest == math.MaxUint32 { if smallest == nil {
return 0, ErrNoExitLeaf return nil, ErrNoExitLeaf
} }
return smallest, nil return smallest, nil
} }
func (v *TapscriptsVtxoScript) ForfeitClosures() []*MultisigClosure { func (v *TapscriptsVtxoScript) ForfeitClosures() []Closure {
forfeits := make([]*MultisigClosure, 0) forfeits := make([]Closure, 0)
for _, closure := range v.Closures { for _, closure := range v.Closures {
if multisigClosure, ok := closure.(*MultisigClosure); ok { switch closure.(type) {
forfeits = append(forfeits, multisigClosure) case *MultisigClosure, *CLTVMultisigClosure:
forfeits = append(forfeits, closure)
} }
} }
return forfeits return forfeits
} }
func (v *TapscriptsVtxoScript) ExitClosures() []*CSVSigClosure { func (v *TapscriptsVtxoScript) ExitClosures() []Closure {
exits := make([]*CSVSigClosure, 0) exits := make([]Closure, 0)
for _, closure := range v.Closures { for _, closure := range v.Closures {
if csvClosure, ok := closure.(*CSVSigClosure); ok { switch closure.(type) {
exits = append(exits, csvClosure) case *CSVSigClosure:
exits = append(exits, closure)
} }
} }
return exits return exits

View File

@@ -31,15 +31,17 @@ It may also contain others closures implementing specific use cases.
VtxoScript abstracts the taproot complexity behind vtxo contracts. VtxoScript abstracts the taproot complexity behind vtxo contracts.
it is compiled, transferred and parsed using descriptor string. it is compiled, transferred and parsed using descriptor string.
// TODO gather common and tree package to prevent circular dependency and move C generic
*/ */
type VtxoScript[T TaprootTree, F interface{}, E interface{}] interface { type VtxoScript[T TaprootTree, C interface{}] interface {
Validate(server *secp256k1.PublicKey, minExitDelay uint) error Validate(server *secp256k1.PublicKey, minLocktime Locktime) error
TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error) TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error)
Encode() ([]string, error) Encode() ([]string, error)
Decode(scripts []string) error Decode(scripts []string) error
SmallestExitDelay() (uint, error) SmallestExitDelay() (*Locktime, error)
ForfeitClosures() []F ForfeitClosures() []C
ExitClosures() []E ExitClosures() []C
} }
// BiggestLeafMerkleProof returns the leaf with the biggest witness size (for fee estimation) // BiggestLeafMerkleProof returns the leaf with the biggest witness size (for fee estimation)

View File

@@ -146,15 +146,25 @@ func (a *arkClient) initWithWallet(
return fmt.Errorf("failed to parse server pubkey: %s", err) return fmt.Errorf("failed to parse server pubkey: %s", err)
} }
lifetimeType := common.LocktimeTypeBlock
if info.RoundLifetime >= 512 {
lifetimeType = common.LocktimeTypeSecond
}
unilateralExitDelayType := common.LocktimeTypeBlock
if info.UnilateralExitDelay >= 512 {
unilateralExitDelayType = common.LocktimeTypeSecond
}
storeData := types.Config{ storeData := types.Config{
ServerUrl: args.ServerUrl, ServerUrl: args.ServerUrl,
ServerPubKey: serverPubkey, ServerPubKey: serverPubkey,
WalletType: args.Wallet.GetType(), WalletType: args.Wallet.GetType(),
ClientType: args.ClientType, ClientType: args.ClientType,
Network: network, Network: network,
RoundLifetime: info.RoundLifetime, RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(info.RoundLifetime)},
RoundInterval: info.RoundInterval, RoundInterval: info.RoundInterval,
UnilateralExitDelay: info.UnilateralExitDelay, UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(info.UnilateralExitDelay)},
Dust: info.Dust, Dust: info.Dust,
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate, BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
ForfeitAddress: info.ForfeitAddress, ForfeitAddress: info.ForfeitAddress,
@@ -213,15 +223,25 @@ func (a *arkClient) init(
return fmt.Errorf("failed to parse server pubkey: %s", err) return fmt.Errorf("failed to parse server pubkey: %s", err)
} }
lifetimeType := common.LocktimeTypeBlock
if info.RoundLifetime >= 512 {
lifetimeType = common.LocktimeTypeSecond
}
unilateralExitDelayType := common.LocktimeTypeBlock
if info.UnilateralExitDelay >= 512 {
unilateralExitDelayType = common.LocktimeTypeSecond
}
cfgData := types.Config{ cfgData := types.Config{
ServerUrl: args.ServerUrl, ServerUrl: args.ServerUrl,
ServerPubKey: serverPubkey, ServerPubKey: serverPubkey,
WalletType: args.WalletType, WalletType: args.WalletType,
ClientType: args.ClientType, ClientType: args.ClientType,
Network: network, Network: network,
RoundLifetime: info.RoundLifetime, RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(info.RoundLifetime)},
RoundInterval: info.RoundInterval, RoundInterval: info.RoundInterval,
UnilateralExitDelay: info.UnilateralExitDelay, UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(info.UnilateralExitDelay)},
Dust: info.Dust, Dust: info.Dust,
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate, BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
ExplorerURL: args.ExplorerURL, ExplorerURL: args.ExplorerURL,
@@ -347,8 +367,8 @@ func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error)
} }
} }
func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time { func getCreatedAtFromExpiry(roundLifetime common.Locktime, expiry time.Time) time.Time {
return expiry.Add(-time.Duration(roundLifetime) * time.Second) return expiry.Add(-time.Duration(roundLifetime.Seconds()) * time.Second)
} }
func filterByOutpoints(vtxos []client.Vtxo, outpoints []client.Outpoint) []client.Vtxo { func filterByOutpoints(vtxos []client.Vtxo, outpoints []client.Outpoint) []client.Vtxo {

View File

@@ -708,7 +708,7 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context, opts
} }
} }
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts) u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
continue continue
} }
@@ -1535,7 +1535,7 @@ func (a *covenantArkClient) coinSelectOnchain(
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts) u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }
@@ -1571,7 +1571,7 @@ func (a *covenantArkClient) coinSelectOnchain(
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts) u := utxo.ToUtxo(a.UnilateralExitDelay, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }
@@ -1719,7 +1719,7 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []
} }
func vtxosToTxsCovenant( func vtxosToTxsCovenant(
roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []types.Transaction, roundLifetime common.Locktime, spendable, spent []client.Vtxo, boardingTxs []types.Transaction,
) ([]types.Transaction, error) { ) ([]types.Transaction, error) {
transactions := make([]types.Transaction, 0) transactions := make([]types.Transaction, 0)
unconfirmedBoardingTxs := make([]types.Transaction, 0) unconfirmedBoardingTxs := make([]types.Transaction, 0)

View File

@@ -1703,7 +1703,7 @@ func (a *covenantlessArkClient) handleRoundSigningStarted(
) (signerSession bitcointree.SignerSession, err error) { ) (signerSession bitcointree.SignerSession, err error) {
sweepClosure := tree.CSVSigClosure{ sweepClosure := tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{a.ServerPubKey}}, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{a.ServerPubKey}},
Seconds: uint(a.RoundLifetime), Locktime: a.RoundLifetime,
} }
script, err := sweepClosure.Script() script, err := sweepClosure.Script()
@@ -2163,7 +2163,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts) u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }
@@ -2199,7 +2199,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts) u := utxo.ToUtxo(a.UnilateralExitDelay, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }
@@ -2387,7 +2387,7 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context, o
} }
} }
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts) u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
continue continue
} }
@@ -2682,5 +2682,5 @@ func buildRedeemTx(
}) })
} }
return bitcointree.BuildRedeemTx(ins, outs, fees) return bitcointree.BuildRedeemTx(ins, outs)
} }

View File

@@ -53,7 +53,7 @@ type SpentStatus struct {
SpentBy string `json:"txid,omitempty"` SpentBy string `json:"txid,omitempty"`
} }
func (e ExplorerUtxo) ToUtxo(delay uint, tapscripts []string) types.Utxo { func (e ExplorerUtxo) ToUtxo(delay common.Locktime, tapscripts []string) types.Utxo {
return newUtxo(e, delay, tapscripts) return newUtxo(e, delay, tapscripts)
} }
@@ -65,7 +65,7 @@ type Explorer interface {
GetUtxos(addr string) ([]ExplorerUtxo, error) GetUtxos(addr string) ([]ExplorerUtxo, error)
GetBalance(addr string) (uint64, error) GetBalance(addr string) (uint64, error)
GetRedeemedVtxosBalance( GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64, addr string, unilateralExitDelay common.Locktime,
) (uint64, map[int64]uint64, error) ) (uint64, map[int64]uint64, error)
GetTxBlockTime( GetTxBlockTime(
txid string, txid string,
@@ -247,7 +247,7 @@ func (e *explorerSvc) GetBalance(addr string) (uint64, error) {
} }
func (e *explorerSvc) GetRedeemedVtxosBalance( func (e *explorerSvc) GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64, addr string, unilateralExitDelay common.Locktime,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) { ) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr) utxos, err := e.GetUtxos(addr)
if err != nil { if err != nil {
@@ -262,7 +262,7 @@ func (e *explorerSvc) GetRedeemedVtxosBalance(
blocktime = time.Unix(utxo.Status.Blocktime, 0) blocktime = time.Unix(utxo.Status.Blocktime, 0)
} }
delay := time.Duration(unilateralExitDelay) * time.Second delay := time.Duration(unilateralExitDelay.Seconds()) * time.Second
availableAt := blocktime.Add(delay) availableAt := blocktime.Add(delay)
if availableAt.After(now) { if availableAt.After(now) {
if _, ok := lockedBalance[availableAt.Unix()]; !ok { if _, ok := lockedBalance[availableAt.Unix()]; !ok {
@@ -415,7 +415,7 @@ func parseBitcoinTx(txStr string) (string, string, error) {
return txhex, txid, nil return txhex, txid, nil
} }
func newUtxo(explorerUtxo ExplorerUtxo, delay uint, tapscripts []string) types.Utxo { func newUtxo(explorerUtxo ExplorerUtxo, delay common.Locktime, tapscripts []string) types.Utxo {
utxoTime := explorerUtxo.Status.Blocktime utxoTime := explorerUtxo.Status.Blocktime
createdAt := time.Unix(utxoTime, 0) createdAt := time.Unix(utxoTime, 0)
if utxoTime == 0 { if utxoTime == 0 {
@@ -429,7 +429,7 @@ func newUtxo(explorerUtxo ExplorerUtxo, delay uint, tapscripts []string) types.U
Amount: explorerUtxo.Amount, Amount: explorerUtxo.Amount,
Asset: explorerUtxo.Asset, Asset: explorerUtxo.Asset,
Delay: delay, Delay: delay,
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second), SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay.Seconds()) * time.Second),
CreatedAt: createdAt, CreatedAt: createdAt,
Tapscripts: tapscripts, Tapscripts: tapscripts,
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -26,12 +27,12 @@ func NewCovenantRedeemBranch(
explorer explorer.Explorer, explorer explorer.Explorer,
vtxoTree tree.VtxoTree, vtxo client.Vtxo, vtxoTree tree.VtxoTree, vtxo client.Vtxo,
) (*CovenantRedeemBranch, error) { ) (*CovenantRedeemBranch, error) {
sweepClosure, seconds, err := findCovenantSweepClosure(vtxoTree) sweepClosure, locktime, err := findCovenantSweepClosure(vtxoTree)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds)) lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", locktime.Seconds()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -183,19 +184,19 @@ func (r *CovenantRedeemBranch) offchainPath() ([]*psetv2.Pset, error) {
func findCovenantSweepClosure( func findCovenantSweepClosure(
vtxoTree tree.VtxoTree, vtxoTree tree.VtxoTree,
) (*taproot.TapElementsLeaf, uint, error) { ) (*taproot.TapElementsLeaf, *common.Locktime, error) {
root, err := vtxoTree.Root() root, err := vtxoTree.Root()
if err != nil { if err != nil {
return nil, 0, err return nil, nil, err
} }
// find the sweep closure // find the sweep closure
tx, err := psetv2.NewPsetFromBase64(root.Tx) tx, err := psetv2.NewPsetFromBase64(root.Tx)
if err != nil { if err != nil {
return nil, 0, err return nil, nil, err
} }
var seconds uint var locktime *common.Locktime
var sweepClosure *taproot.TapElementsLeaf var sweepClosure *taproot.TapElementsLeaf
for _, tapLeaf := range tx.Inputs[0].TapLeafScript { for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
closure := &tree.CSVSigClosure{} closure := &tree.CSVSigClosure{}
@@ -204,15 +205,15 @@ func findCovenantSweepClosure(
continue continue
} }
if valid && closure.Seconds > seconds { if valid && (locktime == nil || closure.Locktime.LessThan(*locktime)) {
seconds = closure.Seconds locktime = &closure.Locktime
sweepClosure = &tapLeaf.TapElementsLeaf sweepClosure = &tapLeaf.TapElementsLeaf
} }
} }
if sweepClosure == nil { if sweepClosure == nil {
return nil, 0, fmt.Errorf("sweep closure not found") return nil, nil, fmt.Errorf("sweep closure not found")
} }
return sweepClosure, seconds, nil return sweepClosure, locktime, nil
} }

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -25,12 +26,12 @@ func NewCovenantlessRedeemBranch(
explorer explorer.Explorer, explorer explorer.Explorer,
vtxoTree tree.VtxoTree, vtxo client.Vtxo, vtxoTree tree.VtxoTree, vtxo client.Vtxo,
) (*CovenantlessRedeemBranch, error) { ) (*CovenantlessRedeemBranch, error) {
_, seconds, err := findCovenantlessSweepClosure(vtxoTree) _, locktime, err := findCovenantlessSweepClosure(vtxoTree)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds)) lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", locktime.Seconds()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -156,19 +157,19 @@ func (r *CovenantlessRedeemBranch) OffchainPath() ([]*psbt.Packet, error) {
func findCovenantlessSweepClosure( func findCovenantlessSweepClosure(
vtxoTree tree.VtxoTree, vtxoTree tree.VtxoTree,
) (*txscript.TapLeaf, uint, error) { ) (*txscript.TapLeaf, *common.Locktime, error) {
root, err := vtxoTree.Root() root, err := vtxoTree.Root()
if err != nil { if err != nil {
return nil, 0, err return nil, nil, err
} }
// find the sweep closure // find the sweep closure
tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true) tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true)
if err != nil { if err != nil {
return nil, 0, err return nil, nil, err
} }
var seconds uint var locktime *common.Locktime
var sweepClosure *txscript.TapLeaf var sweepClosure *txscript.TapLeaf
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript { for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
closure := &tree.CSVSigClosure{} closure := &tree.CSVSigClosure{}
@@ -177,16 +178,16 @@ func findCovenantlessSweepClosure(
continue continue
} }
if valid && closure.Seconds > seconds { if valid && (locktime == nil || closure.Locktime.LessThan(*locktime)) {
seconds = closure.Seconds locktime = &closure.Locktime
leaf := txscript.NewBaseTapLeaf(tapLeaf.Script) leaf := txscript.NewBaseTapLeaf(tapLeaf.Script)
sweepClosure = &leaf sweepClosure = &leaf
} }
} }
if sweepClosure == nil { if sweepClosure == nil {
return nil, 0, fmt.Errorf("sweep closure not found") return nil, nil, fmt.Errorf("sweep closure not found")
} }
return sweepClosure, seconds, nil return sweepClosure, locktime, nil
} }

View File

@@ -57,9 +57,9 @@ func (s *configStore) AddData(ctx context.Context, data types.Config) error {
WalletType: data.WalletType, WalletType: data.WalletType,
ClientType: data.ClientType, ClientType: data.ClientType,
Network: data.Network.Name, Network: data.Network.Name,
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime), RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime.Value),
RoundInterval: fmt.Sprintf("%d", data.RoundInterval), RoundInterval: fmt.Sprintf("%d", data.RoundInterval),
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay), UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay.Value),
Dust: fmt.Sprintf("%d", data.Dust), Dust: fmt.Sprintf("%d", data.Dust),
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate, BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
ExplorerURL: data.ExplorerURL, ExplorerURL: data.ExplorerURL,

View File

@@ -4,6 +4,7 @@ import (
"encoding/hex" "encoding/hex"
"strconv" "strconv"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/types" "github.com/ark-network/ark/pkg/client-sdk/types"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -44,14 +45,25 @@ func (d storeData) decode() types.Config {
buf, _ := hex.DecodeString(d.ServerPubKey) buf, _ := hex.DecodeString(d.ServerPubKey)
serverPubkey, _ := secp256k1.ParsePubKey(buf) serverPubkey, _ := secp256k1.ParsePubKey(buf)
explorerURL := d.ExplorerURL explorerURL := d.ExplorerURL
lifetimeType := common.LocktimeTypeBlock
if roundLifetime >= 512 {
lifetimeType = common.LocktimeTypeSecond
}
unilateralExitDelayType := common.LocktimeTypeBlock
if unilateralExitDelay >= 512 {
unilateralExitDelayType = common.LocktimeTypeSecond
}
return types.Config{ return types.Config{
ServerUrl: d.ServerUrl, ServerUrl: d.ServerUrl,
ServerPubKey: serverPubkey, ServerPubKey: serverPubkey,
WalletType: d.WalletType, WalletType: d.WalletType,
ClientType: d.ClientType, ClientType: d.ClientType,
Network: network, Network: network,
RoundLifetime: int64(roundLifetime), RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(roundLifetime)},
UnilateralExitDelay: int64(unilateralExitDelay), UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(unilateralExitDelay)},
RoundInterval: int64(roundInterval), RoundInterval: int64(roundInterval),
Dust: uint64(dust), Dust: uint64(dust),
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate, BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,

View File

@@ -26,9 +26,9 @@ func TestStore(t *testing.T) {
WalletType: wallet.SingleKeyWallet, WalletType: wallet.SingleKeyWallet,
ClientType: client.GrpcClient, ClientType: client.GrpcClient,
Network: common.LiquidRegTest, Network: common.LiquidRegTest,
RoundLifetime: 512, RoundLifetime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
RoundInterval: 10, RoundInterval: 10,
UnilateralExitDelay: 512, UnilateralExitDelay: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Dust: 1000, Dust: 1000,
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })", BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
ForfeitAddress: "bcrt1qzvqj", ForfeitAddress: "bcrt1qzvqj",

View File

@@ -21,9 +21,9 @@ type Config struct {
WalletType string WalletType string
ClientType string ClientType string
Network common.Network Network common.Network
RoundLifetime int64 RoundLifetime common.Locktime
RoundInterval int64 RoundInterval int64
UnilateralExitDelay int64 UnilateralExitDelay common.Locktime
Dust uint64 Dust uint64
BoardingDescriptorTemplate string BoardingDescriptorTemplate string
ExplorerURL string ExplorerURL string
@@ -110,7 +110,7 @@ type Utxo struct {
VOut uint32 VOut uint32
Amount uint64 Amount uint64
Asset string // liquid only Asset string // liquid only
Delay uint Delay common.Locktime
SpendableAt time.Time SpendableAt time.Time
CreatedAt time.Time CreatedAt time.Time
Tapscripts []string Tapscripts []string

View File

@@ -220,6 +220,13 @@ func (s *bitcoinWallet) SignTransaction(
break break
} }
} }
case *tree.CLTVMultisigClosure:
for _, key := range c.MultisigClosure.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
sign = true
break
}
}
} }
if sign { if sign {
@@ -310,7 +317,7 @@ func (w *bitcoinWallet) getAddress(
defaultVtxoScript := bitcointree.NewDefaultVtxoScript( defaultVtxoScript := bitcointree.NewDefaultVtxoScript(
w.walletData.PubKey, w.walletData.PubKey,
data.ServerPubKey, data.ServerPubKey,
uint(data.UnilateralExitDelay), data.UnilateralExitDelay,
) )
vtxoTapKey, _, err := defaultVtxoScript.TapTree() vtxoTapKey, _, err := defaultVtxoScript.TapTree()
@@ -327,7 +334,10 @@ func (w *bitcoinWallet) getAddress(
boardingVtxoScript := bitcointree.NewDefaultVtxoScript( boardingVtxoScript := bitcointree.NewDefaultVtxoScript(
w.walletData.PubKey, w.walletData.PubKey,
data.ServerPubKey, data.ServerPubKey,
uint(data.UnilateralExitDelay*2), common.Locktime{
Type: data.UnilateralExitDelay.Type,
Value: data.UnilateralExitDelay.Value * 2,
},
) )
boardingTapKey, _, err := boardingVtxoScript.TapTree() boardingTapKey, _, err := boardingVtxoScript.TapTree()

View File

@@ -242,6 +242,13 @@ func (s *liquidWallet) SignTransaction(
break break
} }
} }
case *tree.CLTVMultisigClosure:
for _, key := range c.MultisigClosure.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
sign = true
break
}
}
} }
if sign { if sign {
@@ -332,7 +339,7 @@ func (w *liquidWallet) getAddress(
vtxoScript := tree.NewDefaultVtxoScript( vtxoScript := tree.NewDefaultVtxoScript(
w.walletData.PubKey, w.walletData.PubKey,
data.ServerPubKey, data.ServerPubKey,
uint(data.UnilateralExitDelay), data.UnilateralExitDelay,
) )
vtxoTapKey, _, err := vtxoScript.TapTree() vtxoTapKey, _, err := vtxoScript.TapTree()
@@ -349,7 +356,10 @@ func (w *liquidWallet) getAddress(
boardingVtxoScript := tree.NewDefaultVtxoScript( boardingVtxoScript := tree.NewDefaultVtxoScript(
w.walletData.PubKey, w.walletData.PubKey,
data.ServerPubKey, data.ServerPubKey,
uint(data.UnilateralExitDelay*2), common.Locktime{
Type: data.UnilateralExitDelay.Type,
Value: data.UnilateralExitDelay.Value * 2,
},
) )
boardingTapKey, _, err := boardingVtxoScript.TapTree() boardingTapKey, _, err := boardingVtxoScript.TapTree()

View File

@@ -26,9 +26,9 @@ func TestWallet(t *testing.T) {
WalletType: wallet.SingleKeyWallet, WalletType: wallet.SingleKeyWallet,
ClientType: client.GrpcClient, ClientType: client.GrpcClient,
Network: common.LiquidRegTest, Network: common.LiquidRegTest,
RoundLifetime: 512, RoundLifetime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
RoundInterval: 10, RoundInterval: 10,
UnilateralExitDelay: 512, UnilateralExitDelay: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Dust: 1000, Dust: 1000,
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })", BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
ForfeitAddress: "bcrt1qzvqj", ForfeitAddress: "bcrt1qzvqj",

View File

@@ -11,6 +11,7 @@ import (
"strconv" "strconv"
"syscall/js" "syscall/js"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/types" "github.com/ark-network/ark/pkg/client-sdk/types"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -59,9 +60,9 @@ func (s *configStore) AddData(ctx context.Context, data types.Config) error {
WalletType: data.WalletType, WalletType: data.WalletType,
ClientType: data.ClientType, ClientType: data.ClientType,
Network: data.Network.Name, Network: data.Network.Name,
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime), RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime.Value),
RoundInterval: fmt.Sprintf("%d", data.RoundInterval), RoundInterval: fmt.Sprintf("%d", data.RoundInterval),
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay), UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay.Value),
Dust: fmt.Sprintf("%d", data.Dust), Dust: fmt.Sprintf("%d", data.Dust),
ExplorerURL: data.ExplorerURL, ExplorerURL: data.ExplorerURL,
ForfeitAddress: data.ForfeitAddress, ForfeitAddress: data.ForfeitAddress,
@@ -94,15 +95,25 @@ func (s *configStore) GetData(ctx context.Context) (*types.Config, error) {
dust, _ := strconv.Atoi(s.store.Call("getItem", "dust").String()) dust, _ := strconv.Atoi(s.store.Call("getItem", "dust").String())
withTxFeed, _ := strconv.ParseBool(s.store.Call("getItem", "with_transaction_feed").String()) withTxFeed, _ := strconv.ParseBool(s.store.Call("getItem", "with_transaction_feed").String())
lifetimeType := common.LocktimeTypeBlock
if roundLifetime >= 512 {
lifetimeType = common.LocktimeTypeSecond
}
unilateralExitDelayType := common.LocktimeTypeBlock
if unilateralExitDelay >= 512 {
unilateralExitDelayType = common.LocktimeTypeSecond
}
return &types.Config{ return &types.Config{
ServerUrl: s.store.Call("getItem", "server_url").String(), ServerUrl: s.store.Call("getItem", "server_url").String(),
ServerPubKey: serverPubkey, ServerPubKey: serverPubkey,
WalletType: s.store.Call("getItem", "wallet_type").String(), WalletType: s.store.Call("getItem", "wallet_type").String(),
ClientType: s.store.Call("getItem", "client_type").String(), ClientType: s.store.Call("getItem", "client_type").String(),
Network: network, Network: network,
RoundLifetime: int64(roundLifetime), RoundLifetime: common.Locktime{Value: uint32(roundLifetime), Type: lifetimeType},
RoundInterval: int64(roundInterval), RoundInterval: int64(roundInterval),
UnilateralExitDelay: int64(unilateralExitDelay), UnilateralExitDelay: common.Locktime{Value: uint32(unilateralExitDelay), Type: unilateralExitDelayType},
Dust: uint64(dust), Dust: uint64(dust),
ExplorerURL: s.store.Call("getItem", "explorer_url").String(), ExplorerURL: s.store.Call("getItem", "explorer_url").String(),
ForfeitAddress: s.store.Call("getItem", "forfeit_address").String(), ForfeitAddress: s.store.Call("getItem", "forfeit_address").String(),

View File

@@ -374,7 +374,7 @@ func GetRoundLifetimeWrapper() js.Func {
data, _ := arkSdkClient.GetConfigData(context.Background()) data, _ := arkSdkClient.GetConfigData(context.Background())
var roundLifettime int64 var roundLifettime int64
if data != nil { if data != nil {
roundLifettime = data.RoundLifetime roundLifettime = data.RoundLifetime.Seconds()
} }
return js.ValueOf(roundLifettime) return js.ValueOf(roundLifettime)
}) })
@@ -385,7 +385,7 @@ func GetUnilateralExitDelayWrapper() js.Func {
data, _ := arkSdkClient.GetConfigData(context.Background()) data, _ := arkSdkClient.GetConfigData(context.Background())
var unilateralExitDelay int64 var unilateralExitDelay int64
if data != nil { if data != nil {
unilateralExitDelay = data.UnilateralExitDelay unilateralExitDelay = data.UnilateralExitDelay.Seconds()
} }
return js.ValueOf(unilateralExitDelay) return js.ValueOf(unilateralExitDelay)
}) })

View File

@@ -59,6 +59,17 @@ func mainAction(_ *cli.Context) error {
TLSExtraDomains: cfg.TLSExtraDomains, TLSExtraDomains: cfg.TLSExtraDomains,
} }
lifetimeType, unilateralExitType, boardingExitType := common.LocktimeTypeBlock, common.LocktimeTypeBlock, common.LocktimeTypeBlock
if cfg.RoundLifetime >= 512 {
lifetimeType = common.LocktimeTypeSecond
}
if cfg.UnilateralExitDelay >= 512 {
unilateralExitType = common.LocktimeTypeSecond
}
if cfg.BoardingExitDelay >= 512 {
boardingExitType = common.LocktimeTypeSecond
}
appConfig := &appconfig.Config{ appConfig := &appconfig.Config{
EventDbType: cfg.EventDbType, EventDbType: cfg.EventDbType,
DbType: cfg.DbType, DbType: cfg.DbType,
@@ -70,8 +81,8 @@ func mainAction(_ *cli.Context) error {
SchedulerType: cfg.SchedulerType, SchedulerType: cfg.SchedulerType,
TxBuilderType: cfg.TxBuilderType, TxBuilderType: cfg.TxBuilderType,
WalletAddr: cfg.WalletAddr, WalletAddr: cfg.WalletAddr,
RoundLifetime: cfg.RoundLifetime, RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(cfg.RoundLifetime)},
UnilateralExitDelay: cfg.UnilateralExitDelay, UnilateralExitDelay: common.Locktime{Type: unilateralExitType, Value: uint32(cfg.UnilateralExitDelay)},
EsploraURL: cfg.EsploraURL, EsploraURL: cfg.EsploraURL,
NeutrinoPeer: cfg.NeutrinoPeer, NeutrinoPeer: cfg.NeutrinoPeer,
BitcoindRpcUser: cfg.BitcoindRpcUser, BitcoindRpcUser: cfg.BitcoindRpcUser,
@@ -79,7 +90,7 @@ func mainAction(_ *cli.Context) error {
BitcoindRpcHost: cfg.BitcoindRpcHost, BitcoindRpcHost: cfg.BitcoindRpcHost,
BitcoindZMQBlock: cfg.BitcoindZMQBlock, BitcoindZMQBlock: cfg.BitcoindZMQBlock,
BitcoindZMQTx: cfg.BitcoindZMQTx, BitcoindZMQTx: cfg.BitcoindZMQTx,
BoardingExitDelay: cfg.BoardingExitDelay, BoardingExitDelay: common.Locktime{Type: boardingExitType, Value: uint32(cfg.BoardingExitDelay)},
UnlockerType: cfg.UnlockerType, UnlockerType: cfg.UnlockerType,
UnlockerFilePath: cfg.UnlockerFilePath, UnlockerFilePath: cfg.UnlockerFilePath,
UnlockerPassword: cfg.UnlockerPassword, UnlockerPassword: cfg.UnlockerPassword,

View File

@@ -65,9 +65,9 @@ type Config struct {
SchedulerType string SchedulerType string
TxBuilderType string TxBuilderType string
WalletAddr string WalletAddr string
RoundLifetime int64 RoundLifetime common.Locktime
UnilateralExitDelay int64 UnilateralExitDelay common.Locktime
BoardingExitDelay int64 BoardingExitDelay common.Locktime
NostrDefaultRelays []string NostrDefaultRelays []string
NoteUriPrefix string NoteUriPrefix string
MarketHourStartTime time.Time MarketHourStartTime time.Time
@@ -119,18 +119,18 @@ func (c *Config) Validate() error {
if !supportedNetworks.supports(c.Network.Name) { if !supportedNetworks.supports(c.Network.Name) {
return fmt.Errorf("invalid network, must be one of: %s", supportedNetworks) return fmt.Errorf("invalid network, must be one of: %s", supportedNetworks)
} }
if c.RoundLifetime < minAllowedSequence { if c.RoundLifetime.Type == common.LocktimeTypeBlock {
if c.SchedulerType != "block" { if c.SchedulerType != "block" {
return fmt.Errorf("scheduler type must be block if round lifetime is expressed in blocks") return fmt.Errorf("scheduler type must be block if round lifetime is expressed in blocks")
} }
} else { } else { // seconds
if c.SchedulerType != "gocron" { if c.SchedulerType != "gocron" {
return fmt.Errorf("scheduler type must be gocron if round lifetime is expressed in seconds") return fmt.Errorf("scheduler type must be gocron if round lifetime is expressed in seconds")
} }
// round life time must be a multiple of 512 if expressed in seconds // round life time must be a multiple of 512 if expressed in seconds
if c.RoundLifetime%minAllowedSequence != 0 { if c.RoundLifetime.Value%minAllowedSequence != 0 {
c.RoundLifetime -= c.RoundLifetime % minAllowedSequence c.RoundLifetime.Value -= c.RoundLifetime.Value % minAllowedSequence
log.Infof( log.Infof(
"round lifetime must be a multiple of %d, rounded to %d", "round lifetime must be a multiple of %d, rounded to %d",
minAllowedSequence, c.RoundLifetime, minAllowedSequence, c.RoundLifetime,
@@ -138,28 +138,28 @@ func (c *Config) Validate() error {
} }
} }
if c.UnilateralExitDelay < minAllowedSequence { if c.UnilateralExitDelay.Type == common.LocktimeTypeBlock {
return fmt.Errorf( return fmt.Errorf(
"invalid unilateral exit delay, must at least %d", minAllowedSequence, "invalid unilateral exit delay, must at least %d", minAllowedSequence,
) )
} }
if c.BoardingExitDelay < minAllowedSequence { if c.BoardingExitDelay.Type == common.LocktimeTypeBlock {
return fmt.Errorf( return fmt.Errorf(
"invalid boarding exit delay, must at least %d", minAllowedSequence, "invalid boarding exit delay, must at least %d", minAllowedSequence,
) )
} }
if c.UnilateralExitDelay%minAllowedSequence != 0 { if c.UnilateralExitDelay.Value%minAllowedSequence != 0 {
c.UnilateralExitDelay -= c.UnilateralExitDelay % minAllowedSequence c.UnilateralExitDelay.Value -= c.UnilateralExitDelay.Value % minAllowedSequence
log.Infof( log.Infof(
"unilateral exit delay must be a multiple of %d, rounded to %d", "unilateral exit delay must be a multiple of %d, rounded to %d",
minAllowedSequence, c.UnilateralExitDelay, minAllowedSequence, c.UnilateralExitDelay,
) )
} }
if c.BoardingExitDelay%minAllowedSequence != 0 { if c.BoardingExitDelay.Value%minAllowedSequence != 0 {
c.BoardingExitDelay -= c.BoardingExitDelay % minAllowedSequence c.BoardingExitDelay.Value -= c.BoardingExitDelay.Value % minAllowedSequence
log.Infof( log.Infof(
"boarding exit delay must be a multiple of %d, rounded to %d", "boarding exit delay must be a multiple of %d, rounded to %d",
minAllowedSequence, c.BoardingExitDelay, minAllowedSequence, c.BoardingExitDelay,
@@ -260,7 +260,7 @@ func (c *Config) repoManager() error {
func (c *Config) walletService() error { func (c *Config) walletService() error {
if common.IsLiquid(c.Network) { if common.IsLiquid(c.Network) {
svc, err := liquidwallet.NewService(c.WalletAddr) svc, err := liquidwallet.NewService(c.WalletAddr, c.EsploraURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to connect to wallet: %s", err) return fmt.Errorf("failed to connect to wallet: %s", err)
} }
@@ -384,7 +384,7 @@ func (c *Config) appService() error {
func (c *Config) adminService() error { func (c *Config) adminService() error {
unit := ports.UnixTime unit := ports.UnixTime
if c.RoundLifetime < minAllowedSequence { if c.RoundLifetime.Value < minAllowedSequence {
unit = ports.BlockHeight unit = ports.BlockHeight
} }

View File

@@ -31,10 +31,10 @@ var (
type covenantService struct { type covenantService struct {
network common.Network network common.Network
pubkey *secp256k1.PublicKey pubkey *secp256k1.PublicKey
roundLifetime int64
roundInterval int64 roundInterval int64
unilateralExitDelay int64 roundLifetime common.Locktime
boardingExitDelay int64 unilateralExitDelay common.Locktime
boardingExitDelay common.Locktime
nostrDefaultRelays []string nostrDefaultRelays []string
@@ -57,7 +57,8 @@ type covenantService struct {
func NewCovenantService( func NewCovenantService(
network common.Network, network common.Network,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, roundInterval int64,
roundLifetime, unilateralExitDelay, boardingExitDelay common.Locktime,
nostrDefaultRelays []string, nostrDefaultRelays []string,
walletSvc ports.WalletService, repoManager ports.RepoManager, walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner, builder ports.TxBuilder, scanner ports.BlockchainScanner,
@@ -162,7 +163,7 @@ func (s *covenantService) Stop() {
} }
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, []string, error) { func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, []string, error) {
vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, uint(s.boardingExitDelay)) vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, s.boardingExitDelay)
tapKey, _, err := vtxoScript.TapTree() tapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
@@ -235,7 +236,7 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
} }
// if the exit path is available, forbid registering the boarding utxo // if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(exitDelay) < now { if blocktime+exitDelay.Seconds() < now {
return "", fmt.Errorf("tx %s expired", input.Txid) return "", fmt.Errorf("tx %s expired", input.Txid)
} }
@@ -332,7 +333,7 @@ func (s *covenantService) newBoardingInput(tx *transaction.Transaction, input po
return nil, fmt.Errorf("descriptor does not match script in transaction output") return nil, fmt.Errorf("descriptor does not match script in transaction output")
} }
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil { if err := boardingScript.Validate(s.pubkey, s.unilateralExitDelay); err != nil {
return nil, err return nil, err
} }
@@ -457,8 +458,8 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
return &ServiceInfo{ return &ServiceInfo{
PubKey: pubkey, PubKey: pubkey,
RoundLifetime: s.roundLifetime, RoundLifetime: int64(s.roundLifetime.Value),
UnilateralExitDelay: s.unilateralExitDelay, UnilateralExitDelay: int64(s.unilateralExitDelay.Value),
RoundInterval: s.roundInterval, RoundInterval: s.roundInterval,
Network: s.network.Name, Network: s.network.Name,
Dust: dust, Dust: dust,
@@ -1003,7 +1004,7 @@ func (s *covenantService) scheduleSweepVtxosForRound(round *domain.Round) {
return return
} }
expirationTime := s.sweeper.scheduler.AddNow(s.roundLifetime) expirationTime := s.sweeper.scheduler.AddNow(int64(s.roundLifetime.Value))
if err := s.sweeper.schedule( if err := s.sweeper.schedule(
expirationTime, round.Txid, round.VtxoTree, expirationTime, round.Txid, round.VtxoTree,

View File

@@ -33,10 +33,10 @@ const marketHourDelta = 5 * time.Minute
type covenantlessService struct { type covenantlessService struct {
network common.Network network common.Network
pubkey *secp256k1.PublicKey pubkey *secp256k1.PublicKey
roundLifetime int64 roundLifetime common.Locktime
roundInterval int64 roundInterval int64
unilateralExitDelay int64 unilateralExitDelay common.Locktime
boardingExitDelay int64 boardingExitDelay common.Locktime
nostrDefaultRelays []string nostrDefaultRelays []string
@@ -61,7 +61,8 @@ type covenantlessService struct {
func NewCovenantlessService( func NewCovenantlessService(
network common.Network, network common.Network,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, roundInterval int64,
roundLifetime, unilateralExitDelay, boardingExitDelay common.Locktime,
nostrDefaultRelays []string, nostrDefaultRelays []string,
walletSvc ports.WalletService, repoManager ports.RepoManager, walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner, builder ports.TxBuilder, scanner ports.BlockchainScanner,
@@ -303,6 +304,35 @@ func (s *covenantlessService) SubmitRedeemTx(
return "", fmt.Errorf("witness utxo value mismatch") return "", fmt.Errorf("witness utxo value mismatch")
} }
// verify forfeit closure script
closure, err := tree.DecodeClosure(tapscript.Script)
if err != nil {
return "", fmt.Errorf("failed to decode forfeit closure: %s", err)
}
switch c := closure.(type) {
case *tree.CLTVMultisigClosure:
blocktimestamp, err := s.wallet.GetCurrentBlockTime(ctx)
if err != nil {
return "", fmt.Errorf("failed to get current block time: %s", err)
}
switch c.Locktime.Type {
case common.LocktimeTypeBlock:
if c.Locktime.Value > blocktimestamp.Height {
return "", fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block height)", c.Locktime.Value, blocktimestamp.Height)
}
case common.LocktimeTypeSecond:
if c.Locktime.Value > uint32(blocktimestamp.Time) {
return "", fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block time)", c.Locktime.Value, blocktimestamp.Time)
}
}
case *tree.MultisigClosure:
// prevent failure in case of multisig closure
default:
return "", fmt.Errorf("invalid forfeit closure script")
}
ctrlBlock, err := txscript.ParseControlBlock(tapscript.ControlBlock) ctrlBlock, err := txscript.ParseControlBlock(tapscript.ControlBlock)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse control block: %s", err) return "", fmt.Errorf("failed to parse control block: %s", err)
@@ -349,7 +379,7 @@ func (s *covenantlessService) SubmitRedeemTx(
} }
// recompute redeem tx // recompute redeem tx
rebuiltRedeemTx, err := bitcointree.BuildRedeemTx(ins, outputs, fees) rebuiltRedeemTx, err := bitcointree.BuildRedeemTx(ins, outputs)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to rebuild redeem tx: %s", err) return "", fmt.Errorf("failed to rebuild redeem tx: %s", err)
} }
@@ -439,7 +469,7 @@ func (s *covenantlessService) SubmitRedeemTx(
func (s *covenantlessService) GetBoardingAddress( func (s *covenantlessService) GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey, ctx context.Context, userPubkey *secp256k1.PublicKey,
) (address string, scripts []string, err error) { ) (address string, scripts []string, err error) {
vtxoScript := bitcointree.NewDefaultVtxoScript(s.pubkey, userPubkey, uint(s.boardingExitDelay)) vtxoScript := bitcointree.NewDefaultVtxoScript(s.pubkey, userPubkey, s.boardingExitDelay)
tapKey, _, err := vtxoScript.TapTree() tapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
@@ -546,7 +576,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
} }
// if the exit path is available, forbid registering the boarding utxo // if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(exitDelay) < now { if blocktime+exitDelay.Seconds() < now {
return "", fmt.Errorf("tx %s expired", input.Txid) return "", fmt.Errorf("tx %s expired", input.Txid)
} }
@@ -634,7 +664,7 @@ func (s *covenantlessService) newBoardingInput(tx wire.MsgTx, input ports.Input)
return nil, fmt.Errorf("descriptor does not match script in transaction output") return nil, fmt.Errorf("descriptor does not match script in transaction output")
} }
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil { if err := boardingScript.Validate(s.pubkey, s.unilateralExitDelay); err != nil {
return nil, err return nil, err
} }
@@ -755,8 +785,8 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
return &ServiceInfo{ return &ServiceInfo{
PubKey: pubkey, PubKey: pubkey,
RoundLifetime: s.roundLifetime, RoundLifetime: int64(s.roundLifetime.Value),
UnilateralExitDelay: s.unilateralExitDelay, UnilateralExitDelay: int64(s.unilateralExitDelay.Value),
RoundInterval: s.roundInterval, RoundInterval: s.roundInterval,
Network: s.network.Name, Network: s.network.Name,
Dust: dust, Dust: dust,
@@ -1064,7 +1094,7 @@ func (s *covenantlessService) startFinalization() {
sweepClosure := tree.CSVSigClosure{ sweepClosure := tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{s.pubkey}}, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{s.pubkey}},
Seconds: uint(s.roundLifetime), Locktime: s.roundLifetime,
} }
sweepScript, err := sweepClosure.Script() sweepScript, err := sweepClosure.Script()
@@ -1561,7 +1591,7 @@ func (s *covenantlessService) scheduleSweepVtxosForRound(round *domain.Round) {
return return
} }
expirationTimestamp := s.sweeper.scheduler.AddNow(s.roundLifetime) expirationTimestamp := s.sweeper.scheduler.AddNow(int64(s.roundLifetime.Value))
if err := s.sweeper.schedule(expirationTimestamp, round.Txid, round.VtxoTree); err != nil { if err := s.sweeper.schedule(expirationTimestamp, round.Txid, round.VtxoTree); err != nil {
log.WithError(err).Warn("failed to schedule sweep tx") log.WithError(err).Warn("failed to schedule sweep tx")

View File

@@ -65,16 +65,17 @@ func (p OwnershipProof) validate(vtxo domain.Vtxo) error {
} }
func decodeForfeitClosure(script []byte) ([]*secp256k1.PublicKey, error) { func decodeForfeitClosure(script []byte) ([]*secp256k1.PublicKey, error) {
var forfeit tree.MultisigClosure closure, err := tree.DecodeClosure(script)
valid, err := forfeit.Decode(script)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !valid { switch c := closure.(type) {
return nil, fmt.Errorf("invalid forfeit closure script") case *tree.MultisigClosure:
return c.PubKeys, nil
case *tree.CLTVMultisigClosure:
return c.PubKeys, nil
} }
return forfeit.PubKeys, nil return nil, fmt.Errorf("invalid forfeit closure script")
} }

View File

@@ -7,6 +7,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/note" "github.com/ark-network/ark/common/note"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
@@ -357,12 +358,12 @@ func findSweepableOutputs(
} }
} }
var lifetime int64 var lifetime *common.Locktime
lifetime, sweepInput, err = txbuilder.GetSweepInput(node) lifetime, sweepInput, err = txbuilder.GetSweepInput(node)
if err != nil { if err != nil {
return nil, err return nil, err
} }
expirationTime = blocktimeCache[node.ParentTxid] + lifetime expirationTime = blocktimeCache[node.ParentTxid] + int64(lifetime.Value)
} else { } else {
// cache the blocktime for future use // cache the blocktime for future use
if schedulerUnit == ports.BlockHeight { if schedulerUnit == ports.BlockHeight {

View File

@@ -1,6 +1,7 @@
package ports package ports
import ( import (
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -47,7 +48,7 @@ type TxBuilder interface {
txs []string, txs []string,
) (valid map[domain.VtxoKey][]string, err error) ) (valid map[domain.VtxoKey][]string, err error)
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error) BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error) GetSweepInput(node tree.Node) (lifetime *common.Locktime, sweepInput SweepInput, err error)
FinalizeAndExtract(tx string) (txhex string, err error) FinalizeAndExtract(tx string) (txhex string, err error)
VerifyTapscriptPartialSigs(tx string) (valid bool, err error) VerifyTapscriptPartialSigs(tx string) (valid bool, err error)
// FindLeaves returns all the leaves txs that are reachable from the given outpoint // FindLeaves returns all the leaves txs that are reachable from the given outpoint

View File

@@ -42,6 +42,7 @@ type WalletService interface {
GetTransaction(ctx context.Context, txid string) (string, error) GetTransaction(ctx context.Context, txid string) (string, error)
SignMessage(ctx context.Context, message []byte) ([]byte, error) SignMessage(ctx context.Context, message []byte) ([]byte, error)
VerifyMessageSignature(ctx context.Context, message, signature []byte) (bool, error) VerifyMessageSignature(ctx context.Context, message, signature []byte) (bool, error)
GetCurrentBlockTime(ctx context.Context) (*BlockTimestamp, error)
Close() Close()
} }
@@ -63,3 +64,8 @@ type TxOutpoint interface {
GetTxid() string GetTxid() string
GetIndex() uint32 GetIndex() uint32
} }
type BlockTimestamp struct {
Height uint32
Time int64
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const tipHeightEndpoit = "/blocks/tip/height" const tipHeightEndpoint = "/blocks/tip/height"
type service struct { type service struct {
tipURL string tipURL string
@@ -25,7 +25,7 @@ func NewScheduler(esploraURL string) (ports.SchedulerService, error) {
return nil, fmt.Errorf("esplora URL is required") return nil, fmt.Errorf("esplora URL is required")
} }
tipURL, err := url.JoinPath(esploraURL, tipHeightEndpoit) tipURL, err := url.JoinPath(esploraURL, tipHeightEndpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -28,15 +28,15 @@ import (
type txBuilder struct { type txBuilder struct {
wallet ports.WalletService wallet ports.WalletService
net common.Network net common.Network
roundLifetime int64 // in seconds roundLifetime common.Locktime
boardingExitDelay int64 // in seconds boardingExitDelay common.Locktime
} }
func NewTxBuilder( func NewTxBuilder(
wallet ports.WalletService, wallet ports.WalletService,
net common.Network, net common.Network,
roundLifetime int64, roundLifetime common.Locktime,
boardingExitDelay int64, boardingExitDelay common.Locktime,
) ports.TxBuilder { ) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, boardingExitDelay} return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
} }
@@ -158,6 +158,11 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
validForfeitTxs := make(map[domain.VtxoKey][]string) validForfeitTxs := make(map[domain.VtxoKey][]string)
blocktimestamp, err := b.wallet.GetCurrentBlockTime(context.Background())
if err != nil {
return nil, err
}
for vtxoKey, psets := range forfeitTxsPsets { for vtxoKey, psets := range forfeitTxsPsets {
if len(psets) == 0 { if len(psets) == 0 {
continue continue
@@ -202,13 +207,36 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
vtxoTapscript := firstForfeit.Inputs[1].TapLeafScript[0] vtxoTapscript := firstForfeit.Inputs[1].TapLeafScript[0]
// verify the forfeit closure script
closure, err := tree.DecodeClosure(vtxoTapscript.Script)
if err != nil {
return nil, err
}
switch c := closure.(type) {
case *tree.CLTVMultisigClosure:
switch c.Locktime.Type {
case common.LocktimeTypeBlock:
if c.Locktime.Value > blocktimestamp.Height {
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block height)", c.Locktime.Value, blocktimestamp.Height)
}
case common.LocktimeTypeSecond:
if c.Locktime.Value > uint32(blocktimestamp.Time) {
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block time)", c.Locktime.Value, blocktimestamp.Time)
}
}
case *tree.MultisigClosure:
default:
return nil, fmt.Errorf("invalid forfeit closure script")
}
minFee, err := common.ComputeForfeitTxFee( minFee, err := common.ComputeForfeitTxFee(
minRate, minRate,
&waddrmgr.Tapscript{ &waddrmgr.Tapscript{
RevealedScript: vtxoTapscript.Script, RevealedScript: vtxoTapscript.Script,
ControlBlock: &vtxoTapscript.ControlBlock.ControlBlock, ControlBlock: &vtxoTapscript.ControlBlock.ControlBlock,
}, },
64*2, closure.WitnessSize(),
txscript.GetScriptClass(forfeitScript), txscript.GetScriptClass(forfeitScript),
) )
if err != nil { if err != nil {
@@ -418,14 +446,14 @@ func (b *txBuilder) BuildRoundTx(
return return
} }
func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput ports.SweepInput, err error) { func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime *common.Locktime, sweepInput ports.SweepInput, err error) {
pset, err := psetv2.NewPsetFromBase64(node.Tx) pset, err := psetv2.NewPsetFromBase64(node.Tx)
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
if len(pset.Inputs) != 1 { if len(pset.Inputs) != 1 {
return -1, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(pset.Inputs)) return nil, 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 // if the tx is not onchain, it means that the input is an existing shared output
@@ -435,22 +463,22 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
sweepLeaf, lifetime, err := extractSweepLeaf(input) sweepLeaf, lifetime, err := extractSweepLeaf(input)
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
txhex, err := b.wallet.GetTransaction(context.Background(), txid) txhex, err := b.wallet.GetTransaction(context.Background(), txid)
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
tx, err := transaction.NewTxFromHex(txhex) tx, err := transaction.NewTxFromHex(txhex)
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value) inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value)
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
sweepInput = &sweepLiquidInput{ sweepInput = &sweepLiquidInput{
@@ -509,6 +537,10 @@ func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, error)
for _, key := range c.PubKeys { for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
} }
case *tree.CLTVMultisigClosure:
for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
}
} }
// we don't need to check if server signed // we don't need to check if server signed
@@ -1123,24 +1155,24 @@ func (b *txBuilder) onchainNetwork() *network.Network {
} }
} }
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) { func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime *common.Locktime, err error) {
for _, leaf := range input.TapLeafScript { for _, leaf := range input.TapLeafScript {
closure := &tree.CSVSigClosure{} closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(leaf.Script) valid, err := closure.Decode(leaf.Script)
if err != nil { if err != nil {
return nil, 0, err return nil, nil, err
} }
if valid && closure.Seconds > uint(lifetime) { if valid && (lifetime == nil || lifetime.LessThan(closure.Locktime)) {
sweepLeaf = &leaf sweepLeaf = &leaf
lifetime = int64(closure.Seconds) lifetime = &closure.Locktime
} }
} }
if sweepLeaf == nil { if sweepLeaf == nil {
return nil, 0, fmt.Errorf("sweep leaf not found") return nil, nil, fmt.Errorf("sweep leaf not found")
} }
return sweepLeaf, lifetime, nil return
} }
type sweepLiquidInput struct { type sweepLiquidInput struct {

View File

@@ -18,18 +18,19 @@ import (
) )
const ( const (
testingKey = "020000000000000000000000000000000000000000000000000000000000000001" testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc" connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
forfeitAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc" forfeitAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
minRelayFee = uint64(30) minRelayFee = uint64(30)
roundLifetime = int64(1209344) minRelayFeeRate = 3
boardingExitDelay = int64(512)
minRelayFeeRate = 3
) )
var ( var (
wallet *mockedWallet wallet *mockedWallet
pubkey *secp256k1.PublicKey pubkey *secp256k1.PublicKey
roundLifetime = common.Locktime{Type: common.LocktimeTypeSecond, Value: 1209344}
boardingExitDelay = common.Locktime{Type: common.LocktimeTypeSecond, Value: 512}
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@@ -303,6 +303,10 @@ func (m *mockedWallet) VerifyMessageSignature(ctx context.Context, message, sign
panic("not implemented") panic("not implemented")
} }
func (m *mockedWallet) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
panic("not implemented")
}
type mockedInput struct { type mockedInput struct {
mock.Mock mock.Mock
} }

View File

@@ -90,7 +90,7 @@ func sweepTransaction(
return nil, err return nil, err
} }
sequence, err := common.BIP68Sequence(sweepClosure.Seconds) sequence, err := common.BIP68Sequence(sweepClosure.Locktime)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -27,12 +27,12 @@ import (
type txBuilder struct { type txBuilder struct {
wallet ports.WalletService wallet ports.WalletService
net common.Network net common.Network
roundLifetime int64 // in seconds roundLifetime common.Locktime
boardingExitDelay int64 // in seconds boardingExitDelay common.Locktime
} }
func NewTxBuilder( func NewTxBuilder(
wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay int64, wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay common.Locktime,
) ports.TxBuilder { ) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, boardingExitDelay} return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
} }
@@ -91,6 +91,10 @@ func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, error) {
for _, key := range c.PubKeys { for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
} }
case *tree.CLTVMultisigClosure:
for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
}
} }
// we don't need to check if server signed // we don't need to check if server signed
@@ -326,6 +330,11 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
validForfeitTxs := make(map[domain.VtxoKey][]string) validForfeitTxs := make(map[domain.VtxoKey][]string)
blocktimestamp, err := b.wallet.GetCurrentBlockTime(context.Background())
if err != nil {
return nil, err
}
for vtxoKey, ptxs := range forfeitTxsPtxs { for vtxoKey, ptxs := range forfeitTxsPtxs {
if len(ptxs) == 0 { if len(ptxs) == 0 {
continue continue
@@ -359,6 +368,30 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
} }
vtxoTapscript := firstForfeit.Inputs[1].TaprootLeafScript[0] vtxoTapscript := firstForfeit.Inputs[1].TaprootLeafScript[0]
// verify the forfeit closure script
closure, err := tree.DecodeClosure(vtxoTapscript.Script)
if err != nil {
return nil, err
}
switch c := closure.(type) {
case *tree.CLTVMultisigClosure:
switch c.Locktime.Type {
case common.LocktimeTypeBlock:
if c.Locktime.Value > blocktimestamp.Height {
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block height)", c.Locktime.Value, blocktimestamp.Height)
}
case common.LocktimeTypeSecond:
if c.Locktime.Value > uint32(blocktimestamp.Time) {
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block time)", c.Locktime.Value, blocktimestamp.Time)
}
}
case *tree.MultisigClosure:
default:
return nil, fmt.Errorf("invalid forfeit closure script")
}
ctrlBlock, err := txscript.ParseControlBlock(vtxoTapscript.ControlBlock) ctrlBlock, err := txscript.ParseControlBlock(vtxoTapscript.ControlBlock)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -370,7 +403,7 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
RevealedScript: vtxoTapscript.Script, RevealedScript: vtxoTapscript.Script,
ControlBlock: ctrlBlock, ControlBlock: ctrlBlock,
}, },
64*2, closure.WitnessSize(),
txscript.GetScriptClass(forfeitScript), txscript.GetScriptClass(forfeitScript),
) )
if err != nil { if err != nil {
@@ -569,14 +602,14 @@ func (b *txBuilder) BuildRoundTx(
return roundTx, vtxoTree, connectorAddress, connectors, nil return roundTx, vtxoTree, connectorAddress, connectors, nil
} }
func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput ports.SweepInput, err error) { func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime *common.Locktime, sweepInput ports.SweepInput, err error) {
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true) partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
if len(partialTx.Inputs) != 1 { if len(partialTx.Inputs) != 1 {
return -1, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(partialTx.Inputs)) return nil, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(partialTx.Inputs))
} }
input := partialTx.UnsignedTx.TxIn[0] input := partialTx.UnsignedTx.TxIn[0]
@@ -585,17 +618,17 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
sweepLeaf, internalKey, lifetime, err := extractSweepLeaf(partialTx.Inputs[0]) sweepLeaf, internalKey, lifetime, err := extractSweepLeaf(partialTx.Inputs[0])
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
txhex, err := b.wallet.GetTransaction(context.Background(), txid.String()) txhex, err := b.wallet.GetTransaction(context.Background(), txid.String())
if err != nil { if err != nil {
return -1, nil, err return nil, nil, err
} }
var tx wire.MsgTx var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil { if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return -1, nil, err return nil, nil, err
} }
sweepInput = &sweepBitcoinInput{ sweepInput = &sweepBitcoinInput{
@@ -1180,27 +1213,27 @@ func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
return outpoints return outpoints
} }
func extractSweepLeaf(input psbt.PInput) (sweepLeaf *psbt.TaprootTapLeafScript, internalKey *secp256k1.PublicKey, lifetime int64, err error) { func extractSweepLeaf(input psbt.PInput) (sweepLeaf *psbt.TaprootTapLeafScript, internalKey *secp256k1.PublicKey, lifetime *common.Locktime, err error) {
for _, leaf := range input.TaprootLeafScript { for _, leaf := range input.TaprootLeafScript {
closure := &tree.CSVSigClosure{} closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(leaf.Script) valid, err := closure.Decode(leaf.Script)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, nil, err
} }
if valid && closure.Seconds > 0 { if valid && (lifetime == nil || closure.Locktime.LessThan(*lifetime)) {
sweepLeaf = leaf sweepLeaf = leaf
lifetime = int64(closure.Seconds) lifetime = &closure.Locktime
} }
} }
internalKey, err = schnorr.ParsePubKey(input.TaprootInternalKey) internalKey, err = schnorr.ParsePubKey(input.TaprootInternalKey)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, nil, err
} }
if sweepLeaf == nil { if sweepLeaf == nil {
return nil, nil, 0, fmt.Errorf("sweep leaf not found") return nil, nil, nil, fmt.Errorf("sweep leaf not found")
} }
return sweepLeaf, internalKey, lifetime, nil return sweepLeaf, internalKey, lifetime, nil

View File

@@ -18,18 +18,19 @@ import (
) )
const ( const (
testingKey = "020000000000000000000000000000000000000000000000000000000000000001" testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2" connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
forfeitAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2" forfeitAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
changeAddress = "bcrt1qhhq55mut9easvrncy4se8q6vg3crlug7yj4j56" changeAddress = "bcrt1qhhq55mut9easvrncy4se8q6vg3crlug7yj4j56"
roundLifetime = int64(1209344) minRelayFeeRate = 3
boardingExitDelay = int64(512)
minRelayFeeRate = 3
) )
var ( var (
wallet *mockedWallet wallet *mockedWallet
pubkey *secp256k1.PublicKey pubkey *secp256k1.PublicKey
roundLifetime = common.Locktime{Type: common.LocktimeTypeSecond, Value: 1209344}
boardingExitDelay = common.Locktime{Type: common.LocktimeTypeSecond, Value: 512}
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@@ -284,6 +284,10 @@ func (m *mockedWallet) MainAccountBalance(ctx context.Context) (uint64, uint64,
panic("not implemented") panic("not implemented")
} }
func (m *mockedWallet) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
panic("not implemented")
}
type mockedInput struct { type mockedInput struct {
mock.Mock mock.Mock
} }

View File

@@ -37,7 +37,7 @@ func sweepTransaction(
return nil, fmt.Errorf("invalid csv script") return nil, fmt.Errorf("invalid csv script")
} }
sequence, err := common.BIP68Sequence(sweepClosure.Seconds) sequence, err := common.BIP68Sequence(sweepClosure.Locktime)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1119,6 +1119,23 @@ func (s *service) VerifyMessageSignature(ctx context.Context, message, signature
return sig.Verify(message, s.serverKeyAddr.PubKey()), nil return sig.Verify(message, s.serverKeyAddr.PubKey()), nil
} }
func (s *service) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
blockhash, blockheight, err := s.wallet.GetBestBlock()
if err != nil {
return nil, err
}
header, err := s.wallet.GetBlockHeader(blockhash)
if err != nil {
return nil, err
}
return &ports.BlockTimestamp{
Time: header.Timestamp.Unix(),
Height: uint32(blockheight),
}, nil
}
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue { func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue {
vtxos := make(map[string][]ports.VtxoWithValue) vtxos := make(map[string][]ports.VtxoWithValue)

View File

@@ -2,8 +2,12 @@ package oceanwallet
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http"
"net/url"
"strings" "strings"
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1" pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
@@ -26,9 +30,18 @@ type service struct {
chVtxos chan map[string][]ports.VtxoWithValue chVtxos chan map[string][]ports.VtxoWithValue
isListening bool isListening bool
syncedCh chan struct{} syncedCh chan struct{}
esploraURL string
} }
func NewService(addr string) (ports.WalletService, error) { type blockInfo struct {
Height int64 `json:"height"`
Timestamp int64 `json:"timestamp"`
}
func NewService(addr string, esploraURL string) (ports.WalletService, error) {
if len(esploraURL) == 0 {
return nil, fmt.Errorf("missing esplora url")
}
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -47,6 +60,7 @@ func NewService(addr string) (ports.WalletService, error) {
notifyClient: notifyClient, notifyClient: notifyClient,
chVtxos: chVtxos, chVtxos: chVtxos,
syncedCh: make(chan struct{}), syncedCh: make(chan struct{}),
esploraURL: esploraURL,
} }
ctx := context.Background() ctx := context.Background()
@@ -168,6 +182,45 @@ func (s *service) VerifyMessageSignature(ctx context.Context, message, signature
return false, errors.New("not implemented") return false, errors.New("not implemented")
} }
func (s *service) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
tipHashURL, err := url.JoinPath(s.esploraURL, "blocks/tip/hash")
if err != nil {
return nil, err
}
resp, err := http.Get(tipHashURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
hash, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
blockURL, err := url.JoinPath(s.esploraURL, "block", string(hash))
if err != nil {
return nil, err
}
resp, err = http.Get(blockURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var blockInfo blockInfo
if err := json.NewDecoder(resp.Body).Decode(&blockInfo); err != nil {
return nil, err
}
return &ports.BlockTimestamp{
Height: uint32(blockInfo.Height),
Time: blockInfo.Timestamp,
}, nil
}
func (s *service) listenToNotifications() { func (s *service) listenToNotifications() {
s.isListening = true s.isListening = true
defer func() { defer func() {

View File

@@ -3,22 +3,35 @@ package e2e_test
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
arksdk "github.com/ark-network/ark/pkg/client-sdk" arksdk "github.com/ark-network/ark/pkg/client-sdk"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc" grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/redemption" "github.com/ark-network/ark/pkg/client-sdk/redemption"
"github.com/ark-network/ark/pkg/client-sdk/store" "github.com/ark-network/ark/pkg/client-sdk/store"
inmemorystoreconfig "github.com/ark-network/ark/pkg/client-sdk/store/inmemory"
"github.com/ark-network/ark/pkg/client-sdk/types" "github.com/ark-network/ark/pkg/client-sdk/types"
singlekeywallet "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey"
inmemorystore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store/inmemory"
utils "github.com/ark-network/ark/server/test/e2e" utils "github.com/ark-network/ark/server/test/e2e"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04" "github.com/nbd-wtf/go-nostr/nip04"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -411,6 +424,163 @@ func TestRedeemNotes(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestSendToCLTVMultisigClosure(t *testing.T) {
ctx := context.Background()
alice, grpcAlice := setupArkSDK(t)
defer grpcAlice.Close()
bobPrivKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
configStore, err := inmemorystoreconfig.NewConfigStore()
require.NoError(t, err)
walletStore, err := inmemorystore.NewWalletStore()
require.NoError(t, err)
bobWallet, err := singlekeywallet.NewBitcoinWallet(
configStore,
walletStore,
)
require.NoError(t, err)
_, err = bobWallet.Create(ctx, utils.Password, hex.EncodeToString(bobPrivKey.Serialize()))
require.NoError(t, err)
_, err = bobWallet.Unlock(ctx, utils.Password)
require.NoError(t, err)
bobPubKey := bobPrivKey.PubKey()
// Fund Alice's account
offchainAddr, boardingAddress, err := alice.Receive(ctx)
require.NoError(t, err)
aliceAddr, err := common.DecodeAddress(offchainAddr)
require.NoError(t, err)
_, err = utils.RunCommand("nigiri", "faucet", boardingAddress)
require.NoError(t, err)
time.Sleep(5 * time.Second)
_, err = alice.Settle(ctx)
require.NoError(t, err)
time.Sleep(5 * time.Second)
const cltvBlocks = 10
const sendAmount = 10000
currentHeight, err := utils.GetBlockHeight(false)
require.NoError(t, err)
vtxoScript := bitcointree.TapscriptsVtxoScript{
TapscriptsVtxoScript: tree.TapscriptsVtxoScript{
Closures: []tree.Closure{
&tree.CLTVMultisigClosure{
Locktime: common.Locktime{Type: common.LocktimeTypeBlock, Value: currentHeight + cltvBlocks},
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{bobPubKey},
},
},
},
},
}
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
require.NoError(t, err)
closure := vtxoScript.ForfeitClosures()[0]
bobAddr := common.Address{
HRP: "tark",
VtxoTapKey: vtxoTapKey,
Server: aliceAddr.Server,
}
script, err := closure.Script()
require.NoError(t, err)
merkleProof, err := vtxoTapTree.GetTaprootMerkleProof(txscript.NewBaseTapLeaf(script).TapHash())
require.NoError(t, err)
ctrlBlock, err := txscript.ParseControlBlock(merkleProof.ControlBlock)
require.NoError(t, err)
tapscript := &waddrmgr.Tapscript{
ControlBlock: ctrlBlock,
RevealedScript: merkleProof.Script,
}
bobAddrStr, err := bobAddr.Encode()
require.NoError(t, err)
redeemTx, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)})
require.NoError(t, err)
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(redeemTx), true)
require.NoError(t, err)
var bobOutput *wire.TxOut
var bobOutputIndex uint32
for i, out := range redeemPtx.UnsignedTx.TxOut {
if bytes.Equal(out.PkScript[2:], schnorr.SerializePubKey(bobAddr.VtxoTapKey)) {
bobOutput = out
bobOutputIndex = uint32(i)
break
}
}
require.NotNil(t, bobOutput)
time.Sleep(2 * time.Second)
alicePkScript, err := common.P2TRScript(aliceAddr.VtxoTapKey)
require.NoError(t, err)
ptx, err := bitcointree.BuildRedeemTx(
[]common.VtxoInput{
{
Outpoint: &wire.OutPoint{
Hash: redeemPtx.UnsignedTx.TxHash(),
Index: bobOutputIndex,
},
Tapscript: tapscript,
WitnessSize: closure.WitnessSize(),
Amount: bobOutput.Value,
},
},
[]*wire.TxOut{
{
Value: bobOutput.Value - 500,
PkScript: alicePkScript,
},
},
)
require.NoError(t, err)
signedTx, err := bobWallet.SignTransaction(
ctx,
explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest),
ptx,
)
require.NoError(t, err)
// should fail because the tx is not yet valid
_, err = grpcAlice.SubmitRedeemTx(ctx, signedTx)
require.Error(t, err)
// Generate blocks to pass the timelock
for i := 0; i < cltvBlocks+1; i++ {
err = utils.GenerateBlock()
require.NoError(t, err)
time.Sleep(1 * time.Second)
}
_, err = grpcAlice.SubmitRedeemTx(ctx, signedTx)
require.NoError(t, err)
}
func TestSweep(t *testing.T) { func TestSweep(t *testing.T) {
var receive utils.ArkReceive var receive utils.ArkReceive
receiveStr, err := runClarkCommand("receive") receiveStr, err := runClarkCommand("receive")

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -48,6 +49,24 @@ func GenerateBlock() error {
return nil return nil
} }
func GetBlockHeight(isLiquid bool) (uint32, error) {
var out string
var err error
if isLiquid {
out, err = RunCommand("nigiri", "rpc", "--liquid", "getblockcount")
} else {
out, err = RunCommand("nigiri", "rpc", "getblockcount")
}
if err != nil {
return 0, err
}
height, err := strconv.ParseUint(strings.TrimSpace(out), 10, 32)
if err != nil {
return 0, err
}
return uint32(height), nil
}
func RunDockerExec(container string, arg ...string) (string, error) { func RunDockerExec(container string, arg ...string) (string, error) {
args := append([]string{"exec", "-t", container}, arg...) args := append([]string{"exec", "-t", container}, arg...)
return RunCommand("docker", args...) return RunCommand("docker", args...)