Skip to content

Commit

Permalink
Add support for Out Of Round txs (#359)
Browse files Browse the repository at this point in the history
* [common] rework address encoding

* new address encoding

* replace offchain address by vtxo output key in DB

* merge migrations files into init one

* fix txbuilder fixtures

* fix transaction events

* OOR scheme

* fix conflicts

* [sdk] OOR

* update WASM wrappers

* revert renaming

* revert API changes

* update parser.go

* fix vtxosToTxsCovenantless

* add settled and spent in Utxo and Transaction

* Fixes (#5)

* Revert unneeded changes and rename claim to settle

* Revert changes to wasm and rename claim to settle

---------

Co-authored-by: Pietralberto Mazza <[email protected]>
  • Loading branch information
louisinger and altafan authored Oct 24, 2024
1 parent b536a9e commit bcb2b20
Show file tree
Hide file tree
Showing 41 changed files with 1,080 additions and 1,367 deletions.
2 changes: 1 addition & 1 deletion api-spec/openapi/swagger/ark/v1/service.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,7 @@
"swept": {
"type": "boolean"
},
"pending": {
"isOor": {
"type": "boolean"
},
"redeemTx": {
Expand Down
2 changes: 1 addition & 1 deletion api-spec/protobuf/ark/v1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ message Vtxo {
string spent_by = 4;
int64 expire_at = 5;
bool swept = 6;
bool pending = 7;
bool is_oor = 7;
string redeem_tx = 8;
uint64 amount = 9;
string pubkey = 10;
Expand Down
346 changes: 173 additions & 173 deletions api-spec/protobuf/gen/ark/v1/service.pb.go

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func main() {
&configCommand,
&dumpCommand,
&receiveCommand,
&claimCmd,
&settleCmd,
&sendCommand,
&balanceCommand,
&redeemCommand,
Expand Down Expand Up @@ -160,11 +160,11 @@ var (
return receive(ctx)
},
}
claimCmd = cli.Command{
Name: "claim",
Usage: "Claim onboarding funds or pending payments",
settleCmd = cli.Command{
Name: "settle",
Usage: "Settle onboarding funds or oor payments",
Action: func(ctx *cli.Context) error {
return claim(ctx)
return settle(ctx)
},
Flags: []cli.Flag{passwordFlag},
}
Expand Down Expand Up @@ -266,7 +266,7 @@ func receive(ctx *cli.Context) error {
})
}

func claim(ctx *cli.Context) error {
func settle(ctx *cli.Context) error {
password, err := readPassword(ctx)
if err != nil {
return err
Expand All @@ -275,7 +275,7 @@ func claim(ctx *cli.Context) error {
return err
}

txID, err := arkSdkClient.Claim(ctx.Context)
txID, err := arkSdkClient.Settle(ctx.Context)
if err != nil {
return err
}
Expand Down
107 changes: 8 additions & 99 deletions common/bitcointree/vtxo.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,17 @@ import (
type VtxoScript common.VtxoScript[bitcoinTapTree]

func ParseVtxoScript(desc string) (VtxoScript, error) {
v := &DefaultVtxoScript{}
// TODO add other type
err := v.FromDescriptor(desc)
if err != nil {
v := &ReversibleVtxoScript{}
err = v.FromDescriptor(desc)
if err != nil {
return nil, fmt.Errorf("invalid vtxo descriptor: %s", desc)
}
types := []VtxoScript{
&DefaultVtxoScript{},
}

return v, nil
for _, v := range types {
if err := v.FromDescriptor(desc); err == nil {
return v, nil
}
}

return v, nil
return nil, fmt.Errorf("invalid vtxo descriptor: %s", desc)
}

/*
Expand Down Expand Up @@ -101,94 +98,6 @@ func (v *DefaultVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, err
return taprootKey, bitcoinTapTree{tapTree}, nil
}

/*
* ReversibleVtxoScript allows sender of the VTXO to revert the transaction
* unilateral exit is in favor of the sender
* - Owner and ASP (forfeit owner)
* - Sender and ASP (forfeit sender)
* - Sender after t (unilateral exit)
*/
type ReversibleVtxoScript struct {
Asp *secp256k1.PublicKey
Sender *secp256k1.PublicKey
Owner *secp256k1.PublicKey
ExitDelay uint
}

func (v *ReversibleVtxoScript) ToDescriptor() string {
owner := hex.EncodeToString(schnorr.SerializePubKey(v.Owner))
sender := hex.EncodeToString(schnorr.SerializePubKey(v.Sender))
asp := hex.EncodeToString(schnorr.SerializePubKey(v.Asp))

return fmt.Sprintf(
descriptor.ReversibleVtxoScriptTemplate,
hex.EncodeToString(UnspendableKey().SerializeCompressed()),
sender,
asp,
v.ExitDelay,
sender,
owner,
asp,
)
}

func (v *ReversibleVtxoScript) FromDescriptor(desc string) error {
owner, sender, asp, exitDelay, err := descriptor.ParseReversibleVtxoDescriptor(desc)
if err != nil {
return err
}

v.Owner = owner
v.Sender = sender
v.Asp = asp
v.ExitDelay = exitDelay
return nil
}

func (v *ReversibleVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, error) {
redeemClosure := &CSVSigClosure{
Pubkey: v.Sender,
Seconds: v.ExitDelay,
}

redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}

forfeitClosure := &MultisigClosure{
Pubkey: v.Owner,
AspPubkey: v.Asp,
}

forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}

reverseForfeitClosure := &MultisigClosure{
Pubkey: v.Sender,
AspPubkey: v.Asp,
}

reverseForfeitLeaf, err := reverseForfeitClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}

tapTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf, *reverseForfeitLeaf,
)

root := tapTree.RootNode.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey(
UnspendableKey(),
root[:],
)

return taprootKey, bitcoinTapTree{tapTree}, nil
}

// bitcoinTapTree is a wrapper around txscript.IndexedTapScriptTree to implement the common.TaprootTree interface
type bitcoinTapTree struct {
*txscript.IndexedTapScriptTree
Expand Down
24 changes: 0 additions & 24 deletions common/bitcointree/vtxo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,8 @@ func TestParseDescriptor(t *testing.T) {
aliceKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)

bobKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)

aspPubKey := hex.EncodeToString(schnorr.SerializePubKey(aspKey.PubKey()))
alicePubKey := hex.EncodeToString(schnorr.SerializePubKey(aliceKey.PubKey()))
bobPubKey := hex.EncodeToString(schnorr.SerializePubKey(bobKey.PubKey()))

unspendableKey := hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed())

Expand All @@ -44,24 +40,4 @@ func TestParseDescriptor(t *testing.T) {
require.Equal(t, defaultScriptDescriptor, vtxo.ToDescriptor())
require.Equal(t, alicePubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Owner)))
require.Equal(t, aspPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Asp)))

reversibleScriptDescriptor := fmt.Sprintf(
descriptor.ReversibleVtxoScriptTemplate,
unspendableKey,
alicePubKey,
aspPubKey,
512,
alicePubKey,
bobPubKey,
aspPubKey,
)

vtxo, err = bitcointree.ParseVtxoScript(reversibleScriptDescriptor)
require.NoError(t, err)

require.IsType(t, &bitcointree.ReversibleVtxoScript{}, vtxo)
require.Equal(t, reversibleScriptDescriptor, vtxo.ToDescriptor())
require.Equal(t, alicePubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Sender)))
require.Equal(t, bobPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Owner)))
require.Equal(t, aspPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Asp)))
}
85 changes: 0 additions & 85 deletions common/descriptor/ark.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,91 +11,6 @@ import (
// tr(unspendable, { and(pk(user), pk(asp)), and(older(timeout), pk(user)) })
const DefaultVtxoDescriptorTemplate = "tr(%s,{ and(pk(%s), pk(%s)), and(older(%d), pk(%s)) })"

// tr(unspendable, { { and(pk(sender), pk(asp)), and(older(timeout), pk(sender)) }, and(pk(receiver), pk(asp)) })
const ReversibleVtxoScriptTemplate = "tr(%s,{ { and(pk(%s), pk(%s)), and(older(%d), pk(%s)) }, and(pk(%s), pk(%s)) })"

func ParseReversibleVtxoDescriptor(
descriptor string,
) (user, sender, asp *secp256k1.PublicKey, timeout uint, err error) {
desc, err := ParseTaprootDescriptor(descriptor)
if err != nil {
return nil, nil, nil, 0, err
}

if len(desc.ScriptTree) != 3 {
return nil, nil, nil, 0, errors.New("not a reversible vtxo script descriptor")
}

for _, leaf := range desc.ScriptTree {
if andLeaf, ok := leaf.(*And); ok {
if first, ok := andLeaf.First.(*PK); ok {
if second, ok := andLeaf.Second.(*PK); ok {
keyBytes, err := hex.DecodeString(first.Key.Hex)
if err != nil {
return nil, nil, nil, 0, err
}
if sender == nil {
sender, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
} else {
user, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
}

if asp == nil {
keyBytes, err = hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, nil, 0, err
}

asp, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
}
}
}

if first, ok := andLeaf.First.(*Older); ok {
if second, ok := andLeaf.Second.(*PK); ok {
timeout = first.Timeout
keyBytes, err := hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, nil, 0, err
}

sender, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
}
}
}
}

if user == nil {
return nil, nil, nil, 0, errors.New("descriptor is invalid")
}

if asp == nil {
return nil, nil, nil, 0, errors.New("descriptor is invalid")
}

if timeout == 0 {
return nil, nil, nil, 0, errors.New("descriptor is invalid")
}

if sender == nil {
return nil, nil, nil, 0, errors.New("descriptor is invalid")
}

return
}

func ParseDefaultVtxoDescriptor(
descriptor string,
) (user, asp *secp256k1.PublicKey, timeout uint, err error) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/client-sdk/ark_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type ArkClient interface {
ctx context.Context, addr string, amount uint64, withExpiryCoinselect bool,
) (string, error)
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error)
Claim(ctx context.Context) (string, error)
Settle(ctx context.Context) (string, error)
ListVtxos(ctx context.Context) (spendable, spent []client.Vtxo, err error)
Dump(ctx context.Context) (seed string, err error)
GetTransactionHistory(ctx context.Context) ([]types.Transaction, error)
Expand Down
33 changes: 33 additions & 0 deletions pkg/client-sdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,27 @@ func (a *arkClient) ping(
return ticker.Stop
}

func (a *arkClient) ListVtxos(
ctx context.Context,
) (spendableVtxos, spentVtxos []client.Vtxo, err error) {
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return
}

for _, addr := range offchainAddrs {
spendable, spent, err := a.client.ListVtxos(ctx, addr.Address)
if err != nil {
return nil, nil, err
}

spendableVtxos = append(spendableVtxos, spendable...)
spentVtxos = append(spentVtxos, spent...)
}

return
}

func getClient(
supportedClients utils.SupportedType[utils.ClientFactory], clientType, aspUrl string,
) (client.ASPClient, error) {
Expand Down Expand Up @@ -329,3 +350,15 @@ func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error)
func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time {
return expiry.Add(-time.Duration(roundLifetime) * time.Second)
}

func filterByOutpoints(vtxos []client.Vtxo, outpoints []client.Outpoint) []client.Vtxo {
filtered := make([]client.Vtxo, 0, len(vtxos))
for _, vtxo := range vtxos {
for _, outpoint := range outpoints {
if vtxo.Outpoint.Equals(outpoint) {
filtered = append(filtered, vtxo)
}
}
}
return filtered
}
6 changes: 5 additions & 1 deletion pkg/client-sdk/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ type Outpoint struct {
VOut uint32
}

func (o Outpoint) Equals(other Outpoint) bool {
return o.Txid == other.Txid && o.VOut == other.VOut
}

type Input struct {
Outpoint
Descriptor string
Expand All @@ -95,7 +99,7 @@ type Vtxo struct {
RoundTxid string
ExpiresAt *time.Time
RedeemTx string
Pending bool
IsOOR bool
SpentBy string
}

Expand Down
Loading

0 comments on commit bcb2b20

Please sign in to comment.