Skip to content

Commit

Permalink
Support CLTV absolute locktime (#396)
Browse files Browse the repository at this point in the history
* fix CLTV with AbsoluteLocktime

* rename bip68.go file

* Update common/locktime.go

Co-authored-by: João Bordalo <[email protected]>
Signed-off-by: Louis Singer <[email protected]>

* rename test

* fix config_store.go

---------

Signed-off-by: Louis Singer <[email protected]>
Co-authored-by: João Bordalo <[email protected]>
  • Loading branch information
2 people authored and altafan committed Dec 11, 2024
1 parent b1906e1 commit 3d86094
Show file tree
Hide file tree
Showing 40 changed files with 520 additions and 211 deletions.
6 changes: 3 additions & 3 deletions common/bitcointree/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// CraftSharedOutput returns the taproot script and the amount of the initial root output
func CraftSharedOutput(
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime common.Locktime,
feeSatsPerNode uint64, roundLifetime common.RelativeLocktime,
) ([]byte, int64, error) {
aggregatedKey, _, err := createAggregatedKeyWithSweep(
cosigners, server, roundLifetime,
Expand All @@ -45,7 +45,7 @@ func CraftSharedOutput(
// BuildVtxoTree creates all the tree's transactions
func BuildVtxoTree(
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime common.Locktime,
feeSatsPerNode uint64, roundLifetime common.RelativeLocktime,
) (tree.VtxoTree, error) {
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
cosigners, server, roundLifetime,
Expand Down Expand Up @@ -280,7 +280,7 @@ func createRootNode(
}

func createAggregatedKeyWithSweep(
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime common.Locktime,
cosigners []*secp256k1.PublicKey, server *secp256k1.PublicKey, roundLifetime common.RelativeLocktime,
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
sweepClosure := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{server}},
Expand Down
17 changes: 14 additions & 3 deletions common/bitcointree/forfeit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ func BuildForfeitTxs(
connectorTx *psbt.Packet, vtxoInput *wire.OutPoint,
vtxoAmount, connectorAmount, feeAmount uint64,
vtxoScript, serverScript []byte,
txLocktime uint32,
) (forfeitTxs []*psbt.Packet, err error) {
version := int32(2)
locktime := uint32(0)
connectors, prevouts := getConnectorInputs(connectorTx, int64(connectorAmount))

for i, connectorInput := range connectors {
Expand All @@ -23,8 +23,19 @@ func BuildForfeitTxs(
Value: int64(vtxoAmount) + int64(connectorAmount) - int64(feeAmount),
PkScript: serverScript,
}}
sequences := []uint32{wire.MaxTxInSequenceNum, wire.MaxTxInSequenceNum}
partialTx, err := psbt.New(ins, outs, version, locktime, sequences)

vtxoSequence := wire.MaxTxInSequenceNum
if txLocktime != 0 {
vtxoSequence = wire.MaxTxInSequenceNum - 1
}

partialTx, err := psbt.New(
ins,
outs,
version,
txLocktime,
[]uint32{wire.MaxTxInSequenceNum, vtxoSequence},
)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion common/bitcointree/musig2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const (
exitDelay = 512
)

var lifetime = common.Locktime{Type: common.LocktimeTypeBlock, Value: 144}
var lifetime = common.RelativeLocktime{Type: common.LocktimeTypeBlock, Value: 144}

var testTxid, _ = chainhash.NewHashFromStr("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12")

Expand Down
46 changes: 40 additions & 6 deletions common/bitcointree/pending.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import (
"fmt"

"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
)

const (
// signal CLTV with input sequence number
cltvSequence = wire.MaxTxInSequenceNum - 1
)

func BuildRedeemTx(
vtxos []common.VtxoInput,
outputs []*wire.TxOut,
Expand All @@ -18,9 +24,12 @@ func BuildRedeemTx(
}

ins := make([]*wire.OutPoint, 0, len(vtxos))
sequences := make([]uint32, 0, len(vtxos))
witnessUtxos := make(map[int]*wire.TxOut)
tapscripts := make(map[int]*psbt.TaprootTapLeafScript)

txLocktime := common.AbsoluteLocktime(0)

for index, vtxo := range vtxos {
rootHash := vtxo.Tapscript.ControlBlock.RootHash(vtxo.Tapscript.RevealedScript)
taprootKey := txscript.ComputeTaprootOutputKey(UnspendableKey(), rootHash)
Expand All @@ -46,16 +55,41 @@ func BuildRedeemTx(
LeafVersion: txscript.BaseLeafVersion,
}

ins = append(ins, vtxo.Outpoint)
}
closure, err := tree.DecodeClosure(vtxo.Tapscript.RevealedScript)
if err != nil {
return "", err
}

// check if the closure is a CLTV multisig closure,
// if so, update the tx locktime
var locktime *common.AbsoluteLocktime
if cltv, ok := closure.(*tree.CLTVMultisigClosure); ok {
locktime = &cltv.Locktime
if locktime.IsSeconds() {
if txLocktime != 0 && !txLocktime.IsSeconds() {
return "", fmt.Errorf("mixed absolute locktime types")
}
} else {
if txLocktime != 0 && txLocktime.IsSeconds() {
return "", fmt.Errorf("mixed absolute locktime types")
}
}

if *locktime > txLocktime {
txLocktime = *locktime
}
}

sequences := make([]uint32, len(ins))
for i := range sequences {
sequences[i] = wire.MaxTxInSequenceNum
ins = append(ins, vtxo.Outpoint)
if locktime != nil {
sequences = append(sequences, cltvSequence)
} else {
sequences = append(sequences, wire.MaxTxInSequenceNum)
}
}

redeemPtx, err := psbt.New(
ins, outputs, 2, 0, sequences,
ins, outputs, 2, uint32(txLocktime), sequences,
)
if err != nil {
return "", err
Expand Down
2 changes: 1 addition & 1 deletion common/bitcointree/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func UnspendableKey() *secp256k1.PublicKey {
// - every control block and taproot output scripts
// - input and output amounts
func ValidateVtxoTree(
vtxoTree tree.VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey, roundLifetime common.Locktime,
vtxoTree tree.VtxoTree, roundTx string, serverPubkey *secp256k1.PublicKey, roundLifetime common.RelativeLocktime,
) error {
roundTransaction, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion common/bitcointree/vtxo.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func ParseVtxoScript(scripts []string) (VtxoScript, error) {
return nil, fmt.Errorf("invalid vtxo scripts: %s", scripts)
}

func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay common.Locktime) VtxoScript {
func NewDefaultVtxoScript(owner, server *secp256k1.PublicKey, exitDelay common.RelativeLocktime) VtxoScript {
base := tree.NewDefaultVtxoScript(owner, server, exitDelay)

return &TapscriptsVtxoScript{*base}
Expand Down
4 changes: 2 additions & 2 deletions common/descriptor/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (e *PK) Script(verify bool) (string, error) {
}

type Older struct {
Locktime common.Locktime
Locktime common.RelativeLocktime
}

func (e *Older) String() string {
Expand All @@ -124,7 +124,7 @@ func (e *Older) Parse(policy string) error {
return ErrInvalidOlderPolicy
}

e.Locktime = common.Locktime{Type: common.LocktimeTypeBlock, Value: uint32(timeout)}
e.Locktime = common.RelativeLocktime{Type: common.LocktimeTypeBlock, Value: uint32(timeout)}

return nil
}
Expand Down
24 changes: 12 additions & 12 deletions common/descriptor/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
},
},
Second: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 144},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 144},
},
},
},
Expand Down Expand Up @@ -92,7 +92,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
},
},
First: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 604672},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 604672},
},
},
},
Expand Down Expand Up @@ -157,7 +157,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
},
},
First: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 604672},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 604672},
},
},
&descriptor.And{
Expand Down Expand Up @@ -233,7 +233,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
},
&descriptor.And{
First: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 512},
},
Second: &descriptor.And{
First: &descriptor.PK{
Expand Down Expand Up @@ -279,7 +279,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
},
&descriptor.And{
First: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 1024},
},
Second: &descriptor.And{
First: &descriptor.PK{
Expand All @@ -300,7 +300,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
},
&descriptor.And{
First: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 512},
},
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
Expand All @@ -312,7 +312,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
},
&descriptor.And{
First: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 512},
},
Second: &descriptor.And{
First: &descriptor.PK{
Expand Down Expand Up @@ -394,7 +394,7 @@ func TestCompileDescriptor(t *testing.T) {
},
},
Second: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 1024},
},
},
},
Expand Down Expand Up @@ -465,14 +465,14 @@ func TestParseOlder(t *testing.T) {
policy: "older(512)",
expectedScript: "03010040b275",
expected: descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 512},
},
},
{
policy: "older(1024)",
expectedScript: "03020040b275",
expected: descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 1024},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 1024},
},
},
}
Expand Down Expand Up @@ -507,7 +507,7 @@ func TestParseAnd(t *testing.T) {
},
},
Second: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 512},
},
},
},
Expand All @@ -523,7 +523,7 @@ func TestParseAnd(t *testing.T) {
},
},
First: &descriptor.Older{
Locktime: common.Locktime{Type: common.LocktimeTypeSecond, Value: 512},
Locktime: common.RelativeLocktime{Type: common.LocktimeTypeSecond, Value: 512},
},
},
},
Expand Down
38 changes: 25 additions & 13 deletions common/bip68.go → common/locktime.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,42 @@ const (
SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31

SECONDS_PER_BLOCK = 10 * 60 // 10 minutes

// before this value, nLocktime is interpreted as blockheight
nLocktimeMinSeconds = 500_000_000
)

type LocktimeType uint
// RelativeLocktimeType represents a BIP68 relative locktime
// it is passed as argument to CheckSequenceVerify opcode
type RelativeLocktimeType uint

const (
LocktimeTypeSecond LocktimeType = iota
LocktimeTypeSecond RelativeLocktimeType = 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
// AbsoluteLocktime represents an nLocktime value
// it is used as argument to a CheckLocktimeVerify opcode
type AbsoluteLocktime uint32

func (l AbsoluteLocktime) IsSeconds() bool {
return l >= nLocktimeMinSeconds
}

// RelativeLocktime represents a BIP68 relative timelock value
type RelativeLocktime struct {
Type RelativeLocktimeType
Value uint32
}

func (l Locktime) Seconds() int64 {
func (l RelativeLocktime) Seconds() int64 {
if l.Type == LocktimeTypeBlock {
return int64(l.Value) * SECONDS_PER_BLOCK
}
return int64(l.Value)
}

func (l Locktime) Compare(other Locktime) int {
func (l RelativeLocktime) Compare(other RelativeLocktime) int {
val := l.Seconds()
otherVal := other.Seconds()

Expand All @@ -53,11 +65,11 @@ func (l Locktime) Compare(other Locktime) int {
}

// LessThan returns true if this locktime is less than the other locktime
func (l Locktime) LessThan(other Locktime) bool {
func (l RelativeLocktime) LessThan(other RelativeLocktime) bool {
return l.Compare(other) < 0
}

func BIP68Sequence(locktime Locktime) (uint32, error) {
func BIP68Sequence(locktime RelativeLocktime) (uint32, error) {
value := locktime.Value
isSeconds := locktime.Type == LocktimeTypeSecond
if isSeconds {
Expand All @@ -72,7 +84,7 @@ func BIP68Sequence(locktime Locktime) (uint32, error) {
return blockchain.LockTimeToSequence(isSeconds, value), nil
}

func BIP68DecodeSequence(sequence []byte) (*Locktime, error) {
func BIP68DecodeSequence(sequence []byte) (*RelativeLocktime, error) {
scriptNumber, err := txscript.MakeScriptNum(sequence, true, len(sequence))
if err != nil {
return nil, err
Expand All @@ -89,8 +101,8 @@ func BIP68DecodeSequence(sequence []byte) (*Locktime, error) {
}
if asNumber&SEQUENCE_LOCKTIME_TYPE_FLAG != 0 {
seconds := asNumber & SEQUENCE_LOCKTIME_MASK << SEQUENCE_LOCKTIME_GRANULARITY
return &Locktime{Type: LocktimeTypeSecond, Value: uint32(seconds)}, nil
return &RelativeLocktime{Type: LocktimeTypeSecond, Value: uint32(seconds)}, nil
}

return &Locktime{Type: LocktimeTypeBlock, Value: uint32(asNumber)}, nil
return &RelativeLocktime{Type: LocktimeTypeBlock, Value: uint32(asNumber)}, nil
}
Loading

0 comments on commit 3d86094

Please sign in to comment.