mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 12:14: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_MOD = 1 << SEQUENCE_LOCKTIME_GRANULARITY
|
||||||
SECONDS_MAX = SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
|
SECONDS_MAX = SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
|
||||||
SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31
|
SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31
|
||||||
|
|
||||||
|
SECONDS_PER_BLOCK = 10 * 60 // 10 minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
func closerToModulo512(x uint) uint {
|
type LocktimeType uint
|
||||||
return x - (x % 512)
|
|
||||||
|
const (
|
||||||
|
LocktimeTypeSecond LocktimeType = iota
|
||||||
|
LocktimeTypeBlock
|
||||||
|
)
|
||||||
|
|
||||||
|
// Locktime represents a BIP68 relative timelock value.
|
||||||
|
// This struct is comparable and can be used as a map key.
|
||||||
|
type Locktime struct {
|
||||||
|
Type LocktimeType
|
||||||
|
Value uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func BIP68Sequence(locktime uint) (uint32, error) {
|
func (l Locktime) Seconds() int64 {
|
||||||
isSeconds := locktime >= 512
|
if l.Type == LocktimeTypeBlock {
|
||||||
|
return int64(l.Value) * SECONDS_PER_BLOCK
|
||||||
|
}
|
||||||
|
return int64(l.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Locktime) Compare(other Locktime) int {
|
||||||
|
val := l.Seconds()
|
||||||
|
otherVal := other.Seconds()
|
||||||
|
|
||||||
|
if val == otherVal {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if val < otherVal {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// LessThan returns true if this locktime is less than the other locktime
|
||||||
|
func (l Locktime) LessThan(other Locktime) bool {
|
||||||
|
return l.Compare(other) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func BIP68Sequence(locktime Locktime) (uint32, error) {
|
||||||
|
value := locktime.Value
|
||||||
|
isSeconds := locktime.Type == LocktimeTypeSecond
|
||||||
if isSeconds {
|
if isSeconds {
|
||||||
locktime = closerToModulo512(locktime)
|
if value > SECONDS_MAX {
|
||||||
if locktime > SECONDS_MAX {
|
|
||||||
return 0, fmt.Errorf("seconds too large, max is %d", SECONDS_MAX)
|
return 0, fmt.Errorf("seconds too large, max is %d", SECONDS_MAX)
|
||||||
}
|
}
|
||||||
if locktime%SECONDS_MOD != 0 {
|
if value%SECONDS_MOD != 0 {
|
||||||
return 0, fmt.Errorf("seconds must be a multiple of %d", SECONDS_MOD)
|
return 0, fmt.Errorf("seconds must be a multiple of %d", SECONDS_MOD)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockchain.LockTimeToSequence(isSeconds, uint32(locktime)), nil
|
return blockchain.LockTimeToSequence(isSeconds, value), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BIP68DecodeSequence(sequence []byte) (uint, error) {
|
func BIP68DecodeSequence(sequence []byte) (*Locktime, error) {
|
||||||
scriptNumber, err := txscript.MakeScriptNum(sequence, true, len(sequence))
|
scriptNumber, err := txscript.MakeScriptNum(sequence, true, len(sequence))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if scriptNumber >= txscript.OP_1 && scriptNumber <= txscript.OP_16 {
|
if scriptNumber >= txscript.OP_1 && scriptNumber <= txscript.OP_16 {
|
||||||
@@ -48,12 +85,12 @@ func BIP68DecodeSequence(sequence []byte) (uint, error) {
|
|||||||
asNumber := int64(scriptNumber)
|
asNumber := int64(scriptNumber)
|
||||||
|
|
||||||
if asNumber&SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
|
if asNumber&SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
|
||||||
return 0, fmt.Errorf("sequence is disabled")
|
return nil, fmt.Errorf("sequence is disabled")
|
||||||
}
|
}
|
||||||
if asNumber&SEQUENCE_LOCKTIME_TYPE_FLAG != 0 {
|
if asNumber&SEQUENCE_LOCKTIME_TYPE_FLAG != 0 {
|
||||||
seconds := asNumber & SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
|
seconds := asNumber & SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
|
||||||
return uint(seconds), nil
|
return &Locktime{Type: LocktimeTypeSecond, Value: uint32(seconds)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return uint(asNumber), nil
|
return &Locktime{Type: LocktimeTypeBlock, Value: uint32(asNumber)}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
// CraftSharedOutput returns the taproot script and the amount of the initial root output
|
// CraftSharedOutput returns the taproot script and the amount of the initial root output
|
||||||
func CraftSharedOutput(
|
func CraftSharedOutput(
|
||||||
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
|
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
|
||||||
feeSatsPerNode uint64, roundLifetime int64,
|
feeSatsPerNode uint64, roundLifetime common.Locktime,
|
||||||
) ([]byte, int64, error) {
|
) ([]byte, int64, error) {
|
||||||
aggregatedKey, _, err := createAggregatedKeyWithSweep(
|
aggregatedKey, _, err := createAggregatedKeyWithSweep(
|
||||||
cosigners, server, roundLifetime,
|
cosigners, server, roundLifetime,
|
||||||
@@ -45,7 +45,7 @@ func CraftSharedOutput(
|
|||||||
// BuildVtxoTree creates all the tree's transactions
|
// BuildVtxoTree creates all the tree's transactions
|
||||||
func BuildVtxoTree(
|
func BuildVtxoTree(
|
||||||
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
|
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
|
||||||
feeSatsPerNode uint64, roundLifetime int64,
|
feeSatsPerNode uint64, roundLifetime common.Locktime,
|
||||||
) (tree.VtxoTree, error) {
|
) (tree.VtxoTree, error) {
|
||||||
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
|
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
|
||||||
cosigners, server, roundLifetime,
|
cosigners, server, roundLifetime,
|
||||||
@@ -280,11 +280,11 @@ func createRootNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createAggregatedKeyWithSweep(
|
func createAggregatedKeyWithSweep(
|
||||||
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime int64,
|
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime common.Locktime,
|
||||||
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
|
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
|
||||||
sweepClosure := &tree.CSVSigClosure{
|
sweepClosure := &tree.CSVSigClosure{
|
||||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server}},
|
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server}},
|
||||||
Seconds: uint(roundLifetime),
|
Locktime: roundLifetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepScript, err := sweepClosure.Script()
|
sweepScript, err := sweepClosure.Script()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/bitcointree"
|
"github.com/ark-network/ark/common/bitcointree"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
@@ -18,9 +19,10 @@ import (
|
|||||||
const (
|
const (
|
||||||
minRelayFee = 1000
|
minRelayFee = 1000
|
||||||
exitDelay = 512
|
exitDelay = 512
|
||||||
lifetime = 1024
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var lifetime = common.Locktime{Type: common.LocktimeTypeBlock, Value: 144}
|
||||||
|
|
||||||
var testTxid, _ = chainhash.NewHashFromStr("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12")
|
var testTxid, _ = chainhash.NewHashFromStr("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12")
|
||||||
|
|
||||||
func TestRoundTripSignTree(t *testing.T) {
|
func TestRoundTripSignTree(t *testing.T) {
|
||||||
@@ -63,7 +65,7 @@ func TestRoundTripSignTree(t *testing.T) {
|
|||||||
|
|
||||||
sweepClosure := &tree.CSVSigClosure{
|
sweepClosure := &tree.CSVSigClosure{
|
||||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server.PubKey()}},
|
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server.PubKey()}},
|
||||||
Seconds: uint(lifetime),
|
Locktime: lifetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepScript, err := sweepClosure.Script()
|
sweepScript, err := sweepClosure.Script()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
func BuildRedeemTx(
|
func BuildRedeemTx(
|
||||||
vtxos []common.VtxoInput,
|
vtxos []common.VtxoInput,
|
||||||
outputs []*wire.TxOut,
|
outputs []*wire.TxOut,
|
||||||
feeAmount int64,
|
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
if len(vtxos) <= 0 {
|
if len(vtxos) <= 0 {
|
||||||
return "", fmt.Errorf("missing vtxos")
|
return "", fmt.Errorf("missing vtxos")
|
||||||
@@ -50,10 +49,6 @@ func BuildRedeemTx(
|
|||||||
ins = append(ins, vtxo.Outpoint)
|
ins = append(ins, vtxo.Outpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
if feeAmount >= outputs[len(outputs)-1].Value {
|
|
||||||
return "", fmt.Errorf("redeem tx fee is higher than the amount of the change receiver")
|
|
||||||
}
|
|
||||||
|
|
||||||
sequences := make([]uint32, len(ins))
|
sequences := make([]uint32, len(ins))
|
||||||
for i := range sequences {
|
for i := range sequences {
|
||||||
sequences[i] = wire.MaxTxInSequenceNum
|
sequences[i] = wire.MaxTxInSequenceNum
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
@@ -71,7 +72,7 @@ func UnspendableKey() *secp256k1.PublicKey {
|
|||||||
// - every control block and taproot output scripts
|
// - every control block and taproot output scripts
|
||||||
// - input and output amounts
|
// - input and output amounts
|
||||||
func ValidateVtxoTree(
|
func ValidateVtxoTree(
|
||||||
vtxoTree tree.VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey, roundLifetime int64,
|
vtxoTree tree.VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey, roundLifetime common.Locktime,
|
||||||
) error {
|
) error {
|
||||||
roundTransaction, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
|
roundTransaction, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,7 +126,7 @@ func ValidateVtxoTree(
|
|||||||
|
|
||||||
sweepClosure := &tree.CSVSigClosure{
|
sweepClosure := &tree.CSVSigClosure{
|
||||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{serverPubkey}},
|
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{serverPubkey}},
|
||||||
Seconds: uint(roundLifetime),
|
Locktime: roundLifetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepScript, err := sweepClosure.Script()
|
sweepScript, err := sweepClosure.Script()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VtxoScript common.VtxoScript[bitcoinTapTree, *tree.MultisigClosure, *tree.CSVSigClosure]
|
type VtxoScript common.VtxoScript[bitcoinTapTree, tree.Closure]
|
||||||
|
|
||||||
func ParseVtxoScript(scripts []string) (VtxoScript, error) {
|
func ParseVtxoScript(scripts []string) (VtxoScript, error) {
|
||||||
types := []VtxoScript{
|
types := []VtxoScript{
|
||||||
@@ -26,7 +26,7 @@ func ParseVtxoScript(scripts []string) (VtxoScript, error) {
|
|||||||
return nil, fmt.Errorf("invalid vtxo scripts: %s", scripts)
|
return nil, fmt.Errorf("invalid vtxo scripts: %s", scripts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay uint) VtxoScript {
|
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay common.Locktime) VtxoScript {
|
||||||
base := tree.NewDefaultVtxoScript(owner, server, exitDelay)
|
base := tree.NewDefaultVtxoScript(owner, server, exitDelay)
|
||||||
|
|
||||||
return &TapscriptsVtxoScript{*base}
|
return &TapscriptsVtxoScript{*base}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func ParseDefaultVtxoDescriptor(
|
|||||||
|
|
||||||
if first, ok := andLeaf.First.(*Older); ok {
|
if first, ok := andLeaf.First.(*Older); ok {
|
||||||
if second, ok := andLeaf.Second.(*PK); ok {
|
if second, ok := andLeaf.Second.(*PK); ok {
|
||||||
timeout = first.Timeout
|
timeout = uint(first.Locktime.Value)
|
||||||
keyBytes, err := hex.DecodeString(second.Key.Hex)
|
keyBytes, err := hex.DecodeString(second.Key.Hex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, err
|
return nil, nil, 0, err
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ func (e *PK) Script(verify bool) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Older struct {
|
type Older struct {
|
||||||
Timeout uint
|
Locktime common.Locktime
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Older) String() string {
|
func (e *Older) String() string {
|
||||||
return fmt.Sprintf("older(%d)", e.Timeout)
|
return fmt.Sprintf("older(%d)", e.Locktime.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Older) Parse(policy string) error {
|
func (e *Older) Parse(policy string) error {
|
||||||
@@ -124,13 +124,13 @@ func (e *Older) Parse(policy string) error {
|
|||||||
return ErrInvalidOlderPolicy
|
return ErrInvalidOlderPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Timeout = uint(timeout)
|
e.Locktime = common.Locktime{Type: common.LocktimeTypeBlock, Value: uint32(timeout)}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Older) Script(bool) (string, error) {
|
func (e *Older) Script(bool) (string, error) {
|
||||||
sequence, err := common.BIP68Sequence(e.Timeout)
|
sequence, err := common.BIP68Sequence(e.Locktime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package descriptor_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/descriptor"
|
"github.com/ark-network/ark/common/descriptor"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -53,7 +54,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Second: &descriptor.Older{
|
Second: &descriptor.Older{
|
||||||
Timeout: 144,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 144},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -91,7 +92,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
First: &descriptor.Older{
|
First: &descriptor.Older{
|
||||||
Timeout: 604672,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 604672},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -156,7 +157,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
First: &descriptor.Older{
|
First: &descriptor.Older{
|
||||||
Timeout: 604672,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 604672},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&descriptor.And{
|
&descriptor.And{
|
||||||
@@ -232,7 +233,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&descriptor.And{
|
&descriptor.And{
|
||||||
First: &descriptor.Older{
|
First: &descriptor.Older{
|
||||||
Timeout: 512,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
},
|
},
|
||||||
Second: &descriptor.And{
|
Second: &descriptor.And{
|
||||||
First: &descriptor.PK{
|
First: &descriptor.PK{
|
||||||
@@ -278,7 +279,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&descriptor.And{
|
&descriptor.And{
|
||||||
First: &descriptor.Older{
|
First: &descriptor.Older{
|
||||||
Timeout: 1024,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
|
||||||
},
|
},
|
||||||
Second: &descriptor.And{
|
Second: &descriptor.And{
|
||||||
First: &descriptor.PK{
|
First: &descriptor.PK{
|
||||||
@@ -299,7 +300,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&descriptor.And{
|
&descriptor.And{
|
||||||
First: &descriptor.Older{
|
First: &descriptor.Older{
|
||||||
Timeout: 512,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
},
|
},
|
||||||
Second: &descriptor.PK{
|
Second: &descriptor.PK{
|
||||||
Key: descriptor.XOnlyKey{
|
Key: descriptor.XOnlyKey{
|
||||||
@@ -311,7 +312,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&descriptor.And{
|
&descriptor.And{
|
||||||
First: &descriptor.Older{
|
First: &descriptor.Older{
|
||||||
Timeout: 512,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
},
|
},
|
||||||
Second: &descriptor.And{
|
Second: &descriptor.And{
|
||||||
First: &descriptor.PK{
|
First: &descriptor.PK{
|
||||||
@@ -393,7 +394,7 @@ func TestCompileDescriptor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Second: &descriptor.Older{
|
Second: &descriptor.Older{
|
||||||
Timeout: 1024,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -464,14 +465,14 @@ func TestParseOlder(t *testing.T) {
|
|||||||
policy: "older(512)",
|
policy: "older(512)",
|
||||||
expectedScript: "03010040b275",
|
expectedScript: "03010040b275",
|
||||||
expected: descriptor.Older{
|
expected: descriptor.Older{
|
||||||
Timeout: uint(512),
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
policy: "older(1024)",
|
policy: "older(1024)",
|
||||||
expectedScript: "03020040b275",
|
expectedScript: "03020040b275",
|
||||||
expected: descriptor.Older{
|
expected: descriptor.Older{
|
||||||
Timeout: uint(1024),
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -506,7 +507,7 @@ func TestParseAnd(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Second: &descriptor.Older{
|
Second: &descriptor.Older{
|
||||||
Timeout: 512,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -522,7 +523,7 @@ func TestParseAnd(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
First: &descriptor.Older{
|
First: &descriptor.Older{
|
||||||
Timeout: 512,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
@@ -14,7 +15,7 @@ import (
|
|||||||
|
|
||||||
func BuildVtxoTree(
|
func BuildVtxoTree(
|
||||||
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
|
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
|
||||||
feeSatsPerNode uint64, roundLifetime int64,
|
feeSatsPerNode uint64, roundLifetime common.Locktime,
|
||||||
) (
|
) (
|
||||||
factoryFn TreeFactory,
|
factoryFn TreeFactory,
|
||||||
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
|
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
|
||||||
@@ -53,7 +54,7 @@ type node struct {
|
|||||||
right *node
|
right *node
|
||||||
asset string
|
asset string
|
||||||
feeSats uint64
|
feeSats uint64
|
||||||
roundLifetime int64
|
roundLifetime common.Locktime
|
||||||
|
|
||||||
_inputTaprootKey *secp256k1.PublicKey
|
_inputTaprootKey *secp256k1.PublicKey
|
||||||
_inputTaprootTree *taproot.IndexedElementsTapScriptTree
|
_inputTaprootTree *taproot.IndexedElementsTapScriptTree
|
||||||
@@ -164,7 +165,7 @@ func (n *node) getWitnessData() (
|
|||||||
|
|
||||||
sweepClosure := &CSVSigClosure{
|
sweepClosure := &CSVSigClosure{
|
||||||
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{n.sweepKey}},
|
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{n.sweepKey}},
|
||||||
Seconds: uint(n.roundLifetime),
|
Locktime: n.roundLifetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepLeaf, err := sweepClosure.Script()
|
sweepLeaf, err := sweepClosure.Script()
|
||||||
@@ -380,7 +381,7 @@ func (n *node) buildVtxoTree() TreeFactory {
|
|||||||
|
|
||||||
func buildTreeNodes(
|
func buildTreeNodes(
|
||||||
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
|
asset string, serverPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
|
||||||
feeSatsPerNode uint64, roundLifetime int64,
|
feeSatsPerNode uint64, roundLifetime common.Locktime,
|
||||||
) (root *node, err error) {
|
) (root *node, err error) {
|
||||||
if len(receivers) == 0 {
|
if len(receivers) == 0 {
|
||||||
return nil, fmt.Errorf("no receivers provided")
|
return nil, fmt.Errorf("no receivers provided")
|
||||||
|
|||||||
@@ -53,16 +53,25 @@ type MultisigClosure struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CSVSigClosure is a closure that contains a list of public keys and a
|
// CSVSigClosure is a closure that contains a list of public keys and a
|
||||||
// CHECKSEQUENCEVERIFY + DROP. The witness size is 64 bytes per key, admitting
|
// CHECKSEQUENCEVERIFY. The witness size is 64 bytes per key, admitting
|
||||||
// the sighash type is SIGHASH_DEFAULT.
|
// the sighash type is SIGHASH_DEFAULT.
|
||||||
type CSVSigClosure struct {
|
type CSVSigClosure struct {
|
||||||
MultisigClosure
|
MultisigClosure
|
||||||
Seconds uint
|
Locktime common.Locktime
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLTVMultisigClosure is a closure that contains a list of public keys and a
|
||||||
|
// CHECKLOCKTIMEVERIFY. The witness size is 64 bytes per key, admitting
|
||||||
|
// the sighash type is SIGHASH_DEFAULT.
|
||||||
|
type CLTVMultisigClosure struct {
|
||||||
|
MultisigClosure
|
||||||
|
Locktime common.Locktime
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeClosure(script []byte) (Closure, error) {
|
func DecodeClosure(script []byte) (Closure, error) {
|
||||||
types := []Closure{
|
types := []Closure{
|
||||||
&CSVSigClosure{},
|
&CSVSigClosure{},
|
||||||
|
&CLTVMultisigClosure{},
|
||||||
&MultisigClosure{},
|
&MultisigClosure{},
|
||||||
&UnrollClosure{},
|
&UnrollClosure{},
|
||||||
}
|
}
|
||||||
@@ -324,8 +333,13 @@ func (f *CSVSigClosure) WitnessSize() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *CSVSigClosure) Script() ([]byte, error) {
|
func (d *CSVSigClosure) Script() ([]byte, error) {
|
||||||
|
sequence, err := common.BIP68Sequence(d.Locktime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
csvScript, err := txscript.NewScriptBuilder().
|
csvScript, err := txscript.NewScriptBuilder().
|
||||||
AddInt64(int64(d.Seconds)).
|
AddInt64(int64(sequence)).
|
||||||
AddOps([]byte{
|
AddOps([]byte{
|
||||||
txscript.OP_CHECKSEQUENCEVERIFY,
|
txscript.OP_CHECKSEQUENCEVERIFY,
|
||||||
txscript.OP_DROP,
|
txscript.OP_DROP,
|
||||||
@@ -360,7 +374,7 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
|
|||||||
sequence = sequence[1:]
|
sequence = sequence[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
seconds, err := common.BIP68DecodeSequence(sequence)
|
locktime, err := common.BIP68DecodeSequence(sequence)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -375,7 +389,7 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Seconds = seconds
|
d.Locktime = *locktime
|
||||||
d.MultisigClosure = *multisigClosure
|
d.MultisigClosure = *multisigClosure
|
||||||
|
|
||||||
return valid, nil
|
return valid, nil
|
||||||
@@ -666,3 +680,87 @@ func (c *UnrollClosure) Witness(controlBlock []byte, _ map[string][]byte) (wire.
|
|||||||
// UnrollClosure only needs script and control block
|
// UnrollClosure only needs script and control block
|
||||||
return wire.TxWitness{script, controlBlock}, nil
|
return wire.TxWitness{script, controlBlock}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *CLTVMultisigClosure) Witness(controlBlock []byte, signatures map[string][]byte) (wire.TxWitness, error) {
|
||||||
|
multisigWitness, err := f.MultisigClosure.Witness(controlBlock, signatures)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := f.Script()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace script with cltv script
|
||||||
|
multisigWitness[len(multisigWitness)-2] = script
|
||||||
|
|
||||||
|
return multisigWitness, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *CLTVMultisigClosure) WitnessSize() int {
|
||||||
|
return f.MultisigClosure.WitnessSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CLTVMultisigClosure) Script() ([]byte, error) {
|
||||||
|
locktime, err := common.BIP68Sequence(d.Locktime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cltvScript, err := txscript.NewScriptBuilder().
|
||||||
|
AddInt64(int64(locktime)).
|
||||||
|
AddOps([]byte{
|
||||||
|
txscript.OP_CHECKLOCKTIMEVERIFY,
|
||||||
|
txscript.OP_DROP,
|
||||||
|
}).
|
||||||
|
Script()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
multisigScript, err := d.MultisigClosure.Script()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(cltvScript, multisigScript...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CLTVMultisigClosure) Decode(script []byte) (bool, error) {
|
||||||
|
if len(script) == 0 {
|
||||||
|
return false, fmt.Errorf("empty script")
|
||||||
|
}
|
||||||
|
|
||||||
|
cltvIndex := bytes.Index(
|
||||||
|
script, []byte{txscript.OP_CHECKLOCKTIMEVERIFY, txscript.OP_DROP},
|
||||||
|
)
|
||||||
|
if cltvIndex == -1 || cltvIndex == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
locktime := script[:cltvIndex]
|
||||||
|
if len(locktime) > 1 {
|
||||||
|
locktime = locktime[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
locktimeValue, err := common.BIP68DecodeSequence(locktime)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
multisigClosure := &MultisigClosure{}
|
||||||
|
valid, err := multisigClosure.Decode(script[cltvIndex+2:])
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Locktime = *locktimeValue
|
||||||
|
d.MultisigClosure = *multisigClosure
|
||||||
|
|
||||||
|
return valid, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package tree_test
|
package tree_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
@@ -19,7 +21,7 @@ func TestRoundTripCSV(t *testing.T) {
|
|||||||
MultisigClosure: tree.MultisigClosure{
|
MultisigClosure: tree.MultisigClosure{
|
||||||
PubKeys: []*secp256k1.PublicKey{seckey.PubKey()},
|
PubKeys: []*secp256k1.PublicKey{seckey.PubKey()},
|
||||||
},
|
},
|
||||||
Seconds: 1024,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf, err := csvSig.Script()
|
leaf, err := csvSig.Script()
|
||||||
@@ -31,7 +33,7 @@ func TestRoundTripCSV(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, valid)
|
require.True(t, valid)
|
||||||
|
|
||||||
require.Equal(t, csvSig.Seconds, cl.Seconds)
|
require.Equal(t, csvSig.Locktime.Value, cl.Locktime.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultisigClosure(t *testing.T) {
|
func TestMultisigClosure(t *testing.T) {
|
||||||
@@ -194,7 +196,7 @@ func TestCSVSigClosure(t *testing.T) {
|
|||||||
MultisigClosure: tree.MultisigClosure{
|
MultisigClosure: tree.MultisigClosure{
|
||||||
PubKeys: []*secp256k1.PublicKey{pubkey1},
|
PubKeys: []*secp256k1.PublicKey{pubkey1},
|
||||||
},
|
},
|
||||||
Seconds: 1024,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
|
||||||
}
|
}
|
||||||
|
|
||||||
script, err := csvSig.Script()
|
script, err := csvSig.Script()
|
||||||
@@ -204,7 +206,7 @@ func TestCSVSigClosure(t *testing.T) {
|
|||||||
valid, err := decodedCSV.Decode(script)
|
valid, err := decodedCSV.Decode(script)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, valid)
|
require.True(t, valid)
|
||||||
require.Equal(t, uint32(1024), uint32(decodedCSV.Seconds))
|
require.Equal(t, uint32(1024), uint32(decodedCSV.Locktime.Value))
|
||||||
require.Equal(t, 1, len(decodedCSV.PubKeys))
|
require.Equal(t, 1, len(decodedCSV.PubKeys))
|
||||||
require.Equal(t,
|
require.Equal(t,
|
||||||
schnorr.SerializePubKey(pubkey1),
|
schnorr.SerializePubKey(pubkey1),
|
||||||
@@ -217,7 +219,7 @@ func TestCSVSigClosure(t *testing.T) {
|
|||||||
MultisigClosure: tree.MultisigClosure{
|
MultisigClosure: tree.MultisigClosure{
|
||||||
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
|
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
|
||||||
},
|
},
|
||||||
Seconds: 2016, // ~2 weeks
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512 * 4}, // ~2 weeks
|
||||||
}
|
}
|
||||||
|
|
||||||
script, err := csvSig.Script()
|
script, err := csvSig.Script()
|
||||||
@@ -227,7 +229,7 @@ func TestCSVSigClosure(t *testing.T) {
|
|||||||
valid, err := decodedCSV.Decode(script)
|
valid, err := decodedCSV.Decode(script)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, valid)
|
require.True(t, valid)
|
||||||
require.Equal(t, uint32(2016), uint32(decodedCSV.Seconds))
|
require.Equal(t, uint32(512*4), uint32(decodedCSV.Locktime.Value))
|
||||||
require.Equal(t, 2, len(decodedCSV.PubKeys))
|
require.Equal(t, 2, len(decodedCSV.PubKeys))
|
||||||
require.Equal(t,
|
require.Equal(t,
|
||||||
schnorr.SerializePubKey(pubkey1),
|
schnorr.SerializePubKey(pubkey1),
|
||||||
@@ -265,7 +267,7 @@ func TestCSVSigClosure(t *testing.T) {
|
|||||||
MultisigClosure: tree.MultisigClosure{
|
MultisigClosure: tree.MultisigClosure{
|
||||||
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
|
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
|
||||||
},
|
},
|
||||||
Seconds: 1024,
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
|
||||||
}
|
}
|
||||||
// Should be same as multisig witness size (64 bytes per signature)
|
// Should be same as multisig witness size (64 bytes per signature)
|
||||||
require.Equal(t, 128, csvSig.WitnessSize())
|
require.Equal(t, 128, csvSig.WitnessSize())
|
||||||
@@ -276,7 +278,7 @@ func TestCSVSigClosure(t *testing.T) {
|
|||||||
MultisigClosure: tree.MultisigClosure{
|
MultisigClosure: tree.MultisigClosure{
|
||||||
PubKeys: []*secp256k1.PublicKey{pubkey1},
|
PubKeys: []*secp256k1.PublicKey{pubkey1},
|
||||||
},
|
},
|
||||||
Seconds: 65535, // Maximum allowed value
|
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: common.SECONDS_MAX}, // Maximum allowed value
|
||||||
}
|
}
|
||||||
|
|
||||||
script, err := csvSig.Script()
|
script, err := csvSig.Script()
|
||||||
@@ -286,7 +288,7 @@ func TestCSVSigClosure(t *testing.T) {
|
|||||||
valid, err := decodedCSV.Decode(script)
|
valid, err := decodedCSV.Decode(script)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, valid)
|
require.True(t, valid)
|
||||||
require.Equal(t, uint32(65535), uint32(decodedCSV.Seconds))
|
require.Equal(t, uint32(common.SECONDS_MAX), decodedCSV.Locktime.Value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +418,7 @@ func TestCSVSigClosureWitness(t *testing.T) {
|
|||||||
MultisigClosure: tree.MultisigClosure{
|
MultisigClosure: tree.MultisigClosure{
|
||||||
PubKeys: []*secp256k1.PublicKey{pub1},
|
PubKeys: []*secp256k1.PublicKey{pub1},
|
||||||
},
|
},
|
||||||
Seconds: 144,
|
Locktime: common.Locktime{Type: common.LocktimeTypeBlock, Value: 144},
|
||||||
}
|
}
|
||||||
|
|
||||||
witness, err := closure.Witness(controlBlock, signatures)
|
witness, err := closure.Witness(controlBlock, signatures)
|
||||||
@@ -469,3 +471,156 @@ func TestDecodeChecksigAdd(t *testing.T) {
|
|||||||
require.Equal(t, tree.MultisigTypeChecksigAdd, multisigClosure.Type, "expected MultisigTypeChecksigAdd")
|
require.Equal(t, tree.MultisigTypeChecksigAdd, multisigClosure.Type, "expected MultisigTypeChecksigAdd")
|
||||||
require.Equal(t, 3, len(multisigClosure.PubKeys), "expected 3 public keys")
|
require.Equal(t, 3, len(multisigClosure.PubKeys), "expected 3 public keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCLTVMultisigClosure(t *testing.T) {
|
||||||
|
// Generate test keys
|
||||||
|
privkey1, err := secp256k1.GeneratePrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubkey1 := privkey1.PubKey()
|
||||||
|
|
||||||
|
privkey2, err := secp256k1.GeneratePrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubkey2 := privkey2.PubKey()
|
||||||
|
|
||||||
|
locktime := common.Locktime{
|
||||||
|
Type: common.LocktimeTypeBlock,
|
||||||
|
Value: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("valid single key with CLTV", func(t *testing.T) {
|
||||||
|
closure := &tree.CLTVMultisigClosure{
|
||||||
|
MultisigClosure: tree.MultisigClosure{
|
||||||
|
PubKeys: []*secp256k1.PublicKey{pubkey1},
|
||||||
|
Type: tree.MultisigTypeChecksig,
|
||||||
|
},
|
||||||
|
Locktime: locktime,
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := closure.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decodedClosure := &tree.CLTVMultisigClosure{}
|
||||||
|
valid, err := decodedClosure.Decode(script)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, valid)
|
||||||
|
require.Equal(t, closure.Locktime, decodedClosure.Locktime)
|
||||||
|
require.Equal(t, 1, len(decodedClosure.PubKeys))
|
||||||
|
require.True(t, closure.PubKeys[0].IsEqual(decodedClosure.PubKeys[0]))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid two keys with CLTV", func(t *testing.T) {
|
||||||
|
closure := &tree.CLTVMultisigClosure{
|
||||||
|
MultisigClosure: tree.MultisigClosure{
|
||||||
|
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
|
||||||
|
Type: tree.MultisigTypeChecksig,
|
||||||
|
},
|
||||||
|
Locktime: locktime,
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := closure.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decodedClosure := &tree.CLTVMultisigClosure{}
|
||||||
|
valid, err := decodedClosure.Decode(script)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, valid)
|
||||||
|
require.Equal(t, closure.Locktime, decodedClosure.Locktime)
|
||||||
|
require.Equal(t, 2, len(decodedClosure.PubKeys))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid two keys with CLTV using checksigadd", func(t *testing.T) {
|
||||||
|
closure := &tree.CLTVMultisigClosure{
|
||||||
|
MultisigClosure: tree.MultisigClosure{
|
||||||
|
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
|
||||||
|
Type: tree.MultisigTypeChecksigAdd,
|
||||||
|
},
|
||||||
|
Locktime: locktime,
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := closure.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decodedClosure := &tree.CLTVMultisigClosure{}
|
||||||
|
valid, err := decodedClosure.Decode(script)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, valid)
|
||||||
|
require.Equal(t, closure.Locktime, decodedClosure.Locktime)
|
||||||
|
require.Equal(t, closure.Type, decodedClosure.Type)
|
||||||
|
require.Equal(t, 2, len(decodedClosure.PubKeys))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("witness generation", func(t *testing.T) {
|
||||||
|
closure := &tree.CLTVMultisigClosure{
|
||||||
|
MultisigClosure: tree.MultisigClosure{
|
||||||
|
PubKeys: []*secp256k1.PublicKey{pubkey1, pubkey2},
|
||||||
|
Type: tree.MultisigTypeChecksig,
|
||||||
|
},
|
||||||
|
Locktime: locktime,
|
||||||
|
}
|
||||||
|
|
||||||
|
controlBlock := bytes.Repeat([]byte{0x00}, 32)
|
||||||
|
signatures := map[string][]byte{
|
||||||
|
hex.EncodeToString(schnorr.SerializePubKey(pubkey1)): bytes.Repeat([]byte{0x01}, 64),
|
||||||
|
hex.EncodeToString(schnorr.SerializePubKey(pubkey2)): bytes.Repeat([]byte{0x01}, 64),
|
||||||
|
}
|
||||||
|
|
||||||
|
witness, err := closure.Witness(controlBlock, signatures)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 4, len(witness)) // 2 sigs + script + control block
|
||||||
|
|
||||||
|
script, err := closure.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, script, witness[2])
|
||||||
|
require.Equal(t, controlBlock, witness[3])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid cases", func(t *testing.T) {
|
||||||
|
validClosure := &tree.CLTVMultisigClosure{
|
||||||
|
MultisigClosure: tree.MultisigClosure{
|
||||||
|
PubKeys: []*secp256k1.PublicKey{pubkey1},
|
||||||
|
Type: tree.MultisigTypeChecksig,
|
||||||
|
},
|
||||||
|
Locktime: locktime,
|
||||||
|
}
|
||||||
|
script, err := validClosure.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
emptyScriptErr := "empty script"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
script []byte
|
||||||
|
err *string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty script",
|
||||||
|
script: []byte{},
|
||||||
|
err: &emptyScriptErr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid CLTV index",
|
||||||
|
script: append([]byte{txscript.OP_CHECKLOCKTIMEVERIFY, txscript.OP_DROP}, script...),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing CLTV",
|
||||||
|
script: script[5:],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid multisig after CLTV",
|
||||||
|
script: append(script[:len(script)-1], txscript.OP_CHECKSIGVERIFY),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
closure := &tree.CLTVMultisigClosure{}
|
||||||
|
valid, err := closure.Decode(tc.script)
|
||||||
|
require.False(t, valid)
|
||||||
|
if tc.err != nil {
|
||||||
|
require.Contains(t, err.Error(), *tc.err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
@@ -72,7 +73,7 @@ func UnspendableKey() *secp256k1.PublicKey {
|
|||||||
// - input and output amounts
|
// - input and output amounts
|
||||||
func ValidateVtxoTree(
|
func ValidateVtxoTree(
|
||||||
tree VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey,
|
tree VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey,
|
||||||
roundLifetime int64,
|
roundLifetime common.Locktime,
|
||||||
) error {
|
) error {
|
||||||
roundTransaction, err := psetv2.NewPsetFromBase64(roundTx)
|
roundTransaction, err := psetv2.NewPsetFromBase64(roundTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,7 +149,7 @@ func ValidateVtxoTree(
|
|||||||
func validateNodeTransaction(
|
func validateNodeTransaction(
|
||||||
node Node, tree VtxoTree,
|
node Node, tree VtxoTree,
|
||||||
expectedInternalKey, expectedServerPubkey *secp256k1.PublicKey,
|
expectedInternalKey, expectedServerPubkey *secp256k1.PublicKey,
|
||||||
expectedSequence int64,
|
expectedLifetime common.Locktime,
|
||||||
) error {
|
) error {
|
||||||
if node.Tx == "" {
|
if node.Tx == "" {
|
||||||
return ErrNodeTxEmpty
|
return ErrNodeTxEmpty
|
||||||
@@ -242,7 +243,8 @@ func validateNodeTransaction(
|
|||||||
schnorr.SerializePubKey(c.MultisigClosure.PubKeys[0]),
|
schnorr.SerializePubKey(c.MultisigClosure.PubKeys[0]),
|
||||||
schnorr.SerializePubKey(expectedServerPubkey),
|
schnorr.SerializePubKey(expectedServerPubkey),
|
||||||
)
|
)
|
||||||
isSweepDelay := int64(c.Seconds) == expectedSequence
|
|
||||||
|
isSweepDelay := c.Locktime == expectedLifetime
|
||||||
|
|
||||||
if isServer && !isSweepDelay {
|
if isServer && !isSweepDelay {
|
||||||
return ErrInvalidSweepSequence
|
return ErrInvalidSweepSequence
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/ark-network/ark/common"
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
@@ -17,7 +16,7 @@ var (
|
|||||||
ErrNoExitLeaf = fmt.Errorf("no exit leaf")
|
ErrNoExitLeaf = fmt.Errorf("no exit leaf")
|
||||||
)
|
)
|
||||||
|
|
||||||
type VtxoScript common.VtxoScript[elementsTapTree, *MultisigClosure, *CSVSigClosure]
|
type VtxoScript common.VtxoScript[elementsTapTree, Closure]
|
||||||
|
|
||||||
func ParseVtxoScript(scripts []string) (VtxoScript, error) {
|
func ParseVtxoScript(scripts []string) (VtxoScript, error) {
|
||||||
v := &TapscriptsVtxoScript{}
|
v := &TapscriptsVtxoScript{}
|
||||||
@@ -26,12 +25,12 @@ func ParseVtxoScript(scripts []string) (VtxoScript, error) {
|
|||||||
return v, err
|
return v, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay uint) *TapscriptsVtxoScript {
|
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay common.Locktime) *TapscriptsVtxoScript {
|
||||||
return &TapscriptsVtxoScript{
|
return &TapscriptsVtxoScript{
|
||||||
[]Closure{
|
[]Closure{
|
||||||
&CSVSigClosure{
|
&CSVSigClosure{
|
||||||
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner}},
|
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner}},
|
||||||
Seconds: exitDelay,
|
Locktime: exitDelay,
|
||||||
},
|
},
|
||||||
&MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner, server}},
|
&MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner, server}},
|
||||||
},
|
},
|
||||||
@@ -73,12 +72,17 @@ func (v *TapscriptsVtxoScript) Decode(scripts []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *TapscriptsVtxoScript) Validate(server *secp256k1.PublicKey, minExitDelay uint) error {
|
func (v *TapscriptsVtxoScript) Validate(server *secp256k1.PublicKey, minLocktime common.Locktime) error {
|
||||||
serverXonly := schnorr.SerializePubKey(server)
|
serverXonly := schnorr.SerializePubKey(server)
|
||||||
for _, forfeit := range v.ForfeitClosures() {
|
for _, forfeit := range v.ForfeitClosures() {
|
||||||
|
multisigClosure, ok := forfeit.(*MultisigClosure)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid forfeit closure, expected MultisigClosure")
|
||||||
|
}
|
||||||
|
|
||||||
// must contain server pubkey
|
// must contain server pubkey
|
||||||
found := false
|
found := false
|
||||||
for _, pubkey := range forfeit.PubKeys {
|
for _, pubkey := range multisigClosure.PubKeys {
|
||||||
if bytes.Equal(schnorr.SerializePubKey(pubkey), serverXonly) {
|
if bytes.Equal(schnorr.SerializePubKey(pubkey), serverXonly) {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -97,46 +101,48 @@ func (v *TapscriptsVtxoScript) Validate(server *secp256k1.PublicKey, minExitDela
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if smallestExit < minExitDelay {
|
if smallestExit.LessThan(minLocktime) {
|
||||||
return fmt.Errorf("exit delay is too short")
|
return fmt.Errorf("exit delay is too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *TapscriptsVtxoScript) SmallestExitDelay() (uint, error) {
|
func (v *TapscriptsVtxoScript) SmallestExitDelay() (*common.Locktime, error) {
|
||||||
smallest := uint(math.MaxUint32)
|
var smallest *common.Locktime
|
||||||
|
|
||||||
for _, closure := range v.Closures {
|
for _, closure := range v.Closures {
|
||||||
if csvClosure, ok := closure.(*CSVSigClosure); ok {
|
if csvClosure, ok := closure.(*CSVSigClosure); ok {
|
||||||
if csvClosure.Seconds < smallest {
|
if smallest == nil || csvClosure.Locktime.LessThan(*smallest) {
|
||||||
smallest = csvClosure.Seconds
|
smallest = &csvClosure.Locktime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if smallest == math.MaxUint32 {
|
if smallest == nil {
|
||||||
return 0, ErrNoExitLeaf
|
return nil, ErrNoExitLeaf
|
||||||
}
|
}
|
||||||
|
|
||||||
return smallest, nil
|
return smallest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *TapscriptsVtxoScript) ForfeitClosures() []*MultisigClosure {
|
func (v *TapscriptsVtxoScript) ForfeitClosures() []Closure {
|
||||||
forfeits := make([]*MultisigClosure, 0)
|
forfeits := make([]Closure, 0)
|
||||||
for _, closure := range v.Closures {
|
for _, closure := range v.Closures {
|
||||||
if multisigClosure, ok := closure.(*MultisigClosure); ok {
|
switch closure.(type) {
|
||||||
forfeits = append(forfeits, multisigClosure)
|
case *MultisigClosure, *CLTVMultisigClosure:
|
||||||
|
forfeits = append(forfeits, closure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return forfeits
|
return forfeits
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *TapscriptsVtxoScript) ExitClosures() []*CSVSigClosure {
|
func (v *TapscriptsVtxoScript) ExitClosures() []Closure {
|
||||||
exits := make([]*CSVSigClosure, 0)
|
exits := make([]Closure, 0)
|
||||||
for _, closure := range v.Closures {
|
for _, closure := range v.Closures {
|
||||||
if csvClosure, ok := closure.(*CSVSigClosure); ok {
|
switch closure.(type) {
|
||||||
exits = append(exits, csvClosure)
|
case *CSVSigClosure:
|
||||||
|
exits = append(exits, closure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return exits
|
return exits
|
||||||
|
|||||||
@@ -31,15 +31,17 @@ It may also contain others closures implementing specific use cases.
|
|||||||
|
|
||||||
VtxoScript abstracts the taproot complexity behind vtxo contracts.
|
VtxoScript abstracts the taproot complexity behind vtxo contracts.
|
||||||
it is compiled, transferred and parsed using descriptor string.
|
it is compiled, transferred and parsed using descriptor string.
|
||||||
|
|
||||||
|
// TODO gather common and tree package to prevent circular dependency and move C generic
|
||||||
*/
|
*/
|
||||||
type VtxoScript[T TaprootTree, F interface{}, E interface{}] interface {
|
type VtxoScript[T TaprootTree, C interface{}] interface {
|
||||||
Validate(server *secp256k1.PublicKey, minExitDelay uint) error
|
Validate(server *secp256k1.PublicKey, minLocktime Locktime) error
|
||||||
TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error)
|
TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error)
|
||||||
Encode() ([]string, error)
|
Encode() ([]string, error)
|
||||||
Decode(scripts []string) error
|
Decode(scripts []string) error
|
||||||
SmallestExitDelay() (uint, error)
|
SmallestExitDelay() (*Locktime, error)
|
||||||
ForfeitClosures() []F
|
ForfeitClosures() []C
|
||||||
ExitClosures() []E
|
ExitClosures() []C
|
||||||
}
|
}
|
||||||
|
|
||||||
// BiggestLeafMerkleProof returns the leaf with the biggest witness size (for fee estimation)
|
// BiggestLeafMerkleProof returns the leaf with the biggest witness size (for fee estimation)
|
||||||
|
|||||||
@@ -146,15 +146,25 @@ func (a *arkClient) initWithWallet(
|
|||||||
return fmt.Errorf("failed to parse server pubkey: %s", err)
|
return fmt.Errorf("failed to parse server pubkey: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifetimeType := common.LocktimeTypeBlock
|
||||||
|
if info.RoundLifetime >= 512 {
|
||||||
|
lifetimeType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
unilateralExitDelayType := common.LocktimeTypeBlock
|
||||||
|
if info.UnilateralExitDelay >= 512 {
|
||||||
|
unilateralExitDelayType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
storeData := types.Config{
|
storeData := types.Config{
|
||||||
ServerUrl: args.ServerUrl,
|
ServerUrl: args.ServerUrl,
|
||||||
ServerPubKey: serverPubkey,
|
ServerPubKey: serverPubkey,
|
||||||
WalletType: args.Wallet.GetType(),
|
WalletType: args.Wallet.GetType(),
|
||||||
ClientType: args.ClientType,
|
ClientType: args.ClientType,
|
||||||
Network: network,
|
Network: network,
|
||||||
RoundLifetime: info.RoundLifetime,
|
RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(info.RoundLifetime)},
|
||||||
RoundInterval: info.RoundInterval,
|
RoundInterval: info.RoundInterval,
|
||||||
UnilateralExitDelay: info.UnilateralExitDelay,
|
UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(info.UnilateralExitDelay)},
|
||||||
Dust: info.Dust,
|
Dust: info.Dust,
|
||||||
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
||||||
ForfeitAddress: info.ForfeitAddress,
|
ForfeitAddress: info.ForfeitAddress,
|
||||||
@@ -213,15 +223,25 @@ func (a *arkClient) init(
|
|||||||
return fmt.Errorf("failed to parse server pubkey: %s", err)
|
return fmt.Errorf("failed to parse server pubkey: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifetimeType := common.LocktimeTypeBlock
|
||||||
|
if info.RoundLifetime >= 512 {
|
||||||
|
lifetimeType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
unilateralExitDelayType := common.LocktimeTypeBlock
|
||||||
|
if info.UnilateralExitDelay >= 512 {
|
||||||
|
unilateralExitDelayType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
cfgData := types.Config{
|
cfgData := types.Config{
|
||||||
ServerUrl: args.ServerUrl,
|
ServerUrl: args.ServerUrl,
|
||||||
ServerPubKey: serverPubkey,
|
ServerPubKey: serverPubkey,
|
||||||
WalletType: args.WalletType,
|
WalletType: args.WalletType,
|
||||||
ClientType: args.ClientType,
|
ClientType: args.ClientType,
|
||||||
Network: network,
|
Network: network,
|
||||||
RoundLifetime: info.RoundLifetime,
|
RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(info.RoundLifetime)},
|
||||||
RoundInterval: info.RoundInterval,
|
RoundInterval: info.RoundInterval,
|
||||||
UnilateralExitDelay: info.UnilateralExitDelay,
|
UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(info.UnilateralExitDelay)},
|
||||||
Dust: info.Dust,
|
Dust: info.Dust,
|
||||||
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
||||||
ExplorerURL: args.ExplorerURL,
|
ExplorerURL: args.ExplorerURL,
|
||||||
@@ -347,8 +367,8 @@ func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time {
|
func getCreatedAtFromExpiry(roundLifetime common.Locktime, expiry time.Time) time.Time {
|
||||||
return expiry.Add(-time.Duration(roundLifetime) * time.Second)
|
return expiry.Add(-time.Duration(roundLifetime.Seconds()) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterByOutpoints(vtxos []client.Vtxo, outpoints []client.Outpoint) []client.Vtxo {
|
func filterByOutpoints(vtxos []client.Vtxo, outpoints []client.Outpoint) []client.Vtxo {
|
||||||
|
|||||||
@@ -708,7 +708,7 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context, opts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
|
u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
|
||||||
if u.SpendableAt.Before(now) {
|
if u.SpendableAt.Before(now) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1535,7 +1535,7 @@ func (a *covenantArkClient) coinSelectOnchain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, utxo := range utxos {
|
for _, utxo := range utxos {
|
||||||
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
|
u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
|
||||||
if u.SpendableAt.Before(now) {
|
if u.SpendableAt.Before(now) {
|
||||||
fetchedUtxos = append(fetchedUtxos, u)
|
fetchedUtxos = append(fetchedUtxos, u)
|
||||||
}
|
}
|
||||||
@@ -1571,7 +1571,7 @@ func (a *covenantArkClient) coinSelectOnchain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, utxo := range utxos {
|
for _, utxo := range utxos {
|
||||||
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts)
|
u := utxo.ToUtxo(a.UnilateralExitDelay, addr.Tapscripts)
|
||||||
if u.SpendableAt.Before(now) {
|
if u.SpendableAt.Before(now) {
|
||||||
fetchedUtxos = append(fetchedUtxos, u)
|
fetchedUtxos = append(fetchedUtxos, u)
|
||||||
}
|
}
|
||||||
@@ -1719,7 +1719,7 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []
|
|||||||
}
|
}
|
||||||
|
|
||||||
func vtxosToTxsCovenant(
|
func vtxosToTxsCovenant(
|
||||||
roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []types.Transaction,
|
roundLifetime common.Locktime, spendable, spent []client.Vtxo, boardingTxs []types.Transaction,
|
||||||
) ([]types.Transaction, error) {
|
) ([]types.Transaction, error) {
|
||||||
transactions := make([]types.Transaction, 0)
|
transactions := make([]types.Transaction, 0)
|
||||||
unconfirmedBoardingTxs := make([]types.Transaction, 0)
|
unconfirmedBoardingTxs := make([]types.Transaction, 0)
|
||||||
|
|||||||
@@ -1703,7 +1703,7 @@ func (a *covenantlessArkClient) handleRoundSigningStarted(
|
|||||||
) (signerSession bitcointree.SignerSession, err error) {
|
) (signerSession bitcointree.SignerSession, err error) {
|
||||||
sweepClosure := tree.CSVSigClosure{
|
sweepClosure := tree.CSVSigClosure{
|
||||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{a.ServerPubKey}},
|
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{a.ServerPubKey}},
|
||||||
Seconds: uint(a.RoundLifetime),
|
Locktime: a.RoundLifetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
script, err := sweepClosure.Script()
|
script, err := sweepClosure.Script()
|
||||||
@@ -2163,7 +2163,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, utxo := range utxos {
|
for _, utxo := range utxos {
|
||||||
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
|
u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
|
||||||
if u.SpendableAt.Before(now) {
|
if u.SpendableAt.Before(now) {
|
||||||
fetchedUtxos = append(fetchedUtxos, u)
|
fetchedUtxos = append(fetchedUtxos, u)
|
||||||
}
|
}
|
||||||
@@ -2199,7 +2199,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, utxo := range utxos {
|
for _, utxo := range utxos {
|
||||||
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts)
|
u := utxo.ToUtxo(a.UnilateralExitDelay, addr.Tapscripts)
|
||||||
if u.SpendableAt.Before(now) {
|
if u.SpendableAt.Before(now) {
|
||||||
fetchedUtxos = append(fetchedUtxos, u)
|
fetchedUtxos = append(fetchedUtxos, u)
|
||||||
}
|
}
|
||||||
@@ -2387,7 +2387,7 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context, o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
|
u := utxo.ToUtxo(*boardingTimeout, addr.Tapscripts)
|
||||||
if u.SpendableAt.Before(now) {
|
if u.SpendableAt.Before(now) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -2682,5 +2682,5 @@ func buildRedeemTx(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return bitcointree.BuildRedeemTx(ins, outs, fees)
|
return bitcointree.BuildRedeemTx(ins, outs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ type SpentStatus struct {
|
|||||||
SpentBy string `json:"txid,omitempty"`
|
SpentBy string `json:"txid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ExplorerUtxo) ToUtxo(delay uint, tapscripts []string) types.Utxo {
|
func (e ExplorerUtxo) ToUtxo(delay common.Locktime, tapscripts []string) types.Utxo {
|
||||||
return newUtxo(e, delay, tapscripts)
|
return newUtxo(e, delay, tapscripts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ type Explorer interface {
|
|||||||
GetUtxos(addr string) ([]ExplorerUtxo, error)
|
GetUtxos(addr string) ([]ExplorerUtxo, error)
|
||||||
GetBalance(addr string) (uint64, error)
|
GetBalance(addr string) (uint64, error)
|
||||||
GetRedeemedVtxosBalance(
|
GetRedeemedVtxosBalance(
|
||||||
addr string, unilateralExitDelay int64,
|
addr string, unilateralExitDelay common.Locktime,
|
||||||
) (uint64, map[int64]uint64, error)
|
) (uint64, map[int64]uint64, error)
|
||||||
GetTxBlockTime(
|
GetTxBlockTime(
|
||||||
txid string,
|
txid string,
|
||||||
@@ -247,7 +247,7 @@ func (e *explorerSvc) GetBalance(addr string) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *explorerSvc) GetRedeemedVtxosBalance(
|
func (e *explorerSvc) GetRedeemedVtxosBalance(
|
||||||
addr string, unilateralExitDelay int64,
|
addr string, unilateralExitDelay common.Locktime,
|
||||||
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
|
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
|
||||||
utxos, err := e.GetUtxos(addr)
|
utxos, err := e.GetUtxos(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -262,7 +262,7 @@ func (e *explorerSvc) GetRedeemedVtxosBalance(
|
|||||||
blocktime = time.Unix(utxo.Status.Blocktime, 0)
|
blocktime = time.Unix(utxo.Status.Blocktime, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
delay := time.Duration(unilateralExitDelay) * time.Second
|
delay := time.Duration(unilateralExitDelay.Seconds()) * time.Second
|
||||||
availableAt := blocktime.Add(delay)
|
availableAt := blocktime.Add(delay)
|
||||||
if availableAt.After(now) {
|
if availableAt.After(now) {
|
||||||
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
|
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
|
||||||
@@ -415,7 +415,7 @@ func parseBitcoinTx(txStr string) (string, string, error) {
|
|||||||
return txhex, txid, nil
|
return txhex, txid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUtxo(explorerUtxo ExplorerUtxo, delay uint, tapscripts []string) types.Utxo {
|
func newUtxo(explorerUtxo ExplorerUtxo, delay common.Locktime, tapscripts []string) types.Utxo {
|
||||||
utxoTime := explorerUtxo.Status.Blocktime
|
utxoTime := explorerUtxo.Status.Blocktime
|
||||||
createdAt := time.Unix(utxoTime, 0)
|
createdAt := time.Unix(utxoTime, 0)
|
||||||
if utxoTime == 0 {
|
if utxoTime == 0 {
|
||||||
@@ -429,7 +429,7 @@ func newUtxo(explorerUtxo ExplorerUtxo, delay uint, tapscripts []string) types.U
|
|||||||
Amount: explorerUtxo.Amount,
|
Amount: explorerUtxo.Amount,
|
||||||
Asset: explorerUtxo.Asset,
|
Asset: explorerUtxo.Asset,
|
||||||
Delay: delay,
|
Delay: delay,
|
||||||
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
|
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay.Seconds()) * time.Second),
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
Tapscripts: tapscripts,
|
Tapscripts: tapscripts,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||||
@@ -26,12 +27,12 @@ func NewCovenantRedeemBranch(
|
|||||||
explorer explorer.Explorer,
|
explorer explorer.Explorer,
|
||||||
vtxoTree tree.VtxoTree, vtxo client.Vtxo,
|
vtxoTree tree.VtxoTree, vtxo client.Vtxo,
|
||||||
) (*CovenantRedeemBranch, error) {
|
) (*CovenantRedeemBranch, error) {
|
||||||
sweepClosure, seconds, err := findCovenantSweepClosure(vtxoTree)
|
sweepClosure, locktime, err := findCovenantSweepClosure(vtxoTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
|
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", locktime.Seconds()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -183,19 +184,19 @@ func (r *CovenantRedeemBranch) offchainPath() ([]*psetv2.Pset, error) {
|
|||||||
|
|
||||||
func findCovenantSweepClosure(
|
func findCovenantSweepClosure(
|
||||||
vtxoTree tree.VtxoTree,
|
vtxoTree tree.VtxoTree,
|
||||||
) (*taproot.TapElementsLeaf, uint, error) {
|
) (*taproot.TapElementsLeaf, *common.Locktime, error) {
|
||||||
root, err := vtxoTree.Root()
|
root, err := vtxoTree.Root()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the sweep closure
|
// find the sweep closure
|
||||||
tx, err := psetv2.NewPsetFromBase64(root.Tx)
|
tx, err := psetv2.NewPsetFromBase64(root.Tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var seconds uint
|
var locktime *common.Locktime
|
||||||
var sweepClosure *taproot.TapElementsLeaf
|
var sweepClosure *taproot.TapElementsLeaf
|
||||||
for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
|
for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
|
||||||
closure := &tree.CSVSigClosure{}
|
closure := &tree.CSVSigClosure{}
|
||||||
@@ -204,15 +205,15 @@ func findCovenantSweepClosure(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid && closure.Seconds > seconds {
|
if valid && (locktime == nil || closure.Locktime.LessThan(*locktime)) {
|
||||||
seconds = closure.Seconds
|
locktime = &closure.Locktime
|
||||||
sweepClosure = &tapLeaf.TapElementsLeaf
|
sweepClosure = &tapLeaf.TapElementsLeaf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sweepClosure == nil {
|
if sweepClosure == nil {
|
||||||
return nil, 0, fmt.Errorf("sweep closure not found")
|
return nil, nil, fmt.Errorf("sweep closure not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sweepClosure, seconds, nil
|
return sweepClosure, locktime, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||||
@@ -25,12 +26,12 @@ func NewCovenantlessRedeemBranch(
|
|||||||
explorer explorer.Explorer,
|
explorer explorer.Explorer,
|
||||||
vtxoTree tree.VtxoTree, vtxo client.Vtxo,
|
vtxoTree tree.VtxoTree, vtxo client.Vtxo,
|
||||||
) (*CovenantlessRedeemBranch, error) {
|
) (*CovenantlessRedeemBranch, error) {
|
||||||
_, seconds, err := findCovenantlessSweepClosure(vtxoTree)
|
_, locktime, err := findCovenantlessSweepClosure(vtxoTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
|
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", locktime.Seconds()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -156,19 +157,19 @@ func (r *CovenantlessRedeemBranch) OffchainPath() ([]*psbt.Packet, error) {
|
|||||||
|
|
||||||
func findCovenantlessSweepClosure(
|
func findCovenantlessSweepClosure(
|
||||||
vtxoTree tree.VtxoTree,
|
vtxoTree tree.VtxoTree,
|
||||||
) (*txscript.TapLeaf, uint, error) {
|
) (*txscript.TapLeaf, *common.Locktime, error) {
|
||||||
root, err := vtxoTree.Root()
|
root, err := vtxoTree.Root()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the sweep closure
|
// find the sweep closure
|
||||||
tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true)
|
tx, err := psbt.NewFromRawBytes(strings.NewReader(root.Tx), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var seconds uint
|
var locktime *common.Locktime
|
||||||
var sweepClosure *txscript.TapLeaf
|
var sweepClosure *txscript.TapLeaf
|
||||||
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
|
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
|
||||||
closure := &tree.CSVSigClosure{}
|
closure := &tree.CSVSigClosure{}
|
||||||
@@ -177,16 +178,16 @@ func findCovenantlessSweepClosure(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid && closure.Seconds > seconds {
|
if valid && (locktime == nil || closure.Locktime.LessThan(*locktime)) {
|
||||||
seconds = closure.Seconds
|
locktime = &closure.Locktime
|
||||||
leaf := txscript.NewBaseTapLeaf(tapLeaf.Script)
|
leaf := txscript.NewBaseTapLeaf(tapLeaf.Script)
|
||||||
sweepClosure = &leaf
|
sweepClosure = &leaf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sweepClosure == nil {
|
if sweepClosure == nil {
|
||||||
return nil, 0, fmt.Errorf("sweep closure not found")
|
return nil, nil, fmt.Errorf("sweep closure not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sweepClosure, seconds, nil
|
return sweepClosure, locktime, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ func (s *configStore) AddData(ctx context.Context, data types.Config) error {
|
|||||||
WalletType: data.WalletType,
|
WalletType: data.WalletType,
|
||||||
ClientType: data.ClientType,
|
ClientType: data.ClientType,
|
||||||
Network: data.Network.Name,
|
Network: data.Network.Name,
|
||||||
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime),
|
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime.Value),
|
||||||
RoundInterval: fmt.Sprintf("%d", data.RoundInterval),
|
RoundInterval: fmt.Sprintf("%d", data.RoundInterval),
|
||||||
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay),
|
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay.Value),
|
||||||
Dust: fmt.Sprintf("%d", data.Dust),
|
Dust: fmt.Sprintf("%d", data.Dust),
|
||||||
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
|
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
|
||||||
ExplorerURL: data.ExplorerURL,
|
ExplorerURL: data.ExplorerURL,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/types"
|
"github.com/ark-network/ark/pkg/client-sdk/types"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
@@ -44,14 +45,25 @@ func (d storeData) decode() types.Config {
|
|||||||
buf, _ := hex.DecodeString(d.ServerPubKey)
|
buf, _ := hex.DecodeString(d.ServerPubKey)
|
||||||
serverPubkey, _ := secp256k1.ParsePubKey(buf)
|
serverPubkey, _ := secp256k1.ParsePubKey(buf)
|
||||||
explorerURL := d.ExplorerURL
|
explorerURL := d.ExplorerURL
|
||||||
|
|
||||||
|
lifetimeType := common.LocktimeTypeBlock
|
||||||
|
if roundLifetime >= 512 {
|
||||||
|
lifetimeType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
unilateralExitDelayType := common.LocktimeTypeBlock
|
||||||
|
if unilateralExitDelay >= 512 {
|
||||||
|
unilateralExitDelayType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
return types.Config{
|
return types.Config{
|
||||||
ServerUrl: d.ServerUrl,
|
ServerUrl: d.ServerUrl,
|
||||||
ServerPubKey: serverPubkey,
|
ServerPubKey: serverPubkey,
|
||||||
WalletType: d.WalletType,
|
WalletType: d.WalletType,
|
||||||
ClientType: d.ClientType,
|
ClientType: d.ClientType,
|
||||||
Network: network,
|
Network: network,
|
||||||
RoundLifetime: int64(roundLifetime),
|
RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(roundLifetime)},
|
||||||
UnilateralExitDelay: int64(unilateralExitDelay),
|
UnilateralExitDelay: common.Locktime{Type: unilateralExitDelayType, Value: uint32(unilateralExitDelay)},
|
||||||
RoundInterval: int64(roundInterval),
|
RoundInterval: int64(roundInterval),
|
||||||
Dust: uint64(dust),
|
Dust: uint64(dust),
|
||||||
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,
|
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ func TestStore(t *testing.T) {
|
|||||||
WalletType: wallet.SingleKeyWallet,
|
WalletType: wallet.SingleKeyWallet,
|
||||||
ClientType: client.GrpcClient,
|
ClientType: client.GrpcClient,
|
||||||
Network: common.LiquidRegTest,
|
Network: common.LiquidRegTest,
|
||||||
RoundLifetime: 512,
|
RoundLifetime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
RoundInterval: 10,
|
RoundInterval: 10,
|
||||||
UnilateralExitDelay: 512,
|
UnilateralExitDelay: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
Dust: 1000,
|
Dust: 1000,
|
||||||
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
|
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
|
||||||
ForfeitAddress: "bcrt1qzvqj",
|
ForfeitAddress: "bcrt1qzvqj",
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ type Config struct {
|
|||||||
WalletType string
|
WalletType string
|
||||||
ClientType string
|
ClientType string
|
||||||
Network common.Network
|
Network common.Network
|
||||||
RoundLifetime int64
|
RoundLifetime common.Locktime
|
||||||
RoundInterval int64
|
RoundInterval int64
|
||||||
UnilateralExitDelay int64
|
UnilateralExitDelay common.Locktime
|
||||||
Dust uint64
|
Dust uint64
|
||||||
BoardingDescriptorTemplate string
|
BoardingDescriptorTemplate string
|
||||||
ExplorerURL string
|
ExplorerURL string
|
||||||
@@ -110,7 +110,7 @@ type Utxo struct {
|
|||||||
VOut uint32
|
VOut uint32
|
||||||
Amount uint64
|
Amount uint64
|
||||||
Asset string // liquid only
|
Asset string // liquid only
|
||||||
Delay uint
|
Delay common.Locktime
|
||||||
SpendableAt time.Time
|
SpendableAt time.Time
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
Tapscripts []string
|
Tapscripts []string
|
||||||
|
|||||||
@@ -220,6 +220,13 @@ func (s *bitcoinWallet) SignTransaction(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
for _, key := range c.MultisigClosure.PubKeys {
|
||||||
|
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
|
||||||
|
sign = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sign {
|
if sign {
|
||||||
@@ -310,7 +317,7 @@ func (w *bitcoinWallet) getAddress(
|
|||||||
defaultVtxoScript := bitcointree.NewDefaultVtxoScript(
|
defaultVtxoScript := bitcointree.NewDefaultVtxoScript(
|
||||||
w.walletData.PubKey,
|
w.walletData.PubKey,
|
||||||
data.ServerPubKey,
|
data.ServerPubKey,
|
||||||
uint(data.UnilateralExitDelay),
|
data.UnilateralExitDelay,
|
||||||
)
|
)
|
||||||
|
|
||||||
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
|
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
|
||||||
@@ -327,7 +334,10 @@ func (w *bitcoinWallet) getAddress(
|
|||||||
boardingVtxoScript := bitcointree.NewDefaultVtxoScript(
|
boardingVtxoScript := bitcointree.NewDefaultVtxoScript(
|
||||||
w.walletData.PubKey,
|
w.walletData.PubKey,
|
||||||
data.ServerPubKey,
|
data.ServerPubKey,
|
||||||
uint(data.UnilateralExitDelay*2),
|
common.Locktime{
|
||||||
|
Type: data.UnilateralExitDelay.Type,
|
||||||
|
Value: data.UnilateralExitDelay.Value * 2,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
||||||
|
|||||||
@@ -242,6 +242,13 @@ func (s *liquidWallet) SignTransaction(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
for _, key := range c.MultisigClosure.PubKeys {
|
||||||
|
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
|
||||||
|
sign = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sign {
|
if sign {
|
||||||
@@ -332,7 +339,7 @@ func (w *liquidWallet) getAddress(
|
|||||||
vtxoScript := tree.NewDefaultVtxoScript(
|
vtxoScript := tree.NewDefaultVtxoScript(
|
||||||
w.walletData.PubKey,
|
w.walletData.PubKey,
|
||||||
data.ServerPubKey,
|
data.ServerPubKey,
|
||||||
uint(data.UnilateralExitDelay),
|
data.UnilateralExitDelay,
|
||||||
)
|
)
|
||||||
|
|
||||||
vtxoTapKey, _, err := vtxoScript.TapTree()
|
vtxoTapKey, _, err := vtxoScript.TapTree()
|
||||||
@@ -349,7 +356,10 @@ func (w *liquidWallet) getAddress(
|
|||||||
boardingVtxoScript := tree.NewDefaultVtxoScript(
|
boardingVtxoScript := tree.NewDefaultVtxoScript(
|
||||||
w.walletData.PubKey,
|
w.walletData.PubKey,
|
||||||
data.ServerPubKey,
|
data.ServerPubKey,
|
||||||
uint(data.UnilateralExitDelay*2),
|
common.Locktime{
|
||||||
|
Type: data.UnilateralExitDelay.Type,
|
||||||
|
Value: data.UnilateralExitDelay.Value * 2,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ func TestWallet(t *testing.T) {
|
|||||||
WalletType: wallet.SingleKeyWallet,
|
WalletType: wallet.SingleKeyWallet,
|
||||||
ClientType: client.GrpcClient,
|
ClientType: client.GrpcClient,
|
||||||
Network: common.LiquidRegTest,
|
Network: common.LiquidRegTest,
|
||||||
RoundLifetime: 512,
|
RoundLifetime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
RoundInterval: 10,
|
RoundInterval: 10,
|
||||||
UnilateralExitDelay: 512,
|
UnilateralExitDelay: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
|
||||||
Dust: 1000,
|
Dust: 1000,
|
||||||
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
|
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
|
||||||
ForfeitAddress: "bcrt1qzvqj",
|
ForfeitAddress: "bcrt1qzvqj",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/types"
|
"github.com/ark-network/ark/pkg/client-sdk/types"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
@@ -59,9 +60,9 @@ func (s *configStore) AddData(ctx context.Context, data types.Config) error {
|
|||||||
WalletType: data.WalletType,
|
WalletType: data.WalletType,
|
||||||
ClientType: data.ClientType,
|
ClientType: data.ClientType,
|
||||||
Network: data.Network.Name,
|
Network: data.Network.Name,
|
||||||
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime),
|
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime.Value),
|
||||||
RoundInterval: fmt.Sprintf("%d", data.RoundInterval),
|
RoundInterval: fmt.Sprintf("%d", data.RoundInterval),
|
||||||
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay),
|
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay.Value),
|
||||||
Dust: fmt.Sprintf("%d", data.Dust),
|
Dust: fmt.Sprintf("%d", data.Dust),
|
||||||
ExplorerURL: data.ExplorerURL,
|
ExplorerURL: data.ExplorerURL,
|
||||||
ForfeitAddress: data.ForfeitAddress,
|
ForfeitAddress: data.ForfeitAddress,
|
||||||
@@ -94,15 +95,25 @@ func (s *configStore) GetData(ctx context.Context) (*types.Config, error) {
|
|||||||
dust, _ := strconv.Atoi(s.store.Call("getItem", "dust").String())
|
dust, _ := strconv.Atoi(s.store.Call("getItem", "dust").String())
|
||||||
withTxFeed, _ := strconv.ParseBool(s.store.Call("getItem", "with_transaction_feed").String())
|
withTxFeed, _ := strconv.ParseBool(s.store.Call("getItem", "with_transaction_feed").String())
|
||||||
|
|
||||||
|
lifetimeType := common.LocktimeTypeBlock
|
||||||
|
if roundLifetime >= 512 {
|
||||||
|
lifetimeType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
unilateralExitDelayType := common.LocktimeTypeBlock
|
||||||
|
if unilateralExitDelay >= 512 {
|
||||||
|
unilateralExitDelayType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
return &types.Config{
|
return &types.Config{
|
||||||
ServerUrl: s.store.Call("getItem", "server_url").String(),
|
ServerUrl: s.store.Call("getItem", "server_url").String(),
|
||||||
ServerPubKey: serverPubkey,
|
ServerPubKey: serverPubkey,
|
||||||
WalletType: s.store.Call("getItem", "wallet_type").String(),
|
WalletType: s.store.Call("getItem", "wallet_type").String(),
|
||||||
ClientType: s.store.Call("getItem", "client_type").String(),
|
ClientType: s.store.Call("getItem", "client_type").String(),
|
||||||
Network: network,
|
Network: network,
|
||||||
RoundLifetime: int64(roundLifetime),
|
RoundLifetime: common.Locktime{Value: uint32(roundLifetime), Type: lifetimeType},
|
||||||
RoundInterval: int64(roundInterval),
|
RoundInterval: int64(roundInterval),
|
||||||
UnilateralExitDelay: int64(unilateralExitDelay),
|
UnilateralExitDelay: common.Locktime{Value: uint32(unilateralExitDelay), Type: unilateralExitDelayType},
|
||||||
Dust: uint64(dust),
|
Dust: uint64(dust),
|
||||||
ExplorerURL: s.store.Call("getItem", "explorer_url").String(),
|
ExplorerURL: s.store.Call("getItem", "explorer_url").String(),
|
||||||
ForfeitAddress: s.store.Call("getItem", "forfeit_address").String(),
|
ForfeitAddress: s.store.Call("getItem", "forfeit_address").String(),
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ func GetRoundLifetimeWrapper() js.Func {
|
|||||||
data, _ := arkSdkClient.GetConfigData(context.Background())
|
data, _ := arkSdkClient.GetConfigData(context.Background())
|
||||||
var roundLifettime int64
|
var roundLifettime int64
|
||||||
if data != nil {
|
if data != nil {
|
||||||
roundLifettime = data.RoundLifetime
|
roundLifettime = data.RoundLifetime.Seconds()
|
||||||
}
|
}
|
||||||
return js.ValueOf(roundLifettime)
|
return js.ValueOf(roundLifettime)
|
||||||
})
|
})
|
||||||
@@ -385,7 +385,7 @@ func GetUnilateralExitDelayWrapper() js.Func {
|
|||||||
data, _ := arkSdkClient.GetConfigData(context.Background())
|
data, _ := arkSdkClient.GetConfigData(context.Background())
|
||||||
var unilateralExitDelay int64
|
var unilateralExitDelay int64
|
||||||
if data != nil {
|
if data != nil {
|
||||||
unilateralExitDelay = data.UnilateralExitDelay
|
unilateralExitDelay = data.UnilateralExitDelay.Seconds()
|
||||||
}
|
}
|
||||||
return js.ValueOf(unilateralExitDelay)
|
return js.ValueOf(unilateralExitDelay)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ func mainAction(_ *cli.Context) error {
|
|||||||
TLSExtraDomains: cfg.TLSExtraDomains,
|
TLSExtraDomains: cfg.TLSExtraDomains,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifetimeType, unilateralExitType, boardingExitType := common.LocktimeTypeBlock, common.LocktimeTypeBlock, common.LocktimeTypeBlock
|
||||||
|
if cfg.RoundLifetime >= 512 {
|
||||||
|
lifetimeType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
if cfg.UnilateralExitDelay >= 512 {
|
||||||
|
unilateralExitType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
if cfg.BoardingExitDelay >= 512 {
|
||||||
|
boardingExitType = common.LocktimeTypeSecond
|
||||||
|
}
|
||||||
|
|
||||||
appConfig := &appconfig.Config{
|
appConfig := &appconfig.Config{
|
||||||
EventDbType: cfg.EventDbType,
|
EventDbType: cfg.EventDbType,
|
||||||
DbType: cfg.DbType,
|
DbType: cfg.DbType,
|
||||||
@@ -70,8 +81,8 @@ func mainAction(_ *cli.Context) error {
|
|||||||
SchedulerType: cfg.SchedulerType,
|
SchedulerType: cfg.SchedulerType,
|
||||||
TxBuilderType: cfg.TxBuilderType,
|
TxBuilderType: cfg.TxBuilderType,
|
||||||
WalletAddr: cfg.WalletAddr,
|
WalletAddr: cfg.WalletAddr,
|
||||||
RoundLifetime: cfg.RoundLifetime,
|
RoundLifetime: common.Locktime{Type: lifetimeType, Value: uint32(cfg.RoundLifetime)},
|
||||||
UnilateralExitDelay: cfg.UnilateralExitDelay,
|
UnilateralExitDelay: common.Locktime{Type: unilateralExitType, Value: uint32(cfg.UnilateralExitDelay)},
|
||||||
EsploraURL: cfg.EsploraURL,
|
EsploraURL: cfg.EsploraURL,
|
||||||
NeutrinoPeer: cfg.NeutrinoPeer,
|
NeutrinoPeer: cfg.NeutrinoPeer,
|
||||||
BitcoindRpcUser: cfg.BitcoindRpcUser,
|
BitcoindRpcUser: cfg.BitcoindRpcUser,
|
||||||
@@ -79,7 +90,7 @@ func mainAction(_ *cli.Context) error {
|
|||||||
BitcoindRpcHost: cfg.BitcoindRpcHost,
|
BitcoindRpcHost: cfg.BitcoindRpcHost,
|
||||||
BitcoindZMQBlock: cfg.BitcoindZMQBlock,
|
BitcoindZMQBlock: cfg.BitcoindZMQBlock,
|
||||||
BitcoindZMQTx: cfg.BitcoindZMQTx,
|
BitcoindZMQTx: cfg.BitcoindZMQTx,
|
||||||
BoardingExitDelay: cfg.BoardingExitDelay,
|
BoardingExitDelay: common.Locktime{Type: boardingExitType, Value: uint32(cfg.BoardingExitDelay)},
|
||||||
UnlockerType: cfg.UnlockerType,
|
UnlockerType: cfg.UnlockerType,
|
||||||
UnlockerFilePath: cfg.UnlockerFilePath,
|
UnlockerFilePath: cfg.UnlockerFilePath,
|
||||||
UnlockerPassword: cfg.UnlockerPassword,
|
UnlockerPassword: cfg.UnlockerPassword,
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ type Config struct {
|
|||||||
SchedulerType string
|
SchedulerType string
|
||||||
TxBuilderType string
|
TxBuilderType string
|
||||||
WalletAddr string
|
WalletAddr string
|
||||||
RoundLifetime int64
|
RoundLifetime common.Locktime
|
||||||
UnilateralExitDelay int64
|
UnilateralExitDelay common.Locktime
|
||||||
BoardingExitDelay int64
|
BoardingExitDelay common.Locktime
|
||||||
NostrDefaultRelays []string
|
NostrDefaultRelays []string
|
||||||
NoteUriPrefix string
|
NoteUriPrefix string
|
||||||
MarketHourStartTime time.Time
|
MarketHourStartTime time.Time
|
||||||
@@ -119,18 +119,18 @@ func (c *Config) Validate() error {
|
|||||||
if !supportedNetworks.supports(c.Network.Name) {
|
if !supportedNetworks.supports(c.Network.Name) {
|
||||||
return fmt.Errorf("invalid network, must be one of: %s", supportedNetworks)
|
return fmt.Errorf("invalid network, must be one of: %s", supportedNetworks)
|
||||||
}
|
}
|
||||||
if c.RoundLifetime < minAllowedSequence {
|
if c.RoundLifetime.Type == common.LocktimeTypeBlock {
|
||||||
if c.SchedulerType != "block" {
|
if c.SchedulerType != "block" {
|
||||||
return fmt.Errorf("scheduler type must be block if round lifetime is expressed in blocks")
|
return fmt.Errorf("scheduler type must be block if round lifetime is expressed in blocks")
|
||||||
}
|
}
|
||||||
} else {
|
} else { // seconds
|
||||||
if c.SchedulerType != "gocron" {
|
if c.SchedulerType != "gocron" {
|
||||||
return fmt.Errorf("scheduler type must be gocron if round lifetime is expressed in seconds")
|
return fmt.Errorf("scheduler type must be gocron if round lifetime is expressed in seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
// round life time must be a multiple of 512 if expressed in seconds
|
// round life time must be a multiple of 512 if expressed in seconds
|
||||||
if c.RoundLifetime%minAllowedSequence != 0 {
|
if c.RoundLifetime.Value%minAllowedSequence != 0 {
|
||||||
c.RoundLifetime -= c.RoundLifetime % minAllowedSequence
|
c.RoundLifetime.Value -= c.RoundLifetime.Value % minAllowedSequence
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"round lifetime must be a multiple of %d, rounded to %d",
|
"round lifetime must be a multiple of %d, rounded to %d",
|
||||||
minAllowedSequence, c.RoundLifetime,
|
minAllowedSequence, c.RoundLifetime,
|
||||||
@@ -138,28 +138,28 @@ func (c *Config) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.UnilateralExitDelay < minAllowedSequence {
|
if c.UnilateralExitDelay.Type == common.LocktimeTypeBlock {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"invalid unilateral exit delay, must at least %d", minAllowedSequence,
|
"invalid unilateral exit delay, must at least %d", minAllowedSequence,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.BoardingExitDelay < minAllowedSequence {
|
if c.BoardingExitDelay.Type == common.LocktimeTypeBlock {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"invalid boarding exit delay, must at least %d", minAllowedSequence,
|
"invalid boarding exit delay, must at least %d", minAllowedSequence,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.UnilateralExitDelay%minAllowedSequence != 0 {
|
if c.UnilateralExitDelay.Value%minAllowedSequence != 0 {
|
||||||
c.UnilateralExitDelay -= c.UnilateralExitDelay % minAllowedSequence
|
c.UnilateralExitDelay.Value -= c.UnilateralExitDelay.Value % minAllowedSequence
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"unilateral exit delay must be a multiple of %d, rounded to %d",
|
"unilateral exit delay must be a multiple of %d, rounded to %d",
|
||||||
minAllowedSequence, c.UnilateralExitDelay,
|
minAllowedSequence, c.UnilateralExitDelay,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.BoardingExitDelay%minAllowedSequence != 0 {
|
if c.BoardingExitDelay.Value%minAllowedSequence != 0 {
|
||||||
c.BoardingExitDelay -= c.BoardingExitDelay % minAllowedSequence
|
c.BoardingExitDelay.Value -= c.BoardingExitDelay.Value % minAllowedSequence
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"boarding exit delay must be a multiple of %d, rounded to %d",
|
"boarding exit delay must be a multiple of %d, rounded to %d",
|
||||||
minAllowedSequence, c.BoardingExitDelay,
|
minAllowedSequence, c.BoardingExitDelay,
|
||||||
@@ -260,7 +260,7 @@ func (c *Config) repoManager() error {
|
|||||||
|
|
||||||
func (c *Config) walletService() error {
|
func (c *Config) walletService() error {
|
||||||
if common.IsLiquid(c.Network) {
|
if common.IsLiquid(c.Network) {
|
||||||
svc, err := liquidwallet.NewService(c.WalletAddr)
|
svc, err := liquidwallet.NewService(c.WalletAddr, c.EsploraURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to wallet: %s", err)
|
return fmt.Errorf("failed to connect to wallet: %s", err)
|
||||||
}
|
}
|
||||||
@@ -384,7 +384,7 @@ func (c *Config) appService() error {
|
|||||||
|
|
||||||
func (c *Config) adminService() error {
|
func (c *Config) adminService() error {
|
||||||
unit := ports.UnixTime
|
unit := ports.UnixTime
|
||||||
if c.RoundLifetime < minAllowedSequence {
|
if c.RoundLifetime.Value < minAllowedSequence {
|
||||||
unit = ports.BlockHeight
|
unit = ports.BlockHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ var (
|
|||||||
type covenantService struct {
|
type covenantService struct {
|
||||||
network common.Network
|
network common.Network
|
||||||
pubkey *secp256k1.PublicKey
|
pubkey *secp256k1.PublicKey
|
||||||
roundLifetime int64
|
|
||||||
roundInterval int64
|
roundInterval int64
|
||||||
unilateralExitDelay int64
|
roundLifetime common.Locktime
|
||||||
boardingExitDelay int64
|
unilateralExitDelay common.Locktime
|
||||||
|
boardingExitDelay common.Locktime
|
||||||
|
|
||||||
nostrDefaultRelays []string
|
nostrDefaultRelays []string
|
||||||
|
|
||||||
@@ -57,7 +57,8 @@ type covenantService struct {
|
|||||||
|
|
||||||
func NewCovenantService(
|
func NewCovenantService(
|
||||||
network common.Network,
|
network common.Network,
|
||||||
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64,
|
roundInterval int64,
|
||||||
|
roundLifetime, unilateralExitDelay, boardingExitDelay common.Locktime,
|
||||||
nostrDefaultRelays []string,
|
nostrDefaultRelays []string,
|
||||||
walletSvc ports.WalletService, repoManager ports.RepoManager,
|
walletSvc ports.WalletService, repoManager ports.RepoManager,
|
||||||
builder ports.TxBuilder, scanner ports.BlockchainScanner,
|
builder ports.TxBuilder, scanner ports.BlockchainScanner,
|
||||||
@@ -162,7 +163,7 @@ func (s *covenantService) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, []string, error) {
|
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, []string, error) {
|
||||||
vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, uint(s.boardingExitDelay))
|
vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, s.boardingExitDelay)
|
||||||
|
|
||||||
tapKey, _, err := vtxoScript.TapTree()
|
tapKey, _, err := vtxoScript.TapTree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -235,7 +236,7 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the exit path is available, forbid registering the boarding utxo
|
// if the exit path is available, forbid registering the boarding utxo
|
||||||
if blocktime+int64(exitDelay) < now {
|
if blocktime+exitDelay.Seconds() < now {
|
||||||
return "", fmt.Errorf("tx %s expired", input.Txid)
|
return "", fmt.Errorf("tx %s expired", input.Txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +333,7 @@ func (s *covenantService) newBoardingInput(tx *transaction.Transaction, input po
|
|||||||
return nil, fmt.Errorf("descriptor does not match script in transaction output")
|
return nil, fmt.Errorf("descriptor does not match script in transaction output")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
|
if err := boardingScript.Validate(s.pubkey, s.unilateralExitDelay); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,8 +458,8 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
|
|||||||
|
|
||||||
return &ServiceInfo{
|
return &ServiceInfo{
|
||||||
PubKey: pubkey,
|
PubKey: pubkey,
|
||||||
RoundLifetime: s.roundLifetime,
|
RoundLifetime: int64(s.roundLifetime.Value),
|
||||||
UnilateralExitDelay: s.unilateralExitDelay,
|
UnilateralExitDelay: int64(s.unilateralExitDelay.Value),
|
||||||
RoundInterval: s.roundInterval,
|
RoundInterval: s.roundInterval,
|
||||||
Network: s.network.Name,
|
Network: s.network.Name,
|
||||||
Dust: dust,
|
Dust: dust,
|
||||||
@@ -1003,7 +1004,7 @@ func (s *covenantService) scheduleSweepVtxosForRound(round *domain.Round) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expirationTime := s.sweeper.scheduler.AddNow(s.roundLifetime)
|
expirationTime := s.sweeper.scheduler.AddNow(int64(s.roundLifetime.Value))
|
||||||
|
|
||||||
if err := s.sweeper.schedule(
|
if err := s.sweeper.schedule(
|
||||||
expirationTime, round.Txid, round.VtxoTree,
|
expirationTime, round.Txid, round.VtxoTree,
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ const marketHourDelta = 5 * time.Minute
|
|||||||
type covenantlessService struct {
|
type covenantlessService struct {
|
||||||
network common.Network
|
network common.Network
|
||||||
pubkey *secp256k1.PublicKey
|
pubkey *secp256k1.PublicKey
|
||||||
roundLifetime int64
|
roundLifetime common.Locktime
|
||||||
roundInterval int64
|
roundInterval int64
|
||||||
unilateralExitDelay int64
|
unilateralExitDelay common.Locktime
|
||||||
boardingExitDelay int64
|
boardingExitDelay common.Locktime
|
||||||
|
|
||||||
nostrDefaultRelays []string
|
nostrDefaultRelays []string
|
||||||
|
|
||||||
@@ -61,7 +61,8 @@ type covenantlessService struct {
|
|||||||
|
|
||||||
func NewCovenantlessService(
|
func NewCovenantlessService(
|
||||||
network common.Network,
|
network common.Network,
|
||||||
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64,
|
roundInterval int64,
|
||||||
|
roundLifetime, unilateralExitDelay, boardingExitDelay common.Locktime,
|
||||||
nostrDefaultRelays []string,
|
nostrDefaultRelays []string,
|
||||||
walletSvc ports.WalletService, repoManager ports.RepoManager,
|
walletSvc ports.WalletService, repoManager ports.RepoManager,
|
||||||
builder ports.TxBuilder, scanner ports.BlockchainScanner,
|
builder ports.TxBuilder, scanner ports.BlockchainScanner,
|
||||||
@@ -303,6 +304,35 @@ func (s *covenantlessService) SubmitRedeemTx(
|
|||||||
return "", fmt.Errorf("witness utxo value mismatch")
|
return "", fmt.Errorf("witness utxo value mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verify forfeit closure script
|
||||||
|
closure, err := tree.DecodeClosure(tapscript.Script)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode forfeit closure: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := closure.(type) {
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
blocktimestamp, err := s.wallet.GetCurrentBlockTime(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get current block time: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Locktime.Type {
|
||||||
|
case common.LocktimeTypeBlock:
|
||||||
|
if c.Locktime.Value > blocktimestamp.Height {
|
||||||
|
return "", fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block height)", c.Locktime.Value, blocktimestamp.Height)
|
||||||
|
}
|
||||||
|
case common.LocktimeTypeSecond:
|
||||||
|
if c.Locktime.Value > uint32(blocktimestamp.Time) {
|
||||||
|
return "", fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block time)", c.Locktime.Value, blocktimestamp.Time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *tree.MultisigClosure:
|
||||||
|
// prevent failure in case of multisig closure
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid forfeit closure script")
|
||||||
|
}
|
||||||
|
|
||||||
ctrlBlock, err := txscript.ParseControlBlock(tapscript.ControlBlock)
|
ctrlBlock, err := txscript.ParseControlBlock(tapscript.ControlBlock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to parse control block: %s", err)
|
return "", fmt.Errorf("failed to parse control block: %s", err)
|
||||||
@@ -349,7 +379,7 @@ func (s *covenantlessService) SubmitRedeemTx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// recompute redeem tx
|
// recompute redeem tx
|
||||||
rebuiltRedeemTx, err := bitcointree.BuildRedeemTx(ins, outputs, fees)
|
rebuiltRedeemTx, err := bitcointree.BuildRedeemTx(ins, outputs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to rebuild redeem tx: %s", err)
|
return "", fmt.Errorf("failed to rebuild redeem tx: %s", err)
|
||||||
}
|
}
|
||||||
@@ -439,7 +469,7 @@ func (s *covenantlessService) SubmitRedeemTx(
|
|||||||
func (s *covenantlessService) GetBoardingAddress(
|
func (s *covenantlessService) GetBoardingAddress(
|
||||||
ctx context.Context, userPubkey *secp256k1.PublicKey,
|
ctx context.Context, userPubkey *secp256k1.PublicKey,
|
||||||
) (address string, scripts []string, err error) {
|
) (address string, scripts []string, err error) {
|
||||||
vtxoScript := bitcointree.NewDefaultVtxoScript(s.pubkey, userPubkey, uint(s.boardingExitDelay))
|
vtxoScript := bitcointree.NewDefaultVtxoScript(s.pubkey, userPubkey, s.boardingExitDelay)
|
||||||
|
|
||||||
tapKey, _, err := vtxoScript.TapTree()
|
tapKey, _, err := vtxoScript.TapTree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -546,7 +576,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the exit path is available, forbid registering the boarding utxo
|
// if the exit path is available, forbid registering the boarding utxo
|
||||||
if blocktime+int64(exitDelay) < now {
|
if blocktime+exitDelay.Seconds() < now {
|
||||||
return "", fmt.Errorf("tx %s expired", input.Txid)
|
return "", fmt.Errorf("tx %s expired", input.Txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +664,7 @@ func (s *covenantlessService) newBoardingInput(tx wire.MsgTx, input ports.Input)
|
|||||||
return nil, fmt.Errorf("descriptor does not match script in transaction output")
|
return nil, fmt.Errorf("descriptor does not match script in transaction output")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
|
if err := boardingScript.Validate(s.pubkey, s.unilateralExitDelay); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,8 +785,8 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
|
|||||||
|
|
||||||
return &ServiceInfo{
|
return &ServiceInfo{
|
||||||
PubKey: pubkey,
|
PubKey: pubkey,
|
||||||
RoundLifetime: s.roundLifetime,
|
RoundLifetime: int64(s.roundLifetime.Value),
|
||||||
UnilateralExitDelay: s.unilateralExitDelay,
|
UnilateralExitDelay: int64(s.unilateralExitDelay.Value),
|
||||||
RoundInterval: s.roundInterval,
|
RoundInterval: s.roundInterval,
|
||||||
Network: s.network.Name,
|
Network: s.network.Name,
|
||||||
Dust: dust,
|
Dust: dust,
|
||||||
@@ -1064,7 +1094,7 @@ func (s *covenantlessService) startFinalization() {
|
|||||||
|
|
||||||
sweepClosure := tree.CSVSigClosure{
|
sweepClosure := tree.CSVSigClosure{
|
||||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{s.pubkey}},
|
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{s.pubkey}},
|
||||||
Seconds: uint(s.roundLifetime),
|
Locktime: s.roundLifetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepScript, err := sweepClosure.Script()
|
sweepScript, err := sweepClosure.Script()
|
||||||
@@ -1561,7 +1591,7 @@ func (s *covenantlessService) scheduleSweepVtxosForRound(round *domain.Round) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expirationTimestamp := s.sweeper.scheduler.AddNow(s.roundLifetime)
|
expirationTimestamp := s.sweeper.scheduler.AddNow(int64(s.roundLifetime.Value))
|
||||||
|
|
||||||
if err := s.sweeper.schedule(expirationTimestamp, round.Txid, round.VtxoTree); err != nil {
|
if err := s.sweeper.schedule(expirationTimestamp, round.Txid, round.VtxoTree); err != nil {
|
||||||
log.WithError(err).Warn("failed to schedule sweep tx")
|
log.WithError(err).Warn("failed to schedule sweep tx")
|
||||||
|
|||||||
@@ -65,16 +65,17 @@ func (p OwnershipProof) validate(vtxo domain.Vtxo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decodeForfeitClosure(script []byte) ([]*secp256k1.PublicKey, error) {
|
func decodeForfeitClosure(script []byte) ([]*secp256k1.PublicKey, error) {
|
||||||
var forfeit tree.MultisigClosure
|
closure, err := tree.DecodeClosure(script)
|
||||||
|
|
||||||
valid, err := forfeit.Decode(script)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !valid {
|
switch c := closure.(type) {
|
||||||
return nil, fmt.Errorf("invalid forfeit closure script")
|
case *tree.MultisigClosure:
|
||||||
|
return c.PubKeys, nil
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
return c.PubKeys, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return forfeit.PubKeys, nil
|
return nil, fmt.Errorf("invalid forfeit closure script")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/note"
|
"github.com/ark-network/ark/common/note"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/ark-network/ark/server/internal/core/domain"
|
"github.com/ark-network/ark/server/internal/core/domain"
|
||||||
@@ -357,12 +358,12 @@ func findSweepableOutputs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lifetime int64
|
var lifetime *common.Locktime
|
||||||
lifetime, sweepInput, err = txbuilder.GetSweepInput(node)
|
lifetime, sweepInput, err = txbuilder.GetSweepInput(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
expirationTime = blocktimeCache[node.ParentTxid] + lifetime
|
expirationTime = blocktimeCache[node.ParentTxid] + int64(lifetime.Value)
|
||||||
} else {
|
} else {
|
||||||
// cache the blocktime for future use
|
// cache the blocktime for future use
|
||||||
if schedulerUnit == ports.BlockHeight {
|
if schedulerUnit == ports.BlockHeight {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ports
|
package ports
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/common/tree"
|
"github.com/ark-network/ark/common/tree"
|
||||||
"github.com/ark-network/ark/server/internal/core/domain"
|
"github.com/ark-network/ark/server/internal/core/domain"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
@@ -47,7 +48,7 @@ type TxBuilder interface {
|
|||||||
txs []string,
|
txs []string,
|
||||||
) (valid map[domain.VtxoKey][]string, err error)
|
) (valid map[domain.VtxoKey][]string, err error)
|
||||||
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
|
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
|
||||||
GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error)
|
GetSweepInput(node tree.Node) (lifetime *common.Locktime, sweepInput SweepInput, err error)
|
||||||
FinalizeAndExtract(tx string) (txhex string, err error)
|
FinalizeAndExtract(tx string) (txhex string, err error)
|
||||||
VerifyTapscriptPartialSigs(tx string) (valid bool, err error)
|
VerifyTapscriptPartialSigs(tx string) (valid bool, err error)
|
||||||
// FindLeaves returns all the leaves txs that are reachable from the given outpoint
|
// FindLeaves returns all the leaves txs that are reachable from the given outpoint
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type WalletService interface {
|
|||||||
GetTransaction(ctx context.Context, txid string) (string, error)
|
GetTransaction(ctx context.Context, txid string) (string, error)
|
||||||
SignMessage(ctx context.Context, message []byte) ([]byte, error)
|
SignMessage(ctx context.Context, message []byte) ([]byte, error)
|
||||||
VerifyMessageSignature(ctx context.Context, message, signature []byte) (bool, error)
|
VerifyMessageSignature(ctx context.Context, message, signature []byte) (bool, error)
|
||||||
|
GetCurrentBlockTime(ctx context.Context) (*BlockTimestamp, error)
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,3 +64,8 @@ type TxOutpoint interface {
|
|||||||
GetTxid() string
|
GetTxid() string
|
||||||
GetIndex() uint32
|
GetIndex() uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BlockTimestamp struct {
|
||||||
|
Height uint32
|
||||||
|
Time int64
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tipHeightEndpoit = "/blocks/tip/height"
|
const tipHeightEndpoint = "/blocks/tip/height"
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
tipURL string
|
tipURL string
|
||||||
@@ -25,7 +25,7 @@ func NewScheduler(esploraURL string) (ports.SchedulerService, error) {
|
|||||||
return nil, fmt.Errorf("esplora URL is required")
|
return nil, fmt.Errorf("esplora URL is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
tipURL, err := url.JoinPath(esploraURL, tipHeightEndpoit)
|
tipURL, err := url.JoinPath(esploraURL, tipHeightEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ import (
|
|||||||
type txBuilder struct {
|
type txBuilder struct {
|
||||||
wallet ports.WalletService
|
wallet ports.WalletService
|
||||||
net common.Network
|
net common.Network
|
||||||
roundLifetime int64 // in seconds
|
roundLifetime common.Locktime
|
||||||
boardingExitDelay int64 // in seconds
|
boardingExitDelay common.Locktime
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTxBuilder(
|
func NewTxBuilder(
|
||||||
wallet ports.WalletService,
|
wallet ports.WalletService,
|
||||||
net common.Network,
|
net common.Network,
|
||||||
roundLifetime int64,
|
roundLifetime common.Locktime,
|
||||||
boardingExitDelay int64,
|
boardingExitDelay common.Locktime,
|
||||||
) ports.TxBuilder {
|
) ports.TxBuilder {
|
||||||
return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
|
return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
|
||||||
}
|
}
|
||||||
@@ -158,6 +158,11 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
|
|||||||
|
|
||||||
validForfeitTxs := make(map[domain.VtxoKey][]string)
|
validForfeitTxs := make(map[domain.VtxoKey][]string)
|
||||||
|
|
||||||
|
blocktimestamp, err := b.wallet.GetCurrentBlockTime(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for vtxoKey, psets := range forfeitTxsPsets {
|
for vtxoKey, psets := range forfeitTxsPsets {
|
||||||
if len(psets) == 0 {
|
if len(psets) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -202,13 +207,36 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
|
|||||||
|
|
||||||
vtxoTapscript := firstForfeit.Inputs[1].TapLeafScript[0]
|
vtxoTapscript := firstForfeit.Inputs[1].TapLeafScript[0]
|
||||||
|
|
||||||
|
// verify the forfeit closure script
|
||||||
|
closure, err := tree.DecodeClosure(vtxoTapscript.Script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := closure.(type) {
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
switch c.Locktime.Type {
|
||||||
|
case common.LocktimeTypeBlock:
|
||||||
|
if c.Locktime.Value > blocktimestamp.Height {
|
||||||
|
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block height)", c.Locktime.Value, blocktimestamp.Height)
|
||||||
|
}
|
||||||
|
case common.LocktimeTypeSecond:
|
||||||
|
if c.Locktime.Value > uint32(blocktimestamp.Time) {
|
||||||
|
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block time)", c.Locktime.Value, blocktimestamp.Time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *tree.MultisigClosure:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid forfeit closure script")
|
||||||
|
}
|
||||||
|
|
||||||
minFee, err := common.ComputeForfeitTxFee(
|
minFee, err := common.ComputeForfeitTxFee(
|
||||||
minRate,
|
minRate,
|
||||||
&waddrmgr.Tapscript{
|
&waddrmgr.Tapscript{
|
||||||
RevealedScript: vtxoTapscript.Script,
|
RevealedScript: vtxoTapscript.Script,
|
||||||
ControlBlock: &vtxoTapscript.ControlBlock.ControlBlock,
|
ControlBlock: &vtxoTapscript.ControlBlock.ControlBlock,
|
||||||
},
|
},
|
||||||
64*2,
|
closure.WitnessSize(),
|
||||||
txscript.GetScriptClass(forfeitScript),
|
txscript.GetScriptClass(forfeitScript),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -418,14 +446,14 @@ func (b *txBuilder) BuildRoundTx(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput ports.SweepInput, err error) {
|
func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime *common.Locktime, sweepInput ports.SweepInput, err error) {
|
||||||
pset, err := psetv2.NewPsetFromBase64(node.Tx)
|
pset, err := psetv2.NewPsetFromBase64(node.Tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(pset.Inputs) != 1 {
|
if len(pset.Inputs) != 1 {
|
||||||
return -1, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(pset.Inputs))
|
return nil, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(pset.Inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the tx is not onchain, it means that the input is an existing shared output
|
// if the tx is not onchain, it means that the input is an existing shared output
|
||||||
@@ -435,22 +463,22 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
|
|||||||
|
|
||||||
sweepLeaf, lifetime, err := extractSweepLeaf(input)
|
sweepLeaf, lifetime, err := extractSweepLeaf(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
txhex, err := b.wallet.GetTransaction(context.Background(), txid)
|
txhex, err := b.wallet.GetTransaction(context.Background(), txid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := transaction.NewTxFromHex(txhex)
|
tx, err := transaction.NewTxFromHex(txhex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value)
|
inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepInput = &sweepLiquidInput{
|
sweepInput = &sweepLiquidInput{
|
||||||
@@ -509,6 +537,10 @@ func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, error)
|
|||||||
for _, key := range c.PubKeys {
|
for _, key := range c.PubKeys {
|
||||||
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
|
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
|
||||||
}
|
}
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
for _, key := range c.PubKeys {
|
||||||
|
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't need to check if server signed
|
// we don't need to check if server signed
|
||||||
@@ -1123,24 +1155,24 @@ func (b *txBuilder) onchainNetwork() *network.Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
|
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime *common.Locktime, err error) {
|
||||||
for _, leaf := range input.TapLeafScript {
|
for _, leaf := range input.TapLeafScript {
|
||||||
closure := &tree.CSVSigClosure{}
|
closure := &tree.CSVSigClosure{}
|
||||||
valid, err := closure.Decode(leaf.Script)
|
valid, err := closure.Decode(leaf.Script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if valid && closure.Seconds > uint(lifetime) {
|
if valid && (lifetime == nil || lifetime.LessThan(closure.Locktime)) {
|
||||||
sweepLeaf = &leaf
|
sweepLeaf = &leaf
|
||||||
lifetime = int64(closure.Seconds)
|
lifetime = &closure.Locktime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sweepLeaf == nil {
|
if sweepLeaf == nil {
|
||||||
return nil, 0, fmt.Errorf("sweep leaf not found")
|
return nil, nil, fmt.Errorf("sweep leaf not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sweepLeaf, lifetime, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type sweepLiquidInput struct {
|
type sweepLiquidInput struct {
|
||||||
|
|||||||
@@ -22,14 +22,15 @@ const (
|
|||||||
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
||||||
forfeitAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
forfeitAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
||||||
minRelayFee = uint64(30)
|
minRelayFee = uint64(30)
|
||||||
roundLifetime = int64(1209344)
|
|
||||||
boardingExitDelay = int64(512)
|
|
||||||
minRelayFeeRate = 3
|
minRelayFeeRate = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wallet *mockedWallet
|
wallet *mockedWallet
|
||||||
pubkey *secp256k1.PublicKey
|
pubkey *secp256k1.PublicKey
|
||||||
|
|
||||||
|
roundLifetime = common.Locktime{Type: common.LocktimeTypeSecond, Value: 1209344}
|
||||||
|
boardingExitDelay = common.Locktime{Type: common.LocktimeTypeSecond, Value: 512}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
|||||||
@@ -303,6 +303,10 @@ func (m *mockedWallet) VerifyMessageSignature(ctx context.Context, message, sign
|
|||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
type mockedInput struct {
|
type mockedInput struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func sweepTransaction(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sequence, err := common.BIP68Sequence(sweepClosure.Seconds)
|
sequence, err := common.BIP68Sequence(sweepClosure.Locktime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import (
|
|||||||
type txBuilder struct {
|
type txBuilder struct {
|
||||||
wallet ports.WalletService
|
wallet ports.WalletService
|
||||||
net common.Network
|
net common.Network
|
||||||
roundLifetime int64 // in seconds
|
roundLifetime common.Locktime
|
||||||
boardingExitDelay int64 // in seconds
|
boardingExitDelay common.Locktime
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTxBuilder(
|
func NewTxBuilder(
|
||||||
wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay int64,
|
wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay common.Locktime,
|
||||||
) ports.TxBuilder {
|
) ports.TxBuilder {
|
||||||
return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
|
return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
|
||||||
}
|
}
|
||||||
@@ -91,6 +91,10 @@ func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, error) {
|
|||||||
for _, key := range c.PubKeys {
|
for _, key := range c.PubKeys {
|
||||||
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
|
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
|
||||||
}
|
}
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
for _, key := range c.PubKeys {
|
||||||
|
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't need to check if server signed
|
// we don't need to check if server signed
|
||||||
@@ -326,6 +330,11 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
|
|||||||
|
|
||||||
validForfeitTxs := make(map[domain.VtxoKey][]string)
|
validForfeitTxs := make(map[domain.VtxoKey][]string)
|
||||||
|
|
||||||
|
blocktimestamp, err := b.wallet.GetCurrentBlockTime(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for vtxoKey, ptxs := range forfeitTxsPtxs {
|
for vtxoKey, ptxs := range forfeitTxsPtxs {
|
||||||
if len(ptxs) == 0 {
|
if len(ptxs) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -359,6 +368,30 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
|
|||||||
}
|
}
|
||||||
|
|
||||||
vtxoTapscript := firstForfeit.Inputs[1].TaprootLeafScript[0]
|
vtxoTapscript := firstForfeit.Inputs[1].TaprootLeafScript[0]
|
||||||
|
|
||||||
|
// verify the forfeit closure script
|
||||||
|
closure, err := tree.DecodeClosure(vtxoTapscript.Script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := closure.(type) {
|
||||||
|
case *tree.CLTVMultisigClosure:
|
||||||
|
switch c.Locktime.Type {
|
||||||
|
case common.LocktimeTypeBlock:
|
||||||
|
if c.Locktime.Value > blocktimestamp.Height {
|
||||||
|
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block height)", c.Locktime.Value, blocktimestamp.Height)
|
||||||
|
}
|
||||||
|
case common.LocktimeTypeSecond:
|
||||||
|
if c.Locktime.Value > uint32(blocktimestamp.Time) {
|
||||||
|
return nil, fmt.Errorf("forfeit closure is CLTV locked, %d > %d (block time)", c.Locktime.Value, blocktimestamp.Time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *tree.MultisigClosure:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid forfeit closure script")
|
||||||
|
}
|
||||||
|
|
||||||
ctrlBlock, err := txscript.ParseControlBlock(vtxoTapscript.ControlBlock)
|
ctrlBlock, err := txscript.ParseControlBlock(vtxoTapscript.ControlBlock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -370,7 +403,7 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
|
|||||||
RevealedScript: vtxoTapscript.Script,
|
RevealedScript: vtxoTapscript.Script,
|
||||||
ControlBlock: ctrlBlock,
|
ControlBlock: ctrlBlock,
|
||||||
},
|
},
|
||||||
64*2,
|
closure.WitnessSize(),
|
||||||
txscript.GetScriptClass(forfeitScript),
|
txscript.GetScriptClass(forfeitScript),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -569,14 +602,14 @@ func (b *txBuilder) BuildRoundTx(
|
|||||||
return roundTx, vtxoTree, connectorAddress, connectors, nil
|
return roundTx, vtxoTree, connectorAddress, connectors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput ports.SweepInput, err error) {
|
func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime *common.Locktime, sweepInput ports.SweepInput, err error) {
|
||||||
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(partialTx.Inputs) != 1 {
|
if len(partialTx.Inputs) != 1 {
|
||||||
return -1, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(partialTx.Inputs))
|
return nil, nil, fmt.Errorf("invalid node pset, expect 1 input, got %d", len(partialTx.Inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
input := partialTx.UnsignedTx.TxIn[0]
|
input := partialTx.UnsignedTx.TxIn[0]
|
||||||
@@ -585,17 +618,17 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
|
|||||||
|
|
||||||
sweepLeaf, internalKey, lifetime, err := extractSweepLeaf(partialTx.Inputs[0])
|
sweepLeaf, internalKey, lifetime, err := extractSweepLeaf(partialTx.Inputs[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
txhex, err := b.wallet.GetTransaction(context.Background(), txid.String())
|
txhex, err := b.wallet.GetTransaction(context.Background(), txid.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var tx wire.MsgTx
|
var tx wire.MsgTx
|
||||||
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
|
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
|
||||||
return -1, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepInput = &sweepBitcoinInput{
|
sweepInput = &sweepBitcoinInput{
|
||||||
@@ -1180,27 +1213,27 @@ func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
|
|||||||
return outpoints
|
return outpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractSweepLeaf(input psbt.PInput) (sweepLeaf *psbt.TaprootTapLeafScript, internalKey *secp256k1.PublicKey, lifetime int64, err error) {
|
func extractSweepLeaf(input psbt.PInput) (sweepLeaf *psbt.TaprootTapLeafScript, internalKey *secp256k1.PublicKey, lifetime *common.Locktime, err error) {
|
||||||
for _, leaf := range input.TaprootLeafScript {
|
for _, leaf := range input.TaprootLeafScript {
|
||||||
closure := &tree.CSVSigClosure{}
|
closure := &tree.CSVSigClosure{}
|
||||||
valid, err := closure.Decode(leaf.Script)
|
valid, err := closure.Decode(leaf.Script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid && closure.Seconds > 0 {
|
if valid && (lifetime == nil || closure.Locktime.LessThan(*lifetime)) {
|
||||||
sweepLeaf = leaf
|
sweepLeaf = leaf
|
||||||
lifetime = int64(closure.Seconds)
|
lifetime = &closure.Locktime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internalKey, err = schnorr.ParsePubKey(input.TaprootInternalKey)
|
internalKey, err = schnorr.ParsePubKey(input.TaprootInternalKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if sweepLeaf == nil {
|
if sweepLeaf == nil {
|
||||||
return nil, nil, 0, fmt.Errorf("sweep leaf not found")
|
return nil, nil, nil, fmt.Errorf("sweep leaf not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sweepLeaf, internalKey, lifetime, nil
|
return sweepLeaf, internalKey, lifetime, nil
|
||||||
|
|||||||
@@ -22,14 +22,15 @@ const (
|
|||||||
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
||||||
forfeitAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
forfeitAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
||||||
changeAddress = "bcrt1qhhq55mut9easvrncy4se8q6vg3crlug7yj4j56"
|
changeAddress = "bcrt1qhhq55mut9easvrncy4se8q6vg3crlug7yj4j56"
|
||||||
roundLifetime = int64(1209344)
|
|
||||||
boardingExitDelay = int64(512)
|
|
||||||
minRelayFeeRate = 3
|
minRelayFeeRate = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wallet *mockedWallet
|
wallet *mockedWallet
|
||||||
pubkey *secp256k1.PublicKey
|
pubkey *secp256k1.PublicKey
|
||||||
|
|
||||||
|
roundLifetime = common.Locktime{Type: common.LocktimeTypeSecond, Value: 1209344}
|
||||||
|
boardingExitDelay = common.Locktime{Type: common.LocktimeTypeSecond, Value: 512}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
|||||||
@@ -284,6 +284,10 @@ func (m *mockedWallet) MainAccountBalance(ctx context.Context) (uint64, uint64,
|
|||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockedWallet) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
type mockedInput struct {
|
type mockedInput struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func sweepTransaction(
|
|||||||
return nil, fmt.Errorf("invalid csv script")
|
return nil, fmt.Errorf("invalid csv script")
|
||||||
}
|
}
|
||||||
|
|
||||||
sequence, err := common.BIP68Sequence(sweepClosure.Seconds)
|
sequence, err := common.BIP68Sequence(sweepClosure.Locktime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1119,6 +1119,23 @@ func (s *service) VerifyMessageSignature(ctx context.Context, message, signature
|
|||||||
return sig.Verify(message, s.serverKeyAddr.PubKey()), nil
|
return sig.Verify(message, s.serverKeyAddr.PubKey()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
|
||||||
|
blockhash, blockheight, err := s.wallet.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := s.wallet.GetBlockHeader(blockhash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ports.BlockTimestamp{
|
||||||
|
Time: header.Timestamp.Unix(),
|
||||||
|
Height: uint32(blockheight),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue {
|
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue {
|
||||||
vtxos := make(map[string][]ports.VtxoWithValue)
|
vtxos := make(map[string][]ports.VtxoWithValue)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package oceanwallet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
|
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
|
||||||
@@ -26,9 +30,18 @@ type service struct {
|
|||||||
chVtxos chan map[string][]ports.VtxoWithValue
|
chVtxos chan map[string][]ports.VtxoWithValue
|
||||||
isListening bool
|
isListening bool
|
||||||
syncedCh chan struct{}
|
syncedCh chan struct{}
|
||||||
|
esploraURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(addr string) (ports.WalletService, error) {
|
type blockInfo struct {
|
||||||
|
Height int64 `json:"height"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(addr string, esploraURL string) (ports.WalletService, error) {
|
||||||
|
if len(esploraURL) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing esplora url")
|
||||||
|
}
|
||||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -47,6 +60,7 @@ func NewService(addr string) (ports.WalletService, error) {
|
|||||||
notifyClient: notifyClient,
|
notifyClient: notifyClient,
|
||||||
chVtxos: chVtxos,
|
chVtxos: chVtxos,
|
||||||
syncedCh: make(chan struct{}),
|
syncedCh: make(chan struct{}),
|
||||||
|
esploraURL: esploraURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -168,6 +182,45 @@ func (s *service) VerifyMessageSignature(ctx context.Context, message, signature
|
|||||||
return false, errors.New("not implemented")
|
return false, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) GetCurrentBlockTime(ctx context.Context) (*ports.BlockTimestamp, error) {
|
||||||
|
tipHashURL, err := url.JoinPath(s.esploraURL, "blocks/tip/hash")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(tipHashURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
hash, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blockURL, err := url.JoinPath(s.esploraURL, "block", string(hash))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = http.Get(blockURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var blockInfo blockInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&blockInfo); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ports.BlockTimestamp{
|
||||||
|
Height: uint32(blockInfo.Height),
|
||||||
|
Time: blockInfo.Timestamp,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) listenToNotifications() {
|
func (s *service) listenToNotifications() {
|
||||||
s.isListening = true
|
s.isListening = true
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -3,22 +3,35 @@ package e2e_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ark-network/ark/common"
|
"github.com/ark-network/ark/common"
|
||||||
|
"github.com/ark-network/ark/common/bitcointree"
|
||||||
|
"github.com/ark-network/ark/common/tree"
|
||||||
arksdk "github.com/ark-network/ark/pkg/client-sdk"
|
arksdk "github.com/ark-network/ark/pkg/client-sdk"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
|
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/redemption"
|
"github.com/ark-network/ark/pkg/client-sdk/redemption"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||||
|
inmemorystoreconfig "github.com/ark-network/ark/pkg/client-sdk/store/inmemory"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/types"
|
"github.com/ark-network/ark/pkg/client-sdk/types"
|
||||||
|
singlekeywallet "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey"
|
||||||
|
inmemorystore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store/inmemory"
|
||||||
utils "github.com/ark-network/ark/server/test/e2e"
|
utils "github.com/ark-network/ark/server/test/e2e"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip04"
|
"github.com/nbd-wtf/go-nostr/nip04"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -411,6 +424,163 @@ func TestRedeemNotes(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendToCLTVMultisigClosure(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
alice, grpcAlice := setupArkSDK(t)
|
||||||
|
defer grpcAlice.Close()
|
||||||
|
|
||||||
|
bobPrivKey, err := secp256k1.GeneratePrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
configStore, err := inmemorystoreconfig.NewConfigStore()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
walletStore, err := inmemorystore.NewWalletStore()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bobWallet, err := singlekeywallet.NewBitcoinWallet(
|
||||||
|
configStore,
|
||||||
|
walletStore,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = bobWallet.Create(ctx, utils.Password, hex.EncodeToString(bobPrivKey.Serialize()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = bobWallet.Unlock(ctx, utils.Password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bobPubKey := bobPrivKey.PubKey()
|
||||||
|
|
||||||
|
// Fund Alice's account
|
||||||
|
offchainAddr, boardingAddress, err := alice.Receive(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
aliceAddr, err := common.DecodeAddress(offchainAddr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = utils.RunCommand("nigiri", "faucet", boardingAddress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
_, err = alice.Settle(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
const cltvBlocks = 10
|
||||||
|
const sendAmount = 10000
|
||||||
|
|
||||||
|
currentHeight, err := utils.GetBlockHeight(false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
vtxoScript := bitcointree.TapscriptsVtxoScript{
|
||||||
|
TapscriptsVtxoScript: tree.TapscriptsVtxoScript{
|
||||||
|
Closures: []tree.Closure{
|
||||||
|
&tree.CLTVMultisigClosure{
|
||||||
|
Locktime: common.Locktime{Type: common.LocktimeTypeBlock, Value: currentHeight + cltvBlocks},
|
||||||
|
MultisigClosure: tree.MultisigClosure{
|
||||||
|
PubKeys: []*secp256k1.PublicKey{bobPubKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
closure := vtxoScript.ForfeitClosures()[0]
|
||||||
|
|
||||||
|
bobAddr := common.Address{
|
||||||
|
HRP: "tark",
|
||||||
|
VtxoTapKey: vtxoTapKey,
|
||||||
|
Server: aliceAddr.Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := closure.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
merkleProof, err := vtxoTapTree.GetTaprootMerkleProof(txscript.NewBaseTapLeaf(script).TapHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctrlBlock, err := txscript.ParseControlBlock(merkleProof.ControlBlock)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tapscript := &waddrmgr.Tapscript{
|
||||||
|
ControlBlock: ctrlBlock,
|
||||||
|
RevealedScript: merkleProof.Script,
|
||||||
|
}
|
||||||
|
|
||||||
|
bobAddrStr, err := bobAddr.Encode()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
redeemTx, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(redeemTx), true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var bobOutput *wire.TxOut
|
||||||
|
var bobOutputIndex uint32
|
||||||
|
for i, out := range redeemPtx.UnsignedTx.TxOut {
|
||||||
|
if bytes.Equal(out.PkScript[2:], schnorr.SerializePubKey(bobAddr.VtxoTapKey)) {
|
||||||
|
bobOutput = out
|
||||||
|
bobOutputIndex = uint32(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, bobOutput)
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
alicePkScript, err := common.P2TRScript(aliceAddr.VtxoTapKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ptx, err := bitcointree.BuildRedeemTx(
|
||||||
|
[]common.VtxoInput{
|
||||||
|
{
|
||||||
|
Outpoint: &wire.OutPoint{
|
||||||
|
Hash: redeemPtx.UnsignedTx.TxHash(),
|
||||||
|
Index: bobOutputIndex,
|
||||||
|
},
|
||||||
|
Tapscript: tapscript,
|
||||||
|
WitnessSize: closure.WitnessSize(),
|
||||||
|
Amount: bobOutput.Value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: bobOutput.Value - 500,
|
||||||
|
PkScript: alicePkScript,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signedTx, err := bobWallet.SignTransaction(
|
||||||
|
ctx,
|
||||||
|
explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest),
|
||||||
|
ptx,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// should fail because the tx is not yet valid
|
||||||
|
_, err = grpcAlice.SubmitRedeemTx(ctx, signedTx)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Generate blocks to pass the timelock
|
||||||
|
for i := 0; i < cltvBlocks+1; i++ {
|
||||||
|
err = utils.GenerateBlock()
|
||||||
|
require.NoError(t, err)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = grpcAlice.SubmitRedeemTx(ctx, signedTx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSweep(t *testing.T) {
|
func TestSweep(t *testing.T) {
|
||||||
var receive utils.ArkReceive
|
var receive utils.ArkReceive
|
||||||
receiveStr, err := runClarkCommand("receive")
|
receiveStr, err := runClarkCommand("receive")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -48,6 +49,24 @@ func GenerateBlock() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetBlockHeight(isLiquid bool) (uint32, error) {
|
||||||
|
var out string
|
||||||
|
var err error
|
||||||
|
if isLiquid {
|
||||||
|
out, err = RunCommand("nigiri", "rpc", "--liquid", "getblockcount")
|
||||||
|
} else {
|
||||||
|
out, err = RunCommand("nigiri", "rpc", "getblockcount")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
height, err := strconv.ParseUint(strings.TrimSpace(out), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint32(height), nil
|
||||||
|
}
|
||||||
|
|
||||||
func RunDockerExec(container string, arg ...string) (string, error) {
|
func RunDockerExec(container string, arg ...string) (string, error) {
|
||||||
args := append([]string{"exec", "-t", container}, arg...)
|
args := append([]string{"exec", "-t", container}, arg...)
|
||||||
return RunCommand("docker", args...)
|
return RunCommand("docker", args...)
|
||||||
|
|||||||
Reference in New Issue
Block a user