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_MAX = SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31
SECONDS_PER_BLOCK = 10 * 60 // 10 minutes
)
func closerToModulo512(x uint) uint {
return x - (x % 512)
type LocktimeType uint
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) {
isSeconds := locktime >= 512
func (l Locktime) Seconds() int64 {
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 {
locktime = closerToModulo512(locktime)
if locktime > SECONDS_MAX {
if value > 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 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))
if err != nil {
return 0, err
return nil, err
}
if scriptNumber >= txscript.OP_1 && scriptNumber <= txscript.OP_16 {
@@ -48,12 +85,12 @@ func BIP68DecodeSequence(sequence []byte) (uint, error) {
asNumber := int64(scriptNumber)
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 {
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
func CraftSharedOutput(
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
feeSatsPerNode uint64, roundLifetime common.Locktime,
) ([]byte, int64, error) {
aggregatedKey, _, err := createAggregatedKeyWithSweep(
cosigners, server, roundLifetime,
@@ -45,7 +45,7 @@ func CraftSharedOutput(
// BuildVtxoTree creates all the tree's transactions
func BuildVtxoTree(
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
feeSatsPerNode uint64, roundLifetime common.Locktime,
) (tree.VtxoTree, error) {
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
cosigners, server, roundLifetime,
@@ -280,11 +280,11 @@ func createRootNode(
}
func createAggregatedKeyWithSweep(
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime int64,
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime common.Locktime,
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
sweepClosure := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server}},
Seconds: uint(roundLifetime),
Locktime: roundLifetime,
}
sweepScript, err := sweepClosure.Script()

View File

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

View File

@@ -12,7 +12,6 @@ import (
func BuildRedeemTx(
vtxos []common.VtxoInput,
outputs []*wire.TxOut,
feeAmount int64,
) (string, error) {
if len(vtxos) <= 0 {
return "", fmt.Errorf("missing vtxos")
@@ -50,10 +49,6 @@ func BuildRedeemTx(
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))
for i := range sequences {
sequences[i] = wire.MaxTxInSequenceNum

View File

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

View File

@@ -10,7 +10,7 @@ import (
"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) {
types := []VtxoScript{
@@ -26,7 +26,7 @@ func ParseVtxoScript(scripts []string) (VtxoScript, error) {
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)
return &TapscriptsVtxoScript{*base}

View File

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

View File

@@ -97,11 +97,11 @@ func (e *PK) Script(verify bool) (string, error) {
}
type Older struct {
Timeout uint
Locktime common.Locktime
}
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 {
@@ -124,13 +124,13 @@ func (e *Older) Parse(policy string) error {
return ErrInvalidOlderPolicy
}
e.Timeout = uint(timeout)
e.Locktime = common.Locktime{Type: common.LocktimeTypeBlock, Value: uint32(timeout)}
return nil
}
func (e *Older) Script(bool) (string, error) {
sequence, err := common.BIP68Sequence(e.Timeout)
sequence, err := common.BIP68Sequence(e.Locktime)
if err != nil {
return "", err
}

View File

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

View File

@@ -4,6 +4,7 @@ import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
@@ -14,7 +15,7 @@ import (
func BuildVtxoTree(
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
feeSatsPerNode uint64, roundLifetime common.Locktime,
) (
factoryFn TreeFactory,
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
@@ -53,7 +54,7 @@ type node struct {
right *node
asset string
feeSats uint64
roundLifetime int64
roundLifetime common.Locktime
_inputTaprootKey *secp256k1.PublicKey
_inputTaprootTree *taproot.IndexedElementsTapScriptTree
@@ -164,7 +165,7 @@ func (n *node) getWitnessData() (
sweepClosure := &CSVSigClosure{
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{n.sweepKey}},
Seconds: uint(n.roundLifetime),
Locktime: n.roundLifetime,
}
sweepLeaf, err := sweepClosure.Script()
@@ -380,7 +381,7 @@ func (n *node) buildVtxoTree() TreeFactory {
func buildTreeNodes(
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
feeSatsPerNode uint64, roundLifetime common.Locktime,
) (root *node, err error) {
if len(receivers) == 0 {
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
// 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.
type CSVSigClosure struct {
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) {
types := []Closure{
&CSVSigClosure{},
&CLTVMultisigClosure{},
&MultisigClosure{},
&UnrollClosure{},
}
@@ -324,8 +333,13 @@ func (f *CSVSigClosure) WitnessSize() int {
}
func (d *CSVSigClosure) Script() ([]byte, error) {
sequence, err := common.BIP68Sequence(d.Locktime)
if err != nil {
return nil, err
}
csvScript, err := txscript.NewScriptBuilder().
AddInt64(int64(d.Seconds)).
AddInt64(int64(sequence)).
AddOps([]byte{
txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP,
@@ -360,7 +374,7 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
sequence = sequence[1:]
}
seconds, err := common.BIP68DecodeSequence(sequence)
locktime, err := common.BIP68DecodeSequence(sequence)
if err != nil {
return false, err
}
@@ -375,7 +389,7 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
return false, nil
}
d.Seconds = seconds
d.Locktime = *locktime
d.MultisigClosure = *multisigClosure
return valid, nil
@@ -666,3 +680,87 @@ func (c *UnrollClosure) Witness(controlBlock []byte, _ map[string][]byte) (wire.
// UnrollClosure only needs script and control block
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
import (
"bytes"
"encoding/hex"
"testing"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript"
@@ -19,7 +21,7 @@ func TestRoundTripCSV(t *testing.T) {
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{seckey.PubKey()},
},
Seconds: 1024,
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
}
leaf, err := csvSig.Script()
@@ -31,7 +33,7 @@ func TestRoundTripCSV(t *testing.T) {
require.NoError(t, err)
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) {
@@ -194,7 +196,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1},
},
Seconds: 1024,
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
}
script, err := csvSig.Script()
@@ -204,7 +206,7 @@ func TestCSVSigClosure(t *testing.T) {
valid, err := decodedCSV.Decode(script)
require.NoError(t, err)
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,
schnorr.SerializePubKey(pubkey1),
@@ -217,7 +219,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
},
Seconds: 2016, // ~2 weeks
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512 * 4}, // ~2 weeks
}
script, err := csvSig.Script()
@@ -227,7 +229,7 @@ func TestCSVSigClosure(t *testing.T) {
valid, err := decodedCSV.Decode(script)
require.NoError(t, err)
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,
schnorr.SerializePubKey(pubkey1),
@@ -265,7 +267,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{
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)
require.Equal(t, 128, csvSig.WitnessSize())
@@ -276,7 +278,7 @@ func TestCSVSigClosure(t *testing.T) {
MultisigClosure: tree.MultisigClosure{
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()
@@ -286,7 +288,7 @@ func TestCSVSigClosure(t *testing.T) {
valid, err := decodedCSV.Decode(script)
require.NoError(t, err)
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{
PubKeys: []*secp256k1.PublicKey{pub1},
},
Seconds: 144,
Locktime: common.Locktime{Type: common.LocktimeTypeBlock, Value: 144},
}
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, 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"
"fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -72,7 +73,7 @@ func UnspendableKey() *secp256k1.PublicKey {
// - input and output amounts
func ValidateVtxoTree(
tree VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey,
roundLifetime int64,
roundLifetime common.Locktime,
) error {
roundTransaction, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil {
@@ -148,7 +149,7 @@ func ValidateVtxoTree(
func validateNodeTransaction(
node Node, tree VtxoTree,
expectedInternalKey, expectedServerPubkey *secp256k1.PublicKey,
expectedSequence int64,
expectedLifetime common.Locktime,
) error {
if node.Tx == "" {
return ErrNodeTxEmpty
@@ -242,7 +243,8 @@ func validateNodeTransaction(
schnorr.SerializePubKey(c.MultisigClosure.PubKeys[0]),
schnorr.SerializePubKey(expectedServerPubkey),
)
isSweepDelay := int64(c.Seconds) == expectedSequence
isSweepDelay := c.Locktime == expectedLifetime
if isServer && !isSweepDelay {
return ErrInvalidSweepSequence

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/hex"
"fmt"
"math"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -17,7 +16,7 @@ var (
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) {
v := &TapscriptsVtxoScript{}
@@ -26,12 +25,12 @@ func ParseVtxoScript(scripts []string) (VtxoScript, error) {
return v, err
}
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay uint) *TapscriptsVtxoScript {
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay common.Locktime) *TapscriptsVtxoScript {
return &TapscriptsVtxoScript{
[]Closure{
&CSVSigClosure{
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner}},
Seconds: exitDelay,
Locktime: exitDelay,
},
&MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner, server}},
},
@@ -73,12 +72,17 @@ func (v *TapscriptsVtxoScript) Decode(scripts []string) error {
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)
for _, forfeit := range v.ForfeitClosures() {
multisigClosure, ok := forfeit.(*MultisigClosure)
if !ok {
return fmt.Errorf("invalid forfeit closure, expected MultisigClosure")
}
// must contain server pubkey
found := false
for _, pubkey := range forfeit.PubKeys {
for _, pubkey := range multisigClosure.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(pubkey), serverXonly) {
found = true
break
@@ -97,46 +101,48 @@ func (v *TapscriptsVtxoScript) Validate(server *secp256k1.PublicKey, minExitDela
return err
}
if smallestExit < minExitDelay {
if smallestExit.LessThan(minLocktime) {
return fmt.Errorf("exit delay is too short")
}
return nil
}
func (v *TapscriptsVtxoScript) SmallestExitDelay() (uint, error) {
smallest := uint(math.MaxUint32)
func (v *TapscriptsVtxoScript) SmallestExitDelay() (*common.Locktime, error) {
var smallest *common.Locktime
for _, closure := range v.Closures {
if csvClosure, ok := closure.(*CSVSigClosure); ok {
if csvClosure.Seconds < smallest {
smallest = csvClosure.Seconds
if smallest == nil || csvClosure.Locktime.LessThan(*smallest) {
smallest = &csvClosure.Locktime
}
}
}
if smallest == math.MaxUint32 {
return 0, ErrNoExitLeaf
if smallest == nil {
return nil, ErrNoExitLeaf
}
return smallest, nil
}
func (v *TapscriptsVtxoScript) ForfeitClosures() []*MultisigClosure {
forfeits := make([]*MultisigClosure, 0)
func (v *TapscriptsVtxoScript) ForfeitClosures() []Closure {
forfeits := make([]Closure, 0)
for _, closure := range v.Closures {
if multisigClosure, ok := closure.(*MultisigClosure); ok {
forfeits = append(forfeits, multisigClosure)
switch closure.(type) {
case *MultisigClosure, *CLTVMultisigClosure:
forfeits = append(forfeits, closure)
}
}
return forfeits
}
func (v *TapscriptsVtxoScript) ExitClosures() []*CSVSigClosure {
exits := make([]*CSVSigClosure, 0)
func (v *TapscriptsVtxoScript) ExitClosures() []Closure {
exits := make([]Closure, 0)
for _, closure := range v.Closures {
if csvClosure, ok := closure.(*CSVSigClosure); ok {
exits = append(exits, csvClosure)
switch closure.(type) {
case *CSVSigClosure:
exits = append(exits, closure)
}
}
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.
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 {
Validate(server *secp256k1.PublicKey, minExitDelay uint) error
type VtxoScript[T TaprootTree, C interface{}] interface {
Validate(server *secp256k1.PublicKey, minLocktime Locktime) error
TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error)
Encode() ([]string, error)
Decode(scripts []string) error
SmallestExitDelay() (uint, error)
ForfeitClosures() []F
ExitClosures() []E
SmallestExitDelay() (*Locktime, error)
ForfeitClosures() []C
ExitClosures() []C
}
// 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)
}
lifetimeType := common.LocktimeTypeBlock
if info.RoundLifetime >= 512 {
lifetimeType = common.LocktimeTypeSecond
}
unilateralExitDelayType := common.LocktimeTypeBlock
if info.UnilateralExitDelay >= 512 {
unilateralExitDelayType = common.LocktimeTypeSecond
}
storeData := types.Config{
ServerUrl: args.ServerUrl,
ServerPubKey: serverPubkey,
WalletType: args.Wallet.GetType(),
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(info.RoundLifetime)},
RoundInterval: info.RoundInterval,
UnilateralExitDelay: info.UnilateralExitDelay,
UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(info.UnilateralExitDelay)},
Dust: info.Dust,
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
ForfeitAddress: info.ForfeitAddress,
@@ -213,15 +223,25 @@ func (a *arkClient) init(
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{
ServerUrl: args.ServerUrl,
ServerPubKey: serverPubkey,
WalletType: args.WalletType,
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(info.RoundLifetime)},
RoundInterval: info.RoundInterval,
UnilateralExitDelay: info.UnilateralExitDelay,
UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(info.UnilateralExitDelay)},
Dust: info.Dust,
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
ExplorerURL: args.ExplorerURL,
@@ -347,8 +367,8 @@ func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error)
}
}
func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time {
return expiry.Add(-time.Duration(roundLifetime) * time.Second)
func getCreatedAtFromExpiry(roundLifetime common.Locktime, expiry time.Time) time.Time {
return expiry.Add(-time.Duration(roundLifetime.Seconds()) * time.Second)
}
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) {
continue
}
@@ -1535,7 +1535,7 @@ func (a *covenantArkClient) coinSelectOnchain(
}
for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
@@ -1571,7 +1571,7 @@ func (a *covenantArkClient) coinSelectOnchain(
}
for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts)
u := utxo.ToUtxo(a.UnilateralExitDelay, addr.Tapscripts)
if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
@@ -1719,7 +1719,7 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []
}
func vtxosToTxsCovenant(
roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []types.Transaction,
roundLifetime common.Locktime, spendable, spent []client.Vtxo, boardingTxs []types.Transaction,
) ([]types.Transaction, error) {
transactions := make([]types.Transaction, 0)
unconfirmedBoardingTxs := make([]types.Transaction, 0)

View File

@@ -1703,7 +1703,7 @@ func (a *covenantlessArkClient) handleRoundSigningStarted(
) (signerSession bitcointree.SignerSession, err error) {
sweepClosure := tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{a.ServerPubKey}},
Seconds: uint(a.RoundLifetime),
Locktime: a.RoundLifetime,
}
script, err := sweepClosure.Script()
@@ -2163,7 +2163,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
}
for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
@@ -2199,7 +2199,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
}
for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts)
u := utxo.ToUtxo(a.UnilateralExitDelay, addr.Tapscripts)
if u.SpendableAt.Before(now) {
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) {
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"`
}
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)
}
@@ -65,7 +65,7 @@ type Explorer interface {
GetUtxos(addr string) ([]ExplorerUtxo, error)
GetBalance(addr string) (uint64, error)
GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
addr string, unilateralExitDelay common.Locktime,
) (uint64, map[int64]uint64, error)
GetTxBlockTime(
txid string,
@@ -247,7 +247,7 @@ func (e *explorerSvc) GetBalance(addr string) (uint64, error) {
}
func (e *explorerSvc) GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
addr string, unilateralExitDelay common.Locktime,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr)
if err != nil {
@@ -262,7 +262,7 @@ func (e *explorerSvc) GetRedeemedVtxosBalance(
blocktime = time.Unix(utxo.Status.Blocktime, 0)
}
delay := time.Duration(unilateralExitDelay) * time.Second
delay := time.Duration(unilateralExitDelay.Seconds()) * time.Second
availableAt := blocktime.Add(delay)
if availableAt.After(now) {
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
@@ -415,7 +415,7 @@ func parseBitcoinTx(txStr string) (string, string, error) {
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
createdAt := time.Unix(utxoTime, 0)
if utxoTime == 0 {
@@ -429,7 +429,7 @@ func newUtxo(explorerUtxo ExplorerUtxo, delay uint, tapscripts []string) types.U
Amount: explorerUtxo.Amount,
Asset: explorerUtxo.Asset,
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,
Tapscripts: tapscripts,
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -26,12 +27,12 @@ func NewCovenantRedeemBranch(
explorer explorer.Explorer,
vtxoTree tree.VtxoTree, vtxo client.Vtxo,
) (*CovenantRedeemBranch, error) {
sweepClosure, seconds, err := findCovenantSweepClosure(vtxoTree)
sweepClosure, locktime, err := findCovenantSweepClosure(vtxoTree)
if err != nil {
return nil, err
}
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", locktime.Seconds()))
if err != nil {
return nil, err
}
@@ -183,19 +184,19 @@ func (r *CovenantRedeemBranch) offchainPath() ([]*psetv2.Pset, error) {
func findCovenantSweepClosure(
vtxoTree tree.VtxoTree,
) (*taproot.TapElementsLeaf, uint, error) {
) (*taproot.TapElementsLeaf, *common.Locktime, error) {
root, err := vtxoTree.Root()
if err != nil {
return nil, 0, err
return nil, nil, err
}
// find the sweep closure
tx, err := psetv2.NewPsetFromBase64(root.Tx)
if err != nil {
return nil, 0, err
return nil, nil, err
}
var seconds uint
var locktime *common.Locktime
var sweepClosure *taproot.TapElementsLeaf
for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
closure := &tree.CSVSigClosure{}
@@ -204,15 +205,15 @@ func findCovenantSweepClosure(
continue
}
if valid && closure.Seconds > seconds {
seconds = closure.Seconds
if valid && (locktime == nil || closure.Locktime.LessThan(*locktime)) {
locktime = &closure.Locktime
sweepClosure = &tapLeaf.TapElementsLeaf
}
}
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"
"time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -25,12 +26,12 @@ func NewCovenantlessRedeemBranch(
explorer explorer.Explorer,
vtxoTree tree.VtxoTree, vtxo client.Vtxo,
) (*CovenantlessRedeemBranch, error) {
_, seconds, err := findCovenantlessSweepClosure(vtxoTree)
_, locktime, err := findCovenantlessSweepClosure(vtxoTree)
if err != nil {
return nil, err
}
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", locktime.Seconds()))
if err != nil {
return nil, err
}
@@ -156,19 +157,19 @@ func (r *CovenantlessRedeemBranch) OffchainPath() ([]*psbt.Packet, error) {
func findCovenantlessSweepClosure(
vtxoTree tree.VtxoTree,
) (*txscript.TapLeaf, uint, error) {
) (*txscript.TapLeaf, *common.Locktime, error) {
root, err := vtxoTree.Root()
if err != nil {
return nil, 0, err
return nil, nil, err
}
// find the sweep closure
tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true)
if err != nil {
return nil, 0, err
return nil, nil, err
}
var seconds uint
var locktime *common.Locktime
var sweepClosure *txscript.TapLeaf
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
closure := &tree.CSVSigClosure{}
@@ -177,16 +178,16 @@ func findCovenantlessSweepClosure(
continue
}
if valid && closure.Seconds > seconds {
seconds = closure.Seconds
if valid && (locktime == nil || closure.Locktime.LessThan(*locktime)) {
locktime = &closure.Locktime
leaf := txscript.NewBaseTapLeaf(tapLeaf.Script)
sweepClosure = &leaf
}
}
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,
ClientType: data.ClientType,
Network: data.Network.Name,
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime),
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime.Value),
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),
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
ExplorerURL: data.ExplorerURL,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import (
"strconv"
"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/types"
"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,
ClientType: data.ClientType,
Network: data.Network.Name,
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime),
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime.Value),
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),
ExplorerURL: data.ExplorerURL,
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())
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{
ServerUrl: s.store.Call("getItem", "server_url").String(),
ServerPubKey: serverPubkey,
WalletType: s.store.Call("getItem", "wallet_type").String(),
ClientType: s.store.Call("getItem", "client_type").String(),
Network: network,
RoundLifetime: int64(roundLifetime),
RoundLifetime: common.Locktime{Value: uint32(roundLifetime), Type: lifetimeType},
RoundInterval: int64(roundInterval),
UnilateralExitDelay: int64(unilateralExitDelay),
UnilateralExitDelay: common.Locktime{Value: uint32(unilateralExitDelay), Type: unilateralExitDelayType},
Dust: uint64(dust),
ExplorerURL: s.store.Call("getItem", "explorer_url").String(),
ForfeitAddress: s.store.Call("getItem", "forfeit_address").String(),

View File

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

View File

@@ -59,6 +59,17 @@ func mainAction(_ *cli.Context) error {
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{
EventDbType: cfg.EventDbType,
DbType: cfg.DbType,
@@ -70,8 +81,8 @@ func mainAction(_ *cli.Context) error {
SchedulerType: cfg.SchedulerType,
TxBuilderType: cfg.TxBuilderType,
WalletAddr: cfg.WalletAddr,
RoundLifetime: cfg.RoundLifetime,
UnilateralExitDelay: cfg.UnilateralExitDelay,
RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(cfg.RoundLifetime)},
UnilateralExitDelay: common.Locktime{Type: unilateralExitType, Value: uint32(cfg.UnilateralExitDelay)},
EsploraURL: cfg.EsploraURL,
NeutrinoPeer: cfg.NeutrinoPeer,
BitcoindRpcUser: cfg.BitcoindRpcUser,
@@ -79,7 +90,7 @@ func mainAction(_ *cli.Context) error {
BitcoindRpcHost: cfg.BitcoindRpcHost,
BitcoindZMQBlock: cfg.BitcoindZMQBlock,
BitcoindZMQTx: cfg.BitcoindZMQTx,
BoardingExitDelay: cfg.BoardingExitDelay,
BoardingExitDelay: common.Locktime{Type: boardingExitType, Value: uint32(cfg.BoardingExitDelay)},
UnlockerType: cfg.UnlockerType,
UnlockerFilePath: cfg.UnlockerFilePath,
UnlockerPassword: cfg.UnlockerPassword,

View File

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

View File

@@ -31,10 +31,10 @@ var (
type covenantService struct {
network common.Network
pubkey *secp256k1.PublicKey
roundLifetime int64
roundInterval int64
unilateralExitDelay int64
boardingExitDelay int64
roundLifetime common.Locktime
unilateralExitDelay common.Locktime
boardingExitDelay common.Locktime
nostrDefaultRelays []string
@@ -57,7 +57,8 @@ type covenantService struct {
func NewCovenantService(
network common.Network,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64,
roundInterval int64,
roundLifetime, unilateralExitDelay, boardingExitDelay common.Locktime,
nostrDefaultRelays []string,
walletSvc ports.WalletService, repoManager ports.RepoManager,
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) {
vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, uint(s.boardingExitDelay))
vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, s.boardingExitDelay)
tapKey, _, err := vtxoScript.TapTree()
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 blocktime+int64(exitDelay) < now {
if blocktime+exitDelay.Seconds() < now {
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")
}
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
if err := boardingScript.Validate(s.pubkey, s.unilateralExitDelay); err != nil {
return nil, err
}
@@ -457,8 +458,8 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
return &ServiceInfo{
PubKey: pubkey,
RoundLifetime: s.roundLifetime,
UnilateralExitDelay: s.unilateralExitDelay,
RoundLifetime: int64(s.roundLifetime.Value),
UnilateralExitDelay: int64(s.unilateralExitDelay.Value),
RoundInterval: s.roundInterval,
Network: s.network.Name,
Dust: dust,
@@ -1003,7 +1004,7 @@ func (s *covenantService) scheduleSweepVtxosForRound(round *domain.Round) {
return
}
expirationTime := s.sweeper.scheduler.AddNow(s.roundLifetime)
expirationTime := s.sweeper.scheduler.AddNow(int64(s.roundLifetime.Value))
if err := s.sweeper.schedule(
expirationTime, round.Txid, round.VtxoTree,

View File

@@ -33,10 +33,10 @@ const marketHourDelta = 5 * time.Minute
type covenantlessService struct {
network common.Network
pubkey *secp256k1.PublicKey
roundLifetime int64
roundLifetime common.Locktime
roundInterval int64
unilateralExitDelay int64
boardingExitDelay int64
unilateralExitDelay common.Locktime
boardingExitDelay common.Locktime
nostrDefaultRelays []string
@@ -61,7 +61,8 @@ type covenantlessService struct {
func NewCovenantlessService(
network common.Network,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64,
roundInterval int64,
roundLifetime, unilateralExitDelay, boardingExitDelay common.Locktime,
nostrDefaultRelays []string,
walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner,
@@ -303,6 +304,35 @@ func (s *covenantlessService) SubmitRedeemTx(
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)
if err != nil {
return "", fmt.Errorf("failed to parse control block: %s", err)
@@ -349,7 +379,7 @@ func (s *covenantlessService) SubmitRedeemTx(
}
// recompute redeem tx
rebuiltRedeemTx, err := bitcointree.BuildRedeemTx(ins, outputs, fees)
rebuiltRedeemTx, err := bitcointree.BuildRedeemTx(ins, outputs)
if err != nil {
return "", fmt.Errorf("failed to rebuild redeem tx: %s", err)
}
@@ -439,7 +469,7 @@ func (s *covenantlessService) SubmitRedeemTx(
func (s *covenantlessService) GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey,
) (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()
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 blocktime+int64(exitDelay) < now {
if blocktime+exitDelay.Seconds() < now {
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")
}
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
if err := boardingScript.Validate(s.pubkey, s.unilateralExitDelay); err != nil {
return nil, err
}
@@ -755,8 +785,8 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
return &ServiceInfo{
PubKey: pubkey,
RoundLifetime: s.roundLifetime,
UnilateralExitDelay: s.unilateralExitDelay,
RoundLifetime: int64(s.roundLifetime.Value),
UnilateralExitDelay: int64(s.unilateralExitDelay.Value),
RoundInterval: s.roundInterval,
Network: s.network.Name,
Dust: dust,
@@ -1064,7 +1094,7 @@ func (s *covenantlessService) startFinalization() {
sweepClosure := tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{s.pubkey}},
Seconds: uint(s.roundLifetime),
Locktime: s.roundLifetime,
}
sweepScript, err := sweepClosure.Script()
@@ -1561,7 +1591,7 @@ func (s *covenantlessService) scheduleSweepVtxosForRound(round *domain.Round) {
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 {
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) {
var forfeit tree.MultisigClosure
valid, err := forfeit.Decode(script)
closure, err := tree.DecodeClosure(script)
if err != nil {
return nil, err
}
if !valid {
return nil, fmt.Errorf("invalid forfeit closure script")
switch c := closure.(type) {
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"
"time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/note"
"github.com/ark-network/ark/common/tree"
"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)
if err != nil {
return nil, err
}
expirationTime = blocktimeCache[node.ParentTxid] + lifetime
expirationTime = blocktimeCache[node.ParentTxid] + int64(lifetime.Value)
} else {
// cache the blocktime for future use
if schedulerUnit == ports.BlockHeight {

View File

@@ -1,6 +1,7 @@
package ports
import (
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -47,7 +48,7 @@ type TxBuilder interface {
txs []string,
) (valid map[domain.VtxoKey][]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)
VerifyTapscriptPartialSigs(tx string) (valid bool, err error)
// 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)
SignMessage(ctx context.Context, message []byte) ([]byte, error)
VerifyMessageSignature(ctx context.Context, message, signature []byte) (bool, error)
GetCurrentBlockTime(ctx context.Context) (*BlockTimestamp, error)
Close()
}
@@ -63,3 +64,8 @@ type TxOutpoint interface {
GetTxid() string
GetIndex() uint32
}
type BlockTimestamp struct {
Height uint32
Time int64
}

View File

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

View File

@@ -28,15 +28,15 @@ import (
type txBuilder struct {
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
boardingExitDelay int64 // in seconds
roundLifetime common.Locktime
boardingExitDelay common.Locktime
}
func NewTxBuilder(
wallet ports.WalletService,
net common.Network,
roundLifetime int64,
boardingExitDelay int64,
roundLifetime common.Locktime,
boardingExitDelay common.Locktime,
) ports.TxBuilder {
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)
blocktimestamp, err := b.wallet.GetCurrentBlockTime(context.Background())
if err != nil {
return nil, err
}
for vtxoKey, psets := range forfeitTxsPsets {
if len(psets) == 0 {
continue
@@ -202,13 +207,36 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
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(
minRate,
&waddrmgr.Tapscript{
RevealedScript: vtxoTapscript.Script,
ControlBlock: &vtxoTapscript.ControlBlock.ControlBlock,
},
64*2,
closure.WitnessSize(),
txscript.GetScriptClass(forfeitScript),
)
if err != nil {
@@ -418,14 +446,14 @@ func (b *txBuilder) BuildRoundTx(
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)
if err != nil {
return -1, nil, err
return nil, nil, err
}
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
@@ -435,22 +463,22 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
sweepLeaf, lifetime, err := extractSweepLeaf(input)
if err != nil {
return -1, nil, err
return nil, nil, err
}
txhex, err := b.wallet.GetTransaction(context.Background(), txid)
if err != nil {
return -1, nil, err
return nil, nil, err
}
tx, err := transaction.NewTxFromHex(txhex)
if err != nil {
return -1, nil, err
return nil, nil, err
}
inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value)
if err != nil {
return -1, nil, err
return nil, nil, err
}
sweepInput = &sweepLiquidInput{
@@ -509,6 +537,10 @@ func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, error)
for _, key := range c.PubKeys {
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
@@ -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 {
closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(leaf.Script)
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
lifetime = int64(closure.Seconds)
lifetime = &closure.Locktime
}
}
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 {

View File

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

View File

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

View File

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

View File

@@ -27,12 +27,12 @@ import (
type txBuilder struct {
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
boardingExitDelay int64 // in seconds
roundLifetime common.Locktime
boardingExitDelay common.Locktime
}
func NewTxBuilder(
wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay int64,
wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay common.Locktime,
) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
}
@@ -91,6 +91,10 @@ func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, error) {
for _, key := range c.PubKeys {
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
@@ -326,6 +330,11 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
validForfeitTxs := make(map[domain.VtxoKey][]string)
blocktimestamp, err := b.wallet.GetCurrentBlockTime(context.Background())
if err != nil {
return nil, err
}
for vtxoKey, ptxs := range forfeitTxsPtxs {
if len(ptxs) == 0 {
continue
@@ -359,6 +368,30 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
}
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)
if err != nil {
return nil, err
@@ -370,7 +403,7 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
RevealedScript: vtxoTapscript.Script,
ControlBlock: ctrlBlock,
},
64*2,
closure.WitnessSize(),
txscript.GetScriptClass(forfeitScript),
)
if err != nil {
@@ -569,14 +602,14 @@ func (b *txBuilder) BuildRoundTx(
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)
if err != nil {
return -1, nil, err
return nil, nil, err
}
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]
@@ -585,17 +618,17 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
sweepLeaf, internalKey, lifetime, err := extractSweepLeaf(partialTx.Inputs[0])
if err != nil {
return -1, nil, err
return nil, nil, err
}
txhex, err := b.wallet.GetTransaction(context.Background(), txid.String())
if err != nil {
return -1, nil, err
return nil, nil, err
}
var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return -1, nil, err
return nil, nil, err
}
sweepInput = &sweepBitcoinInput{
@@ -1180,27 +1213,27 @@ func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
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 {
closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(leaf.Script)
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
lifetime = int64(closure.Seconds)
lifetime = &closure.Locktime
}
}
internalKey, err = schnorr.ParsePubKey(input.TaprootInternalKey)
if err != nil {
return nil, nil, 0, err
return nil, nil, nil, err
}
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

View File

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

View File

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

View File

@@ -37,7 +37,7 @@ func sweepTransaction(
return nil, fmt.Errorf("invalid csv script")
}
sequence, err := common.BIP68Sequence(sweepClosure.Seconds)
sequence, err := common.BIP68Sequence(sweepClosure.Locktime)
if err != nil {
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
}
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 {
vtxos := make(map[string][]ports.VtxoWithValue)

View File

@@ -2,8 +2,12 @@ package oceanwallet
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
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
isListening bool
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()))
if err != nil {
return nil, err
@@ -47,6 +60,7 @@ func NewService(addr string) (ports.WalletService, error) {
notifyClient: notifyClient,
chVtxos: chVtxos,
syncedCh: make(chan struct{}),
esploraURL: esploraURL,
}
ctx := context.Background()
@@ -168,6 +182,45 @@ func (s *service) VerifyMessageSignature(ctx context.Context, message, signature
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() {
s.isListening = true
defer func() {

View File

@@ -3,22 +3,35 @@ package e2e_test
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"testing"
"time"
"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"
"github.com/ark-network/ark/pkg/client-sdk/client"
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/redemption"
"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"
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"
"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/nip04"
"github.com/stretchr/testify/require"
@@ -411,6 +424,163 @@ func TestRedeemNotes(t *testing.T) {
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) {
var receive utils.ArkReceive
receiveStr, err := runClarkCommand("receive")

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync"
"time"
@@ -48,6 +49,24 @@ func GenerateBlock() error {
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) {
args := append([]string{"exec", "-t", container}, arg...)
return RunCommand("docker", args...)