mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 04:04:21 +01:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,14 +22,15 @@ const (
|
||||
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
||||
forfeitAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
||||
minRelayFee = uint64(30)
|
||||
roundLifetime = int64(1209344)
|
||||
boardingExitDelay = int64(512)
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,14 +22,15 @@ const (
|
||||
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
||||
forfeitAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
||||
changeAddress = "bcrt1qhhq55mut9easvrncy4se8q6vg3crlug7yj4j56"
|
||||
roundLifetime = int64(1209344)
|
||||
boardingExitDelay = int64(512)
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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...)
|
||||
|
||||
Reference in New Issue
Block a user