Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/client-lib/ark_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/arkade-os/arkd/pkg/ark-lib/asset"
"github.com/arkade-os/arkd/pkg/ark-lib/extension"
"github.com/arkade-os/arkd/pkg/ark-lib/script"
"github.com/arkade-os/arkd/pkg/client-lib/client"
"github.com/arkade-os/arkd/pkg/client-lib/explorer"
"github.com/arkade-os/arkd/pkg/client-lib/indexer"
Expand Down Expand Up @@ -36,6 +37,7 @@ type ArkClient interface {
Receive(
ctx context.Context,
) (onchainAddr string, offchainAddr, boardingAddr *types.Address, err error)
NewBoardingAddress(ctx context.Context, vtxoScript script.VtxoScript) (*types.Address, error)
GetAddresses(ctx context.Context) (
onchainAddresses, offchainAddresses, boardingAddresses, redemptionAddresses []string,
err error,
Expand Down
16 changes: 16 additions & 0 deletions pkg/client-lib/funding.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ import (
"github.com/arkade-os/arkd/pkg/client-lib/types"
)

func (a *service) NewBoardingAddress(
ctx context.Context, vtxoScript script.VtxoScript,
) (*types.Address, error) {
if err := a.safeCheck(); err != nil {
return nil, err
}

if err := vtxoScript.Validate(
a.SignerPubKey, a.BoardingExitDelay, false,
); err != nil {
return nil, fmt.Errorf("invalid boarding vtxo script: %w", err)
}

return a.wallet.NewBoardingAddress(ctx, vtxoScript)
}

func (a *service) Receive(ctx context.Context) (
onchainAddr string, offchainAddr, boardingAddr *types.Address, err error,
) {
Expand Down
18 changes: 6 additions & 12 deletions pkg/client-lib/unroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,19 +531,8 @@ func (a *service) getExpiredBoardingUtxos(
}

func (a *service) addInputs(
ctx context.Context, updater *psbt.Updater, utxos []types.Utxo,
_ context.Context, updater *psbt.Updater, utxos []types.Utxo,
) error {
// TODO works only with single-key wallet
_, offchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return err
}

vtxoScript, err := script.ParseVtxoScript(offchain.Tapscripts)
if err != nil {
return err
}

for _, utxo := range utxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil {
Expand All @@ -563,6 +552,11 @@ func (a *service) addInputs(
Sequence: sequence,
})

vtxoScript, err := script.ParseVtxoScript(utxo.Tapscripts)
if err != nil {
return fmt.Errorf("failed to parse vtxo script for %s:%d: %w", utxo.Txid, utxo.VOut, err)
}

exitClosures := vtxoScript.ExitClosures()
if len(exitClosures) <= 0 {
return fmt.Errorf("no exit closures found")
Expand Down
84 changes: 84 additions & 0 deletions pkg/client-lib/unroll_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package arksdk

import (
"context"
"testing"

arklib "github.com/arkade-os/arkd/pkg/ark-lib"
"github.com/arkade-os/arkd/pkg/ark-lib/script"
"github.com/arkade-os/arkd/pkg/client-lib/types"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/require"
)

func TestAddInputsPerUtxoScript(t *testing.T) {
signerKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
ownerKey1, err := btcec.NewPrivateKey()
require.NoError(t, err)
ownerKey2, err := btcec.NewPrivateKey()
require.NoError(t, err)

exitDelay := arklib.RelativeLocktime{Type: arklib.LocktimeTypeSecond, Value: 512}

vtxoScript1 := script.NewDefaultVtxoScript(ownerKey1.PubKey(), signerKey.PubKey(), exitDelay)
vtxoScript2 := script.NewDefaultVtxoScript(ownerKey2.PubKey(), signerKey.PubKey(), exitDelay)

tapscripts1, err := vtxoScript1.Encode()
require.NoError(t, err)
tapscripts2, err := vtxoScript2.Encode()
require.NoError(t, err)

// Scripts must differ because the owner keys differ.
require.NotEqual(t, tapscripts1, tapscripts2)

// Build a minimal PSBT with one output so the updater is valid.
tx := wire.NewMsgTx(2)
tx.AddTxOut(wire.NewTxOut(1000, []byte{0x51, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}))

pkt, err := psbt.New(nil, tx.TxOut, 2, 0, nil)
require.NoError(t, err)

updater, err := psbt.NewUpdater(pkt)
require.NoError(t, err)

utxos := []types.Utxo{
{
Outpoint: types.Outpoint{Txid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", VOut: 0},
Tapscripts: tapscripts1,
Delay: exitDelay,
},
{
Outpoint: types.Outpoint{Txid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", VOut: 1},
Tapscripts: tapscripts2,
Delay: exitDelay,
},
}

svc := &service{}
err = svc.addInputs(context.Background(), updater, utxos)
require.NoError(t, err)

// Each utxo should produce its own PSBT input.
require.Len(t, updater.Upsbt.Inputs, 2)

// Each input must have a taproot leaf script.
require.Len(t, updater.Upsbt.Inputs[0].TaprootLeafScript, 1)
require.Len(t, updater.Upsbt.Inputs[1].TaprootLeafScript, 1)

// The leaf scripts must differ because the owner keys are different.
script0 := updater.Upsbt.Inputs[0].TaprootLeafScript[0].Script
script1 := updater.Upsbt.Inputs[1].TaprootLeafScript[0].Script
require.NotEqual(t, script0, script1,
"each PSBT input must use its own utxo's tapscript, not a shared one")

// The control blocks must also differ (different internal keys).
cb0 := updater.Upsbt.Inputs[0].TaprootLeafScript[0].ControlBlock
cb1 := updater.Upsbt.Inputs[1].TaprootLeafScript[0].ControlBlock
require.NotEqual(t, cb0, cb1)
}
77 changes: 77 additions & 0 deletions pkg/client-lib/wallet/singlekey/bitcoin_wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ func (w *bitcoinWallet) GetAddresses(
},
}

// Append persisted custom boarding addresses
customDescriptors, err := w.walletStore.GetBoardingDescriptors()
if err != nil {
return nil, nil, nil, nil, err
}
for _, d := range customDescriptors {
boardingAddrs = append(boardingAddrs, types.Address{
Tapscripts: d.Tapscripts,
Address: d.Address,
})
}

onchainAddr, err := w.getP2TRAddress(ctx)
if err != nil {
return nil, nil, nil, nil, err
Expand Down Expand Up @@ -165,6 +177,71 @@ func (w *bitcoinWallet) NewAddresses(
return onchainAddrs, offchainAddrs, boardingAddrs, nil
}

func (w *bitcoinWallet) NewBoardingAddress(
ctx context.Context, vtxoScript script.VtxoScript,
) (*types.Address, error) {
if w.walletData == nil {
return nil, fmt.Errorf("wallet not initialized")
}

data, err := w.configStore.GetData(ctx)
if err != nil {
return nil, err
}
if data == nil {
return nil, fmt.Errorf("config store not initialized")
}

// Validate here (not just in the service layer) so that direct callers of
// WalletService.NewBoardingAddress cannot persist a script with an exit
// delay below the configured floor.
if err := vtxoScript.Validate(
data.SignerPubKey, data.BoardingExitDelay, false,
); err != nil {
return nil, fmt.Errorf("invalid boarding vtxo script: %w", err)
}

netParams := utils.ToBitcoinNetwork(data.Network)

tapKey, _, err := vtxoScript.TapTree()
if err != nil {
return nil, err
}

addr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(tapKey),
&netParams,
)
if err != nil {
return nil, err
}

tapscripts, err := vtxoScript.Encode()
if err != nil {
return nil, err
}

// Skip persisting if this matches the built-in default boarding address.
_, defaultBoarding, err := w.getArkAddresses(ctx)
if err != nil {
return nil, err
}
if addr.EncodeAddress() != defaultBoarding.Address {
descriptor := walletstore.BoardingDescriptor{
Address: addr.EncodeAddress(),
Tapscripts: tapscripts,
}
if err := w.walletStore.AddBoardingDescriptor(descriptor); err != nil {
return nil, err
}
}

return &types.Address{
Address: addr.EncodeAddress(),
Tapscripts: tapscripts,
}, nil
}

func (s *bitcoinWallet) SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (string, error) {
Expand Down
94 changes: 93 additions & 1 deletion pkg/client-lib/wallet/singlekey/store/file/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ type walletData struct {
PubKey string `json:"pubkey"`
}

type boardingDescriptor struct {
Address string `json:"address"`
Tapscripts []string `json:"tapscripts"`
}

func (d walletData) isEmpty() bool {
return d == walletData{}
}
Expand Down Expand Up @@ -99,6 +104,93 @@ func (s *fileStore) GetWallet() (*walletstore.WalletData, error) {
return &data, nil
}

func (s *fileStore) AddBoardingDescriptor(descriptor walletstore.BoardingDescriptor) error {
file, err := os.ReadFile(s.filePath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
}

currentData := map[string]any{}
if len(file) > 0 {
if err := json.Unmarshal(file, &currentData); err != nil {
return fmt.Errorf("failed to read file store: %s", err)
}
}

descriptors := make([]boardingDescriptor, 0)
if raw, ok := currentData["boarding_descriptors"]; ok {
buf, err := json.Marshal(raw)
if err != nil {
return err
}
if err := json.Unmarshal(buf, &descriptors); err != nil {
return err
}
}

for _, d := range descriptors {
if d.Address == descriptor.Address {
return nil
}
}

descriptors = append(descriptors, boardingDescriptor{
Address: descriptor.Address,
Tapscripts: descriptor.Tapscripts,
})

currentData["boarding_descriptors"] = descriptors

jsonString, err := json.Marshal(currentData)
if err != nil {
return err
}
return os.WriteFile(s.filePath, jsonString, 0600)
}

func (s *fileStore) GetBoardingDescriptors() ([]walletstore.BoardingDescriptor, error) {
file, err := os.ReadFile(s.filePath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}

currentData := map[string]any{}
if len(file) > 0 {
if err := json.Unmarshal(file, &currentData); err != nil {
return nil, fmt.Errorf("failed to read file store: %s", err)
}
}

raw, ok := currentData["boarding_descriptors"]
if !ok {
return nil, nil
}

buf, err := json.Marshal(raw)
if err != nil {
return nil, err
}

var descriptors []boardingDescriptor
if err := json.Unmarshal(buf, &descriptors); err != nil {
return nil, err
}

result := make([]walletstore.BoardingDescriptor, 0, len(descriptors))
for _, d := range descriptors {
result = append(result, walletstore.BoardingDescriptor{
Address: d.Address,
Tapscripts: d.Tapscripts,
})
}
return result, nil
}

func (s *fileStore) open() (*walletData, error) {
file, err := os.ReadFile(s.filePath)
if err != nil {
Expand Down Expand Up @@ -139,7 +231,7 @@ func (s *fileStore) write(data *walletData) error {
return err
}

err = os.WriteFile(s.filePath, jsonString, 0755)
err = os.WriteFile(s.filePath, jsonString, 0600)
if err != nil {
return err
}
Expand Down
Loading
Loading