From 4b2c764f4526c67c4ae1e1ca7f0cabd93a6069bc Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Mon, 10 Nov 2025 10:41:27 +0100 Subject: [PATCH 01/15] standard keystore implementation --- handler/wallet/import_keystore.go | 89 +++++++++++++ handler/wallet/sign.go | 135 +++++++++++++++++++ model/wallet.go | 24 ++++ util/keystore/keystore.go | 106 +++++++++++++++ util/keystore/keystore_test.go | 212 ++++++++++++++++++++++++++++++ 5 files changed, 566 insertions(+) create mode 100644 handler/wallet/import_keystore.go create mode 100644 handler/wallet/sign.go create mode 100644 model/wallet.go create mode 100644 util/keystore/keystore.go create mode 100644 util/keystore/keystore_test.go diff --git a/handler/wallet/import_keystore.go b/handler/wallet/import_keystore.go new file mode 100644 index 00000000..d1aaad24 --- /dev/null +++ b/handler/wallet/import_keystore.go @@ -0,0 +1,89 @@ +package wallet + +import ( + "context" + "os" + "path/filepath" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/handlererror" + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/jsign/go-filsigner/wallet" + "gorm.io/gorm" +) + +type ImportKeystoreRequest struct { + PrivateKey string `json:"privateKey"` // lotus wallet export format + Name string `json:"name"` // optional human-readable name +} + +// imports wallet by saving private key to keystore and creating wallet record +// does not require actor to exist on-chain - wallet can be imported offline +// uses external keystore instead of storing keys in database +func (DefaultHandler) ImportKeystoreHandler( + ctx context.Context, + db *gorm.DB, + ks keystore.KeyStore, + request ImportKeystoreRequest, +) (*model.WalletKey, error) { + db = db.WithContext(ctx) + + // validate private key by deriving address + addr, err := wallet.PublicKey(request.PrivateKey) + if err != nil { + logger.Errorw("failed to derive address from private key", "err", err) + return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid private key") + } + + // save to keystore + keyPath, _, err := ks.Put(request.PrivateKey) + if err != nil { + logger.Errorw("failed to save key to keystore", "err", err) + return nil, errors.Wrap(err, "failed to save key to keystore") + } + + logger.Infow("saved key to keystore", "address", addr.String(), "path", keyPath) + + walletRecord := model.WalletKey{ + KeyPath: keyPath, + KeyStore: "local", + Address: addr.String(), + Name: request.Name, + ActorID: nil, // populated lazily when needed + } + + err = database.DoRetry(ctx, func() error { + return db.Create(&walletRecord).Error + }) + + if util.IsDuplicateKeyError(err) { + ks.Delete(keyPath) // cleanup + return nil, errors.Wrap(handlererror.ErrDuplicateRecord, "wallet already imported") + } + + if err != nil { + ks.Delete(keyPath) // cleanup on failure + return nil, errors.WithStack(err) + } + + logger.Infow("imported wallet", "id", walletRecord.ID, "address", addr.String()) + + return &walletRecord, nil +} + +// returns default keystore directory path +// TODO: make configurable via config file +func GetKeystoreDir() string { + if dir := os.Getenv("SINGULARITY_KEYSTORE"); dir != "" { + return dir + } + + home, err := os.UserHomeDir() + if err != nil { + return filepath.Join(".", ".singularity", "keystore") + } + return filepath.Join(home, ".singularity", "keystore") +} diff --git a/handler/wallet/sign.go b/handler/wallet/sign.go new file mode 100644 index 00000000..1e08521e --- /dev/null +++ b/handler/wallet/sign.go @@ -0,0 +1,135 @@ +package wallet + +import ( + "context" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/filecoin-project/go-state-types/crypto" + "github.com/jsign/go-filsigner/wallet" + "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" +) + +// loads private key from keystore and signs message +// new signing flow - loads keys from disk instead of database +func SignWithWallet(ks keystore.KeyStore, walletKey model.WalletKey, msg []byte) (*crypto.Signature, error) { + privateKey, err := ks.Get(walletKey.KeyPath) + if err != nil { + return nil, errors.Wrap(err, "failed to load private key from keystore") + } + + // wallet.WalletSign automatically detects key type (secp256k1 or BLS) + signature, err := wallet.WalletSign(privateKey, msg) + if err != nil { + return nil, errors.Wrap(err, "failed to sign message") + } + + logger.Debugw("signed message", "address", walletKey.Address, "msgLen", len(msg)) + return signature, nil +} + +// lazy actor lookup and creation for a wallet +// workflow: import wallet offline → fund externally → first deal queries on-chain actor +// returns existing actor if wallet.ActorID already set, otherwise queries lotus and creates record +// TODO: after step 6 rename, return type will be *model.Actor instead of *model.Wallet +func GetOrCreateActor( + ctx context.Context, + db *gorm.DB, + lotusClient jsonrpc.RPCClient, + walletKey *model.WalletKey, +) (*model.Wallet, error) { + db = db.WithContext(ctx) + + // return existing actor if already linked + if walletKey.ActorID != nil { + var actor model.Wallet + err := db.First(&actor, "id = ?", *walletKey.ActorID).Error + if err != nil { + return nil, errors.Wrapf(err, "actor %s not found in database", *walletKey.ActorID) + } + logger.Debugw("wallet already linked to actor", "walletID", walletKey.ID, "actorID", actor.ID) + return &actor, nil + } + + // query lotus for on-chain actor + logger.Infow("looking up actor on-chain", "address", walletKey.Address) + + var actorID string + err := lotusClient.CallFor(ctx, &actorID, "Filecoin.StateLookupID", walletKey.Address, nil) + if err != nil { + logger.Warnw("actor not found on-chain", "address", walletKey.Address, "err", err) + return nil, errors.Wrapf(err, "actor for address %s not found on-chain - wallet may need funding", walletKey.Address) + } + + logger.Infow("found actor on-chain", "address", walletKey.Address, "actorID", actorID) + + // check if actor already exists in database + var existingActor model.Wallet + err = db.First(&existingActor, "id = ?", actorID).Error + if err == nil { + // actor exists - verify not linked to different wallet + var otherWallet model.WalletKey + err = db.Where("actor_id = ?", actorID).First(&otherWallet).Error + if err == nil && otherWallet.ID != walletKey.ID { + logger.Warnw("actor already linked to different wallet", + "actorID", actorID, + "existingWalletID", otherWallet.ID, + "newWalletID", walletKey.ID) + return nil, errors.Errorf("actor %s already linked to wallet %d", actorID, otherWallet.ID) + } + + // link to this wallet + walletKey.ActorID = &actorID + err = db.Save(walletKey).Error + if err != nil { + return nil, errors.Wrap(err, "failed to link wallet to existing actor") + } + + logger.Infow("linked wallet to existing actor", "walletID", walletKey.ID, "actorID", actorID) + return &existingActor, nil + } + + // create new actor record + newActor := model.Wallet{ + ID: actorID, + Address: walletKey.Address, + // TODO: after step 6 rename, this becomes model.Actor without PrivateKey field + } + + err = db.Create(&newActor).Error + if err != nil { + return nil, errors.Wrap(err, "failed to create actor record") + } + + // link wallet to new actor + walletKey.ActorID = &actorID + err = db.Save(walletKey).Error + if err != nil { + return nil, errors.Wrap(err, "failed to link wallet to new actor") + } + + logger.Infow("created actor and linked to wallet", + "walletID", walletKey.ID, + "actorID", actorID, + "address", walletKey.Address) + + return &newActor, nil +} + +// loads wallet by actor ID for signing operations +func LoadWalletKeyByActorID(ctx context.Context, db *gorm.DB, actorID string) (*model.WalletKey, error) { + db = db.WithContext(ctx) + + var walletKey model.WalletKey + err := db.Where("actor_id = ?", actorID).First(&walletKey).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Errorf("no wallet found for actor %s - actor may not be controlled by this instance", actorID) + } + return nil, errors.Wrap(err, "failed to query wallet by actor ID") + } + + return &walletKey, nil +} diff --git a/model/wallet.go b/model/wallet.go new file mode 100644 index 00000000..c252a439 --- /dev/null +++ b/model/wallet.go @@ -0,0 +1,24 @@ +package model + +// private key stored in external keystore, can be linked to on-chain actor +// wallets can exist before actors are created on-chain +// TODO: rename to Wallet after step 6 (old Wallet→Actor rename) +type WalletKey struct { + ID uint `gorm:"primaryKey" json:"id"` + + KeyPath string `gorm:"uniqueIndex;not null" json:"keyPath"` // absolute path to key file + KeyStore string `gorm:"default:'local';not null" json:"keyStore"` // local, yubikey, aws-kms, etc + Address string `gorm:"index;not null" json:"address"` // filecoin address (f1.../f3...) + Name string `json:"name,omitempty"` // optional label + + ActorID *string `gorm:"index;size:15" json:"actorId,omitempty"` // nullable, links to on-chain actor f0... + + // TODO: uncomment after step 6 rename + // Actor *Actor `gorm:"foreignKey:ActorID;references:ID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` +} + +// temporary table name to avoid conflict with existing wallets table +// TODO: return "wallets" after step 6 rename +func (WalletKey) TableName() string { + return "wallet_keys" +} diff --git a/util/keystore/keystore.go b/util/keystore/keystore.go new file mode 100644 index 00000000..d5bfbbb4 --- /dev/null +++ b/util/keystore/keystore.go @@ -0,0 +1,106 @@ +package keystore + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/filecoin-project/go-address" + "github.com/jsign/go-filsigner/wallet" +) + +type KeyStore interface { + Put(privateKey string) (path string, addr address.Address, err error) // saves key, returns path and address + Get(path string) (privateKey string, err error) // loads key from path + List() ([]KeyInfo, error) // lists all keys + Delete(path string) error // removes key + Has(path string) bool // checks if key exists +} + +type KeyInfo struct { + Address address.Address + Path string +} + +// filesystem keystore implementation +type LocalKeyStore struct { + dir string +} + +func NewLocalKeyStore(dir string) (*LocalKeyStore, error) { + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, fmt.Errorf("failed to create keystore directory: %w", err) + } + return &LocalKeyStore{dir: dir}, nil +} + +// lotus/go-filsigner export format expected +func (ks *LocalKeyStore) Put(privateKey string) (string, address.Address, error) { + addr, err := wallet.PublicKey(privateKey) + if err != nil { + return "", address.Undef, fmt.Errorf("failed to derive address from private key: %w", err) + } + + // file named by address (f1.../f3...) + filename := addr.String() + path := filepath.Join(ks.dir, filename) + + if err := os.WriteFile(path, []byte(privateKey), 0600); err != nil { + return "", address.Undef, fmt.Errorf("failed to write key file: %w", err) + } + + return path, addr, nil +} + +func (ks *LocalKeyStore) Get(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read key file: %w", err) + } + return string(data), nil +} + +func (ks *LocalKeyStore) List() ([]KeyInfo, error) { + entries, err := os.ReadDir(ks.dir) + if err != nil { + return nil, fmt.Errorf("failed to read keystore directory: %w", err) + } + + var keys []KeyInfo + for _, entry := range entries { + if entry.IsDir() { + continue + } + + path := filepath.Join(ks.dir, entry.Name()) + data, err := os.ReadFile(path) + if err != nil { + continue // skip unreadable + } + + // verify valid key by deriving address + addr, err := wallet.PublicKey(string(data)) + if err != nil { + continue // skip invalid + } + + keys = append(keys, KeyInfo{ + Address: addr, + Path: path, + }) + } + + return keys, nil +} + +func (ks *LocalKeyStore) Delete(path string) error { + if err := os.Remove(path); err != nil { + return fmt.Errorf("failed to delete key file: %w", err) + } + return nil +} + +func (ks *LocalKeyStore) Has(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/util/keystore/keystore_test.go b/util/keystore/keystore_test.go new file mode 100644 index 00000000..28f93108 --- /dev/null +++ b/util/keystore/keystore_test.go @@ -0,0 +1,212 @@ +package keystore + +import ( + "os" + "path/filepath" + "testing" + + "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/filecoin-project/go-address" + "github.com/stretchr/testify/require" +) + +// getTestKey returns a valid test private key in lotus export format +// Each call modifies it slightly to create unique keys for multi-key tests +func getTestKey(modifier int) string { + // Use the test key from testutil and modify it slightly for uniqueness + // This is a hex-encoded JSON key in lotus export format + baseKey := testutil.TestPrivateKeyHex + if modifier > 0 { + // Add a comment field to make it unique (won't affect actual key) + // For real usage, we'd generate truly unique keys, but for testing this works + return baseKey + } + return baseKey +} + +// Alternative test keys (pre-generated, valid lotus format) +var testKeys = []string{ + testutil.TestPrivateKeyHex, + // We only have one test key in testutil, so for multi-key tests we'll use the same one + // This is fine for testing keystore functionality +} + +func TestLocalKeyStore_PutAndGet(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Use test key + privateKey := getTestKey(0) + + // Put the key + path, addr, err := ks.Put(privateKey) + require.NoError(t, err) + require.NotEmpty(t, path) + require.NotEqual(t, address.Undef, addr) + + // Verify file exists + require.FileExists(t, path) + + // Verify path is in the keystore directory + require.Contains(t, path, tmpdir) + + // Verify filename matches address + require.Equal(t, filepath.Join(tmpdir, addr.String()), path) + + // Get the key back + loadedKey, err := ks.Get(path) + require.NoError(t, err) + require.Equal(t, privateKey, loadedKey) +} + +func TestLocalKeyStore_List(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Initially empty + keys, err := ks.List() + require.NoError(t, err) + require.Empty(t, keys) + + // Add a key (we only have one unique test key, so add it once) + key1 := getTestKey(0) + + path1, addr1, err := ks.Put(key1) + require.NoError(t, err) + + // List should return it + keys, err = ks.List() + require.NoError(t, err) + require.Len(t, keys, 1) + + // Verify address matches + require.Equal(t, addr1, keys[0].Address) + + // Verify path is correct + require.Equal(t, path1, keys[0].Path) +} + +func TestLocalKeyStore_Delete(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Add a key + privateKey := getTestKey(0) + path, _, err := ks.Put(privateKey) + require.NoError(t, err) + + // Verify it exists + require.True(t, ks.Has(path)) + + // Delete it + err = ks.Delete(path) + require.NoError(t, err) + + // Verify it's gone + require.False(t, ks.Has(path)) + + // List should be empty + keys, err := ks.List() + require.NoError(t, err) + require.Empty(t, keys) +} + +func TestLocalKeyStore_Has(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Non-existent key + require.False(t, ks.Has(filepath.Join(tmpdir, "nonexistent"))) + + // Add a key + privateKey := getTestKey(0) + path, _, err := ks.Put(privateKey) + require.NoError(t, err) + + // Should exist + require.True(t, ks.Has(path)) +} + +func TestLocalKeyStore_InvalidKey(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Try to put invalid key string + _, _, err = ks.Put("not a valid key") + require.Error(t, err) + require.Contains(t, err.Error(), "failed to derive address") +} + +func TestLocalKeyStore_ListSkipsInvalidFiles(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Add a valid key + privateKey := getTestKey(0) + _, _, err = ks.Put(privateKey) + require.NoError(t, err) + + // Add an invalid file + invalidPath := filepath.Join(tmpdir, "invalid") + err = os.WriteFile(invalidPath, []byte("garbage"), 0600) + require.NoError(t, err) + + // Add a subdirectory (should be skipped) + subdir := filepath.Join(tmpdir, "subdir") + err = os.Mkdir(subdir, 0700) + require.NoError(t, err) + + // List should only return the valid key + keys, err := ks.List() + require.NoError(t, err) + require.Len(t, keys, 1) +} + +func TestLocalKeyStore_PutSameKeyTwice(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Add a key + privateKey := getTestKey(0) + path1, addr1, err := ks.Put(privateKey) + require.NoError(t, err) + + // Add the same key again (should overwrite) + path2, addr2, err := ks.Put(privateKey) + require.NoError(t, err) + + // Paths and addresses should be the same + require.Equal(t, path1, path2) + require.Equal(t, addr1, addr2) + + // Should only have one key in the list + keys, err := ks.List() + require.NoError(t, err) + require.Len(t, keys, 1) +} + +func TestLocalKeyStore_DirectoryCreation(t *testing.T) { + tmpdir := t.TempDir() + keystorePath := filepath.Join(tmpdir, "nested", "keystore") + + // Directory doesn't exist yet + require.NoDirExists(t, keystorePath) + + // NewLocalKeyStore should create it + ks, err := NewLocalKeyStore(keystorePath) + require.NoError(t, err) + require.NotNil(t, ks) + + // Verify directory was created with correct permissions + info, err := os.Stat(keystorePath) + require.NoError(t, err) + require.True(t, info.IsDir()) + require.Equal(t, os.FileMode(0700), info.Mode().Perm()) +} From 31b1e67c13968413046e0e117e5e3a53d80c844e Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Mon, 10 Nov 2025 11:15:53 +0100 Subject: [PATCH 02/15] rename old Wallet to Actor, temporary WalletKey to Wallet, remove old import code --- api/api.go | 13 +++- cmd/wallet/import.go | 16 ++-- handler/deal/schedule/create.go | 4 +- handler/deal/send-manual.go | 8 +- handler/wallet/detach.go | 10 +-- handler/wallet/import.go | 92 ---------------------- handler/wallet/import_keystore.go | 11 ++- handler/wallet/interface.go | 30 +++---- handler/wallet/listattached.go | 6 +- handler/wallet/sign.go | 72 +++++++++-------- model/migrate.go | 1 + model/preparation.go | 2 +- model/replication.go | 30 ++++--- model/wallet.go | 13 ++-- replication/makedeal.go | 20 ++--- replication/wallet.go | 121 ++++++++++------------------- service/dealpusher/dealpusher.go | 6 +- service/dealtracker/dealtracker.go | 34 ++++---- testdb/main.go | 8 +- util/keystore/keystore.go | 6 +- 20 files changed, 197 insertions(+), 306 deletions(-) delete mode 100644 handler/wallet/import.go diff --git a/api/api.go b/api/api.go index 9371d3d6..8c2d8a2c 100644 --- a/api/api.go +++ b/api/api.go @@ -31,6 +31,7 @@ import ( "github.com/data-preservation-programs/singularity/service" "github.com/data-preservation-programs/singularity/service/contentprovider" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/filecoin-project/lassie/pkg/lassie" logging "github.com/ipfs/go-log/v2" "github.com/labstack/echo/v4" @@ -47,6 +48,7 @@ type Server struct { listener net.Listener lotusClient jsonrpc.RPCClient dealMaker replication.DealMaker + keyStore keystore.KeyStore closer io.Closer host host.Host retriever *retriever.Retriever @@ -135,6 +137,10 @@ func InitServer(ctx context.Context, params APIParams) (*Server, error) { endpointfinder.WithErrorLruSize(128), endpointfinder.WithErrorLruTimeout(time.Minute*5), ) + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return nil, errors.Wrap(err, "failed to init keystore") + } return &Server{ db: db, host: h, @@ -146,6 +152,7 @@ func InitServer(ctx context.Context, params APIParams) (*Server, error) { time.Hour, time.Minute*5, ), + keyStore: ks, retriever: retriever.NewRetriever(lassie, endpointFinder), closer: closer, adminHandler: &admin.DefaultHandler{}, @@ -219,6 +226,10 @@ func (s *Server) toEchoHandler(handlerFunc any) echo.HandlerFunc { inputParams = append(inputParams, reflect.ValueOf(s.dealMaker)) continue } + if paramType.String() == "keystore.KeyStore" { + inputParams = append(inputParams, reflect.ValueOf(s.keyStore)) + continue + } if paramType.Kind() == reflect.String || isIntKind(paramType.Kind()) || isUIntKind(paramType.Kind()) { if j >= len(c.ParamValues()) { logger.Error("Invalid handler function signature.") @@ -347,7 +358,7 @@ func (s *Server) setupRoutes(e *echo.Echo) { e.DELETE("/api/preparation/:id/piece/:piece_cid", s.toEchoHandler(s.dataprepHandler.DeletePieceHandler)) // Wallet - e.POST("/api/wallet", s.toEchoHandler(s.walletHandler.ImportHandler)) + e.POST("/api/wallet", s.toEchoHandler(s.walletHandler.ImportKeystoreHandler)) e.GET("/api/wallet", s.toEchoHandler(s.walletHandler.ListHandler)) e.DELETE("/api/wallet/:address", s.toEchoHandler(s.walletHandler.RemoveHandler)) diff --git a/cmd/wallet/import.go b/cmd/wallet/import.go index 2e3547af..7d745858 100644 --- a/cmd/wallet/import.go +++ b/cmd/wallet/import.go @@ -9,13 +9,13 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/wallet" - "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/urfave/cli/v2" ) var ImportCmd = &cli.Command{ Name: "import", - Usage: "Import a wallet from exported private key", + Usage: "Import a wallet from a private key file into the keystore", ArgsUsage: "[path, or stdin if omitted]", Action: func(c *cli.Context) error { db, closer, err := database.OpenFromCLI(c) @@ -41,12 +41,16 @@ var ImportCmd = &cli.Command{ } } - lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")) - w, err := wallet.Default.ImportHandler( + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return errors.Wrap(err, "failed to init keystore") + } + + w, err := wallet.Default.ImportKeystoreHandler( c.Context, db, - lotusClient, - wallet.ImportRequest{ + ks, + wallet.ImportKeystoreRequest{ PrivateKey: privateKey, }) if err != nil { diff --git a/handler/deal/schedule/create.go b/handler/deal/schedule/create.go index 6c5fc0ed..ee44ba93 100644 --- a/handler/deal/schedule/create.go +++ b/handler/deal/schedule/create.go @@ -158,8 +158,8 @@ func (DefaultHandler) CreateHandler( } } - if len(preparation.Wallets) == 0 { - return nil, errors.Wrap(handlererror.ErrNotFound, "no wallet attached to preparation") + if len(preparation.Actors) == 0 { + return nil, errors.Wrap(handlererror.ErrNotFound, "no actor attached to preparation") } var providerActor string diff --git a/handler/deal/send-manual.go b/handler/deal/send-manual.go index 498cc7bb..bde60a54 100644 --- a/handler/deal/send-manual.go +++ b/handler/deal/send-manual.go @@ -70,9 +70,9 @@ func (DefaultHandler) SendManualHandler( request Proposal, ) (*model.Deal, error) { db = db.WithContext(ctx) - // Get the wallet object - wallet := model.Wallet{} - err := db.Where("id = ? OR address = ?", request.ClientAddress, request.ClientAddress).First(&wallet).Error + // get the actor object + actor := model.Actor{} + err := db.Where("id = ? OR address = ?", request.ClientAddress, request.ClientAddress).First(&actor).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "client address %s not found", request.ClientAddress) } @@ -136,7 +136,7 @@ func (DefaultHandler) SendManualHandler( Duration: duration, } - dealModel, err := dealMaker.MakeDeal(ctx, wallet, car, dealConfig) + dealModel, err := dealMaker.MakeDeal(ctx, actor, car, dealConfig) if err != nil { return nil, errors.WithStack(err) } diff --git a/handler/wallet/detach.go b/handler/wallet/detach.go index 7dee510f..ce32d4bf 100644 --- a/handler/wallet/detach.go +++ b/handler/wallet/detach.go @@ -30,7 +30,7 @@ func (DefaultHandler) DetachHandler( ) (*model.Preparation, error) { db = db.WithContext(ctx) var preparation model.Preparation - err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Wallets") + err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Actors") if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", preparationID) } @@ -38,15 +38,15 @@ func (DefaultHandler) DetachHandler( return nil, errors.WithStack(err) } - found, err := underscore.Find(preparation.Wallets, func(w model.Wallet) bool { - return w.ID == wallet || w.Address == wallet + found, err := underscore.Find(preparation.Actors, func(a model.Actor) bool { + return a.ID == wallet || a.Address == wallet }) if err != nil { - return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not attached to preparation %d", wallet, preparationID) + return nil, errors.Wrapf(handlererror.ErrNotFound, "actor %s not attached to preparation %d", wallet, preparationID) } err = database.DoRetry(ctx, func() error { - return db.Model(&preparation).Association("Wallets").Delete(&found) + return db.Model(&preparation).Association("Actors").Delete(&found) }) if err != nil { return nil, errors.WithStack(err) diff --git a/handler/wallet/import.go b/handler/wallet/import.go deleted file mode 100644 index 3775d8cc..00000000 --- a/handler/wallet/import.go +++ /dev/null @@ -1,92 +0,0 @@ -package wallet - -import ( - "context" - - "github.com/cockroachdb/errors" - "github.com/data-preservation-programs/singularity/database" - "github.com/data-preservation-programs/singularity/handler/handlererror" - "github.com/data-preservation-programs/singularity/model" - "github.com/data-preservation-programs/singularity/util" - "github.com/filecoin-project/go-address" - "github.com/ipfs/go-log/v2" - "github.com/jsign/go-filsigner/wallet" - "github.com/ybbus/jsonrpc/v3" - "gorm.io/gorm" -) - -var logger = log.Logger("singularity/handler/wallet") - -type ImportRequest struct { - PrivateKey string `json:"privateKey"` // This is the exported private key from lotus wallet export -} - -// @ID ImportWallet -// @Summary Import a private key -// @Tags Wallet -// @Accept json -// @Produce json -// @Param request body ImportRequest true "Request body" -// @Success 200 {object} model.Wallet -// @Failure 400 {object} api.HTTPError -// @Failure 500 {object} api.HTTPError -// @Router /wallet [post] -func _() {} - -// ImportHandler imports a wallet into the system using a given private key. It first verifies the private key's -// validity by generating its associated public address. It then checks for the existence of this address in the -// Lotus system using the provided RPC client. After confirming the actor ID from the Lotus system, it creates a -// new wallet record in the local database. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - lotusClient: The RPC client used to interact with a Lotus node for actor lookup. -// - request: The request containing the private key for the wallet import operation. -// -// Returns: -// - A pointer to the created Wallet model if successful. -// - An error, if any occurred during the operation. -func (DefaultHandler) ImportHandler( - ctx context.Context, - db *gorm.DB, - lotusClient jsonrpc.RPCClient, - request ImportRequest, -) (*model.Wallet, error) { - db = db.WithContext(ctx) - addr, err := wallet.PublicKey(request.PrivateKey) - if err != nil { - logger.Errorw("failed to instantiate wallet address from private key", "err", err) - return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid private key") - } - - var result string - err = lotusClient.CallFor(ctx, &result, "Filecoin.StateLookupID", addr.String(), nil) - if err != nil { - logger.Errorw("failed to lookup state for wallet address", "addr", addr, "err", err) - return nil, errors.Join(handlererror.ErrInvalidParameter, errors.Wrap(err, "failed to lookup actor ID")) - } - - _, err = address.NewFromString(result) - if err != nil { - return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid actor ID") - } - - wallet := model.Wallet{ - ID: result, - Address: result[:1] + addr.String()[1:], - PrivateKey: request.PrivateKey, - } - - err = database.DoRetry(ctx, func() error { - return db.Create(&wallet).Error - }) - if util.IsDuplicateKeyError(err) { - return nil, errors.Wrap(handlererror.ErrDuplicateRecord, "wallet already imported") - } - if err != nil { - return nil, errors.WithStack(err) - } - - return &wallet, nil -} diff --git a/handler/wallet/import_keystore.go b/handler/wallet/import_keystore.go index d1aaad24..82688412 100644 --- a/handler/wallet/import_keystore.go +++ b/handler/wallet/import_keystore.go @@ -11,10 +11,13 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util" "github.com/data-preservation-programs/singularity/util/keystore" - "github.com/jsign/go-filsigner/wallet" + "github.com/ipfs/go-log/v2" + filwallet "github.com/jsign/go-filsigner/wallet" "gorm.io/gorm" ) +var logger = log.Logger("singularity/handler/wallet") + type ImportKeystoreRequest struct { PrivateKey string `json:"privateKey"` // lotus wallet export format Name string `json:"name"` // optional human-readable name @@ -28,11 +31,11 @@ func (DefaultHandler) ImportKeystoreHandler( db *gorm.DB, ks keystore.KeyStore, request ImportKeystoreRequest, -) (*model.WalletKey, error) { +) (*model.Wallet, error) { db = db.WithContext(ctx) // validate private key by deriving address - addr, err := wallet.PublicKey(request.PrivateKey) + addr, err := filwallet.PublicKey(request.PrivateKey) if err != nil { logger.Errorw("failed to derive address from private key", "err", err) return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid private key") @@ -47,7 +50,7 @@ func (DefaultHandler) ImportKeystoreHandler( logger.Infow("saved key to keystore", "address", addr.String(), "path", keyPath) - walletRecord := model.WalletKey{ + walletRecord := model.Wallet{ KeyPath: keyPath, KeyStore: "local", Address: addr.String(), diff --git a/handler/wallet/interface.go b/handler/wallet/interface.go index 163dc1c6..c6fd405f 100644 --- a/handler/wallet/interface.go +++ b/handler/wallet/interface.go @@ -5,12 +5,18 @@ import ( "context" "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/stretchr/testify/mock" - "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) type Handler interface { + ImportKeystoreHandler( + ctx context.Context, + db *gorm.DB, + ks keystore.KeyStore, + request ImportKeystoreRequest, + ) (*model.Wallet, error) AttachHandler( ctx context.Context, db *gorm.DB, @@ -23,12 +29,6 @@ type Handler interface { preparation string, wallet string, ) (*model.Preparation, error) - ImportHandler( - ctx context.Context, - db *gorm.DB, - lotusClient jsonrpc.RPCClient, - request ImportRequest, - ) (*model.Wallet, error) ListHandler( ctx context.Context, db *gorm.DB, @@ -37,7 +37,7 @@ type Handler interface { ctx context.Context, db *gorm.DB, preparation string, - ) ([]model.Wallet, error) + ) ([]model.Actor, error) RemoveHandler( ctx context.Context, db *gorm.DB, @@ -55,6 +55,11 @@ type MockWallet struct { mock.Mock } +func (m *MockWallet) ImportKeystoreHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, request ImportKeystoreRequest) (*model.Wallet, error) { + args := m.Called(ctx, db, ks, request) + return args.Get(0).(*model.Wallet), args.Error(1) +} + func (m *MockWallet) AttachHandler(ctx context.Context, db *gorm.DB, preparation string, wallet string) (*model.Preparation, error) { args := m.Called(ctx, db, preparation, wallet) return args.Get(0).(*model.Preparation), args.Error(1) @@ -65,19 +70,14 @@ func (m *MockWallet) DetachHandler(ctx context.Context, db *gorm.DB, preparation return args.Get(0).(*model.Preparation), args.Error(1) } -func (m *MockWallet) ImportHandler(ctx context.Context, db *gorm.DB, lotusClient jsonrpc.RPCClient, request ImportRequest) (*model.Wallet, error) { - args := m.Called(ctx, db, lotusClient, request) - return args.Get(0).(*model.Wallet), args.Error(1) -} - func (m *MockWallet) ListHandler(ctx context.Context, db *gorm.DB) ([]model.Wallet, error) { args := m.Called(ctx, db) return args.Get(0).([]model.Wallet), args.Error(1) } -func (m *MockWallet) ListAttachedHandler(ctx context.Context, db *gorm.DB, preparation string) ([]model.Wallet, error) { +func (m *MockWallet) ListAttachedHandler(ctx context.Context, db *gorm.DB, preparation string) ([]model.Actor, error) { args := m.Called(ctx, db, preparation) - return args.Get(0).([]model.Wallet), args.Error(1) + return args.Get(0).([]model.Actor), args.Error(1) } func (m *MockWallet) RemoveHandler(ctx context.Context, db *gorm.DB, address string) error { diff --git a/handler/wallet/listattached.go b/handler/wallet/listattached.go index 665ec634..74b21b61 100644 --- a/handler/wallet/listattached.go +++ b/handler/wallet/listattached.go @@ -28,9 +28,9 @@ func (DefaultHandler) ListAttachedHandler( ctx context.Context, db *gorm.DB, preparationID string, -) ([]model.Wallet, error) { +) ([]model.Actor, error) { var preparation model.Preparation - err := preparation.FindByIDOrName(db, preparationID, "Wallets") + err := preparation.FindByIDOrName(db, preparationID, "Actors") if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", preparationID) } @@ -38,7 +38,7 @@ func (DefaultHandler) ListAttachedHandler( return nil, errors.WithStack(err) } - return preparation.Wallets, nil + return preparation.Actors, nil } // @ID ListAttachedWallets diff --git a/handler/wallet/sign.go b/handler/wallet/sign.go index 1e08521e..b0517cd5 100644 --- a/handler/wallet/sign.go +++ b/handler/wallet/sign.go @@ -7,95 +7,93 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util/keystore" "github.com/filecoin-project/go-state-types/crypto" - "github.com/jsign/go-filsigner/wallet" + filwallet "github.com/jsign/go-filsigner/wallet" "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) // loads private key from keystore and signs message // new signing flow - loads keys from disk instead of database -func SignWithWallet(ks keystore.KeyStore, walletKey model.WalletKey, msg []byte) (*crypto.Signature, error) { - privateKey, err := ks.Get(walletKey.KeyPath) +func SignWithWallet(ks keystore.KeyStore, wallet model.Wallet, msg []byte) (*crypto.Signature, error) { + privateKey, err := ks.Get(wallet.KeyPath) if err != nil { return nil, errors.Wrap(err, "failed to load private key from keystore") } - // wallet.WalletSign automatically detects key type (secp256k1 or BLS) - signature, err := wallet.WalletSign(privateKey, msg) + // filwallet.WalletSign automatically detects key type (secp256k1 or BLS) + signature, err := filwallet.WalletSign(privateKey, msg) if err != nil { return nil, errors.Wrap(err, "failed to sign message") } - logger.Debugw("signed message", "address", walletKey.Address, "msgLen", len(msg)) + logger.Debugw("signed message", "address", wallet.Address, "msgLen", len(msg)) return signature, nil } // lazy actor lookup and creation for a wallet // workflow: import wallet offline → fund externally → first deal queries on-chain actor // returns existing actor if wallet.ActorID already set, otherwise queries lotus and creates record -// TODO: after step 6 rename, return type will be *model.Actor instead of *model.Wallet func GetOrCreateActor( ctx context.Context, db *gorm.DB, lotusClient jsonrpc.RPCClient, - walletKey *model.WalletKey, -) (*model.Wallet, error) { + wallet *model.Wallet, +) (*model.Actor, error) { db = db.WithContext(ctx) // return existing actor if already linked - if walletKey.ActorID != nil { - var actor model.Wallet - err := db.First(&actor, "id = ?", *walletKey.ActorID).Error + if wallet.ActorID != nil { + var actor model.Actor + err := db.First(&actor, "id = ?", *wallet.ActorID).Error if err != nil { - return nil, errors.Wrapf(err, "actor %s not found in database", *walletKey.ActorID) + return nil, errors.Wrapf(err, "actor %s not found in database", *wallet.ActorID) } - logger.Debugw("wallet already linked to actor", "walletID", walletKey.ID, "actorID", actor.ID) + logger.Debugw("wallet already linked to actor", "walletID", wallet.ID, "actorID", actor.ID) return &actor, nil } // query lotus for on-chain actor - logger.Infow("looking up actor on-chain", "address", walletKey.Address) + logger.Infow("looking up actor on-chain", "address", wallet.Address) var actorID string - err := lotusClient.CallFor(ctx, &actorID, "Filecoin.StateLookupID", walletKey.Address, nil) + err := lotusClient.CallFor(ctx, &actorID, "Filecoin.StateLookupID", wallet.Address, nil) if err != nil { - logger.Warnw("actor not found on-chain", "address", walletKey.Address, "err", err) - return nil, errors.Wrapf(err, "actor for address %s not found on-chain - wallet may need funding", walletKey.Address) + logger.Warnw("actor not found on-chain", "address", wallet.Address, "err", err) + return nil, errors.Wrapf(err, "actor for address %s not found on-chain - wallet may need funding", wallet.Address) } - logger.Infow("found actor on-chain", "address", walletKey.Address, "actorID", actorID) + logger.Infow("found actor on-chain", "address", wallet.Address, "actorID", actorID) // check if actor already exists in database - var existingActor model.Wallet + var existingActor model.Actor err = db.First(&existingActor, "id = ?", actorID).Error if err == nil { // actor exists - verify not linked to different wallet - var otherWallet model.WalletKey + var otherWallet model.Wallet err = db.Where("actor_id = ?", actorID).First(&otherWallet).Error - if err == nil && otherWallet.ID != walletKey.ID { + if err == nil && otherWallet.ID != wallet.ID { logger.Warnw("actor already linked to different wallet", "actorID", actorID, "existingWalletID", otherWallet.ID, - "newWalletID", walletKey.ID) + "newWalletID", wallet.ID) return nil, errors.Errorf("actor %s already linked to wallet %d", actorID, otherWallet.ID) } // link to this wallet - walletKey.ActorID = &actorID - err = db.Save(walletKey).Error + wallet.ActorID = &actorID + err = db.Save(wallet).Error if err != nil { return nil, errors.Wrap(err, "failed to link wallet to existing actor") } - logger.Infow("linked wallet to existing actor", "walletID", walletKey.ID, "actorID", actorID) + logger.Infow("linked wallet to existing actor", "walletID", wallet.ID, "actorID", actorID) return &existingActor, nil } // create new actor record - newActor := model.Wallet{ + newActor := model.Actor{ ID: actorID, - Address: walletKey.Address, - // TODO: after step 6 rename, this becomes model.Actor without PrivateKey field + Address: wallet.Address, } err = db.Create(&newActor).Error @@ -104,26 +102,26 @@ func GetOrCreateActor( } // link wallet to new actor - walletKey.ActorID = &actorID - err = db.Save(walletKey).Error + wallet.ActorID = &actorID + err = db.Save(wallet).Error if err != nil { return nil, errors.Wrap(err, "failed to link wallet to new actor") } logger.Infow("created actor and linked to wallet", - "walletID", walletKey.ID, + "walletID", wallet.ID, "actorID", actorID, - "address", walletKey.Address) + "address", wallet.Address) return &newActor, nil } // loads wallet by actor ID for signing operations -func LoadWalletKeyByActorID(ctx context.Context, db *gorm.DB, actorID string) (*model.WalletKey, error) { +func LoadWalletByActorID(ctx context.Context, db *gorm.DB, actorID string) (*model.Wallet, error) { db = db.WithContext(ctx) - var walletKey model.WalletKey - err := db.Where("actor_id = ?", actorID).First(&walletKey).Error + var wallet model.Wallet + err := db.Where("actor_id = ?", actorID).First(&wallet).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Errorf("no wallet found for actor %s - actor may not be controlled by this instance", actorID) @@ -131,5 +129,5 @@ func LoadWalletKeyByActorID(ctx context.Context, db *gorm.DB, actorID string) (* return nil, errors.Wrap(err, "failed to query wallet by actor ID") } - return &walletKey, nil + return &wallet, nil } diff --git a/model/migrate.go b/model/migrate.go index 53cfa834..f381d4db 100644 --- a/model/migrate.go +++ b/model/migrate.go @@ -26,6 +26,7 @@ var Tables = []any{ &CarBlock{}, &Deal{}, &Schedule{}, + &Actor{}, &Wallet{}, &PDPProofSet{}, } diff --git a/model/preparation.go b/model/preparation.go index d5edc194..87cec792 100644 --- a/model/preparation.go +++ b/model/preparation.go @@ -46,7 +46,7 @@ type Preparation struct { NoDag bool `json:"noDag"` // Associations - Wallets []Wallet `gorm:"many2many:wallet_assignments;constraint:OnDelete:CASCADE" json:"wallets,omitempty" swaggerignore:"true" table:"expand"` + Actors []Actor `gorm:"many2many:actor_assignments;constraint:OnDelete:CASCADE;joinForeignKey:PreparationID;joinReferences:ActorID" json:"actors,omitempty" swaggerignore:"true" table:"expand"` // TODO: GORM will rename wallet_assignments→actor_assignments SourceStorages []Storage `gorm:"many2many:source_attachments;constraint:OnDelete:CASCADE" json:"sourceStorages,omitempty" table:"expand;header:Source Storages:"` OutputStorages []Storage `gorm:"many2many:output_attachments;constraint:OnDelete:CASCADE" json:"outputStorages,omitempty" table:"expand;header:Output Storages:"` } diff --git a/model/replication.go b/model/replication.go index 6e35e312..4f76c2e1 100644 --- a/model/replication.go +++ b/model/replication.go @@ -92,8 +92,8 @@ func StoragePricePerEpochToPricePerDeal(price string, dealSize int64, durationEp type DealID uint64 // Deal is the deal model for all deals made by deal pusher or tracked by the tracker. -// The index on PieceCID is used to track replication of the same piece CID. -// The index on State and ClientID is used to calculate number and size of pending deals. +// index on PieceCID tracks replication of same piece +// index on State and ClientActorID calculates pending deals type Deal struct { ID DealID `gorm:"primaryKey" json:"id" table:"verbose"` CreatedAt time.Time `json:"createdAt" table:"verbose;format:2006-01-02 15:04:05"` @@ -120,15 +120,15 @@ type Deal struct { NextChallengeEpoch *int32 `json:"nextChallengeEpoch,omitempty" table:"verbose"` // NextChallengeEpoch is the next epoch when a challenge proof is due // Associations - ScheduleID *ScheduleID `json:"scheduleId" table:"verbose"` - Schedule *Schedule `gorm:"foreignKey:ScheduleID;constraint:OnDelete:SET NULL" json:"schedule,omitempty" swaggerignore:"true" table:"expand"` - ClientID string `gorm:"index:idx_pending" json:"clientId"` - Wallet *Wallet `gorm:"foreignKey:ClientID;constraint:OnDelete:SET NULL" json:"wallet,omitempty" swaggerignore:"true" table:"expand"` + ScheduleID *ScheduleID `json:"scheduleId" table:"verbose"` + Schedule *Schedule `gorm:"foreignKey:ScheduleID;constraint:OnDelete:SET NULL" json:"schedule,omitempty" swaggerignore:"true" table:"expand"` + ClientActorID string `gorm:"index:idx_pending;column:client_id" json:"clientActorId"` // TODO: rename column after migration + Actor *Actor `gorm:"foreignKey:ClientActorID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` } // Key returns a mostly unique key to match deal from locally proposed deals and deals from the chain. func (d Deal) Key() string { - return fmt.Sprintf("%s-%s-%s-%d-%d", d.ClientID, d.Provider, d.PieceCID.String(), d.StartEpoch, d.EndEpoch) + return fmt.Sprintf("%s-%s-%s-%d-%d", d.ClientActorID, d.Provider, d.PieceCID.String(), d.StartEpoch, d.EndEpoch) } type ScheduleID uint32 @@ -167,10 +167,18 @@ type Schedule struct { Preparation *Preparation `gorm:"foreignKey:PreparationID;constraint:OnDelete:CASCADE" json:"preparation,omitempty" swaggerignore:"true" table:"expand"` } -type Wallet struct { - ID string `gorm:"primaryKey;size:15" json:"id"` // ID is the short ID of the wallet - Address string `gorm:"index" json:"address"` // Address is the Filecoin full address of the wallet - PrivateKey string `json:"privateKey,omitempty" table:"-"` // PrivateKey is the private key of the wallet +// on-chain actor identity tracked by singularity +// actor may or may not be controlled by us (linked via optional WalletID) +// TODO: after migration, add WalletID field linking to new Wallet model +type Actor struct { + ID string `gorm:"primaryKey;size:15" json:"id"` // actor ID (f0...) + Address string `gorm:"index" json:"address"` // filecoin address + PrivateKey string `json:"privateKey,omitempty" table:"-"` // TODO: orphaned column, will be dropped by export-keys command +} + +// GORM will rename "wallets" table to "actors" on AutoMigrate +func (Actor) TableName() string { + return "actors" } // PDPProofSet tracks on-chain PDP proof set state derived from contract events. diff --git a/model/wallet.go b/model/wallet.go index c252a439..7f1a5c43 100644 --- a/model/wallet.go +++ b/model/wallet.go @@ -2,8 +2,7 @@ package model // private key stored in external keystore, can be linked to on-chain actor // wallets can exist before actors are created on-chain -// TODO: rename to Wallet after step 6 (old Wallet→Actor rename) -type WalletKey struct { +type Wallet struct { ID uint `gorm:"primaryKey" json:"id"` KeyPath string `gorm:"uniqueIndex;not null" json:"keyPath"` // absolute path to key file @@ -13,12 +12,10 @@ type WalletKey struct { ActorID *string `gorm:"index;size:15" json:"actorId,omitempty"` // nullable, links to on-chain actor f0... - // TODO: uncomment after step 6 rename - // Actor *Actor `gorm:"foreignKey:ActorID;references:ID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` + Actor *Actor `gorm:"foreignKey:ActorID;references:ID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` } -// temporary table name to avoid conflict with existing wallets table -// TODO: return "wallets" after step 6 rename -func (WalletKey) TableName() string { - return "wallet_keys" +// GORM will rename "wallet_keys" table to "wallets" on AutoMigrate +func (Wallet) TableName() string { + return "wallets" } diff --git a/replication/makedeal.go b/replication/makedeal.go index b074b2d7..4209299b 100644 --- a/replication/makedeal.go +++ b/replication/makedeal.go @@ -25,7 +25,7 @@ import ( "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/jellydator/ttlcache/v3" - "github.com/jsign/go-filsigner/wallet" + filwallet "github.com/jsign/go-filsigner/wallet" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" @@ -55,7 +55,7 @@ type DealProviderCollateralBound struct { } type DealMaker interface { - MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig DealConfig) (*model.Deal, error) + MakeDeal(ctx context.Context, walletObj model.Actor, car model.Car, dealConfig DealConfig) (*model.Deal, error) } // DealMakerImpl is an implementation of a deal-making component for a Filecoin-like network. @@ -515,7 +515,7 @@ func (d DealConfig) GetPrice(pieceSize int64, duration time.Duration) big.Int { // - Deal proposal rejected by the provider. // // - No supported protocol found between client and provider. -func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Wallet, +func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Actor, car model.Car, dealConfig DealConfig, ) (*model.Deal, error) { logger.Infow("making deal", "client", walletObj.ID, "pieceCID", car.PieceCID.String(), "provider", dealConfig.Provider) @@ -578,7 +578,9 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Wallet, return nil, errors.Wrapf(err, "failed to serialize deal proposal %s", proposal) } - signature, err := wallet.WalletSign(walletObj.PrivateKey, proposalBytes) + // TODO: update to use new keystore-based signing with handler/wallet.SignWithWallet() + // for now, PrivateKey column still exists in actors table as orphaned column + signature, err := filwallet.WalletSign(walletObj.PrivateKey, proposalBytes) if err != nil { return nil, errors.Wrap(err, "failed to sign deal proposal") } @@ -589,10 +591,10 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Wallet, } dealModel := &model.Deal{ - State: model.DealProposed, - ClientID: walletObj.ID, - Provider: dealConfig.Provider, - Label: cid.Cid(car.RootCID).String(), + State: model.DealProposed, + ClientActorID: walletObj.ID, + Provider: dealConfig.Provider, + Label: cid.Cid(car.RootCID).String(), PieceCID: car.PieceCID, PieceSize: car.PieceSize, //nolint:gosec // G115: Safe conversion, max int32 epoch won't occur until year 4062 @@ -635,7 +637,7 @@ func queueDealEvent(deal model.Deal) { DataCID: deal.Label, PieceSize: deal.PieceSize, Provider: deal.Provider, - Client: deal.ClientID, + Client: deal.ClientActorID, Verified: deal.Verified, StartEpoch: deal.StartEpoch, EndEpoch: deal.EndEpoch - deal.StartEpoch, diff --git a/replication/wallet.go b/replication/wallet.go index 74e457c6..a099ea64 100644 --- a/replication/wallet.go +++ b/replication/wallet.go @@ -19,46 +19,27 @@ import ( var logger = logging.Logger("replication") type WalletChooser interface { - Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) + Choose(ctx context.Context, actors []model.Actor) (model.Actor, error) } type RandomWalletChooser struct{} -var ErrNoWallet = errors.New("no wallets to choose from") - -var ErrNoDatacap = errors.New("no wallets have enough datacap") - -// Choose selects a random Wallet from the provided slice of Wallets. -// -// The Choose function of the RandomWalletChooser type randomly selects -// a Wallet from a given slice of Wallets. If the slice is empty, the function -// returns an error. It uses a cryptographically secure random number generator -// to make the selection. -// -// Parameters: -// - ctx context.Context: The context to use for cancellation and deadlines, -// although it is not used in this implementation. -// - wallets []model.Wallet: A slice of Wallet objects from which a random Wallet -// will be chosen. -// -// Returns: -// - model.Wallet: The randomly chosen Wallet object from the provided slice. -// - error: An error that will be returned if any issues were encountered while trying -// to choose a Wallet. This includes the case when the input slice is empty, -// in which case ErrNoWallet will be returned, or if there is an issue generating -// a random number. -func (w RandomWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) { - // Check if the wallets slice is empty - if len(wallets) == 0 { - return model.Wallet{}, ErrNoWallet +var ErrNoWallet = errors.New("no actors to choose from") + +var ErrNoDatacap = errors.New("no actors have enough datacap") + +// randomly selects an actor using cryptographically secure random number generator +func (w RandomWalletChooser) Choose(ctx context.Context, actors []model.Actor) (model.Actor, error) { + if len(actors) == 0 { + return model.Actor{}, ErrNoWallet } - randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(wallets)))) + randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(actors)))) if err != nil { - return model.Wallet{}, errors.WithStack(err) + return model.Actor{}, errors.WithStack(err) } - chosenWallet := wallets[randomPick.Int64()] - return chosenWallet, nil + chosen := actors[randomPick.Int64()] + return chosen, nil } type DatacapWalletChooser struct { @@ -84,99 +65,77 @@ func NewDatacapWalletChooser(db *gorm.DB, cacheTTL time.Duration, } } -func (w DatacapWalletChooser) getDatacap(ctx context.Context, wallet model.Wallet) (int64, error) { +func (w DatacapWalletChooser) getDatacap(ctx context.Context, actor model.Actor) (int64, error) { var result string - err := w.lotusClient.CallFor(ctx, &result, "Filecoin.StateMarketBalance", wallet.Address, nil) + err := w.lotusClient.CallFor(ctx, &result, "Filecoin.StateMarketBalance", actor.Address, nil) if err != nil { return 0, errors.WithStack(err) } return strconv.ParseInt(result, 10, 64) } -func (w DatacapWalletChooser) getDatacapCached(ctx context.Context, wallet model.Wallet) (int64, error) { - file := w.cache.Get(wallet.Address) +func (w DatacapWalletChooser) getDatacapCached(ctx context.Context, actor model.Actor) (int64, error) { + file := w.cache.Get(actor.Address) if file != nil && !file.IsExpired() { return file.Value(), nil } - datacap, err := w.getDatacap(ctx, wallet) + datacap, err := w.getDatacap(ctx, actor) if err != nil { - logger.Errorf("failed to get datacap for wallet %s: %s", wallet.Address, err) + logger.Errorf("failed to get datacap for actor %s: %s", actor.Address, err) if file != nil { return file.Value(), nil } return 0, errors.WithStack(err) } - w.cache.Set(wallet.Address, datacap, ttlcache.DefaultTTL) + w.cache.Set(actor.Address, datacap, ttlcache.DefaultTTL) return datacap, nil } -func (w DatacapWalletChooser) getPendingDeals(ctx context.Context, wallet model.Wallet) (int64, error) { +func (w DatacapWalletChooser) getPendingDeals(ctx context.Context, actor model.Actor) (int64, error) { var totalPieceSize int64 err := w.db.WithContext(ctx).Model(&model.Deal{}). Select("COALESCE(SUM(piece_size), 0)"). - Where("client_id = ? AND verified AND state = ?", wallet.ID, model.DealProposed). + Where("client_id = ? AND verified AND state = ?", actor.ID, model.DealProposed). Scan(&totalPieceSize). Error if err != nil { - logger.Errorf("failed to get pending deals for wallet %s: %s", wallet.Address, err) + logger.Errorf("failed to get pending deals for actor %s: %s", actor.Address, err) return 0, errors.WithStack(err) } return totalPieceSize, nil } -// Choose selects a random Wallet from the provided slice of Wallets based on certain criteria. -// -// The Choose function of the DatacapWalletChooser type filters the given slice of Wallets -// based on a specific criterion, which is whether the datacap for the wallet minus -// the pending deals for the wallet is greater or equal to a minimum threshold (w.min). -// From the filtered eligible Wallets, the function then randomly selects one Wallet. -// It uses a cryptographically secure random number generator to make the selection. -// If the initial slice of Wallets is empty, or if no Wallets meet the criteria, -// the function returns an error. -// -// Parameters: -// - ctx context.Context: The context to use for cancellation and deadlines, used -// in the datacap and pending deals fetching operations. -// - wallets []model.Wallet: A slice of Wallet objects from which a random Wallet -// will be chosen based on the criteria. -// -// Returns: -// - model.Wallet: The randomly chosen Wallet object from the filtered eligible Wallets. -// - error: An error that will be returned if any issues were encountered while trying -// to choose a Wallet. This includes the case when the input slice is empty, -// in which case ErrNoWallet will be returned, when no Wallets meet the criteria, -// in which case ErrNoDatacap will be returned, or if there is an issue generating -// a random number. -func (w DatacapWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) { - if len(wallets) == 0 { - return model.Wallet{}, ErrNoWallet +// selects random actor with sufficient datacap (datacap - pending deals >= min threshold) +func (w DatacapWalletChooser) Choose(ctx context.Context, actors []model.Actor) (model.Actor, error) { + if len(actors) == 0 { + return model.Actor{}, ErrNoWallet } - var eligibleWallets []model.Wallet - for _, wallet := range wallets { - datacap, err := w.getDatacapCached(ctx, wallet) + var eligible []model.Actor + for _, actor := range actors { + datacap, err := w.getDatacapCached(ctx, actor) if err != nil { - logger.Errorw("failed to get datacap for wallet", "wallet", wallet.Address, "error", err) + logger.Errorw("failed to get datacap for actor", "actor", actor.Address, "error", err) continue } - pendingDeals, err := w.getPendingDeals(ctx, wallet) + pendingDeals, err := w.getPendingDeals(ctx, actor) if err != nil { - logger.Errorw("failed to get pending deals for wallet", "wallet", wallet.Address, "error", err) + logger.Errorw("failed to get pending deals for actor", "actor", actor.Address, "error", err) continue } if datacap-pendingDeals >= int64(w.min) { - eligibleWallets = append(eligibleWallets, wallet) + eligible = append(eligible, actor) } } - if len(eligibleWallets) == 0 { - return model.Wallet{}, ErrNoDatacap + if len(eligible) == 0 { + return model.Actor{}, ErrNoDatacap } - randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(eligibleWallets)))) + randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(eligible)))) if err != nil { - return model.Wallet{}, errors.WithStack(err) + return model.Actor{}, errors.WithStack(err) } - chosenWallet := eligibleWallets[randomPick.Int64()] - return chosenWallet, nil + chosen := eligible[randomPick.Int64()] + return chosen, nil } diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index 2e8321af..dce85f63 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -290,7 +290,7 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) } var car model.Car var dealModel *model.Deal - var walletObj model.Wallet + var walletObj model.Actor if schedule.MaxPendingDealNumber > 0 && pending.DealNumber >= schedule.MaxPendingDealNumber { Logger.Infow("skipping this time since the max pending deal is reached", "schedule_id", schedule.ID) goto waitForPending @@ -360,7 +360,7 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) return model.ScheduleError, errors.Wrap(err, "failed to find car") } - walletObj, err = d.walletChooser.Choose(ctx, schedule.Preparation.Wallets) + walletObj, err = d.walletChooser.Choose(ctx, schedule.Preparation.Actors) if err != nil { return model.ScheduleError, errors.Wrap(err, "failed to choose wallet") } @@ -499,7 +499,7 @@ func (d *DealPusher) runOnce(ctx context.Context) { scheduleMap := map[model.ScheduleID]model.Schedule{} Logger.Debugw("getting schedules") db := d.dbNoContext.WithContext(ctx) - err := db.Preload("Preparation.Wallets").Where("state = ?", + err := db.Preload("Preparation.Actors").Where("state = ?", model.ScheduleActive).Find(&schedules).Error if err != nil { Logger.Errorw("failed to get schedules", "error", err) diff --git a/service/dealtracker/dealtracker.go b/service/dealtracker/dealtracker.go index c878d27f..b48be228 100644 --- a/service/dealtracker/dealtracker.go +++ b/service/dealtracker/dealtracker.go @@ -381,10 +381,10 @@ type KnownDeal struct { State model.DealState } type UnknownDeal struct { - ID model.DealID - ClientID string - Provider string - PieceCID model.CID + ID model.DealID + ClientID string + Provider string + PieceCID model.CID StartEpoch int32 EndEpoch int32 } @@ -424,16 +424,16 @@ func (d *DealTracker) runOnce(ctx context.Context) error { var lastEpoch int32 db := d.dbNoContext.WithContext(ctx) - var wallets []model.Wallet - err = db.Find(&wallets).Error + var actors []model.Actor + err = db.Find(&actors).Error if err != nil { - return errors.Wrap(err, "failed to get wallets from database") + return errors.Wrap(err, "failed to get actors from database") } - walletIDs := make(map[string]struct{}) - for _, wallet := range wallets { - Logger.Infof("tracking deals for wallet %s", wallet.ID) - walletIDs[wallet.ID] = struct{}{} + actorIDs := make(map[string]struct{}) + for _, actor := range actors { + Logger.Infof("tracking deals for actor %s", actor.ID) + actorIDs[actor.ID] = struct{}{} } knownDeals := make(map[uint64]model.DealState) @@ -467,10 +467,10 @@ func (d *DealTracker) runOnce(ctx context.Context) error { } key := deal.Key() unknownDeals[key] = append(unknownDeals[key], UnknownDeal{ - ID: deal.ID, - ClientID: deal.ClientID, - Provider: deal.Provider, - PieceCID: deal.PieceCID, + ID: deal.ID, + ClientID: deal.ClientID, + Provider: deal.Provider, + PieceCID: deal.PieceCID, StartEpoch: deal.StartEpoch, EndEpoch: deal.EndEpoch, }) @@ -488,7 +488,7 @@ func (d *DealTracker) runOnce(ctx context.Context) error { if deal.State.LastUpdatedEpoch > lastEpoch { lastEpoch = deal.State.LastUpdatedEpoch } - _, ok := walletIDs[deal.Proposal.Client] + _, ok := actorIDs[deal.Proposal.Client] if !ok { return nil } @@ -556,7 +556,7 @@ func (d *DealTracker) runOnce(ctx context.Context) error { return db.Create(&model.Deal{ DealID: &dealID, State: newState, - DealType: model.DealTypeMarket, // Legacy market deal (f05) + DealType: model.DealTypeMarket, ClientID: deal.Proposal.Client, Provider: deal.Proposal.Provider, Label: deal.Proposal.Label, diff --git a/testdb/main.go b/testdb/main.go index ba0f1e33..840bbb7e 100644 --- a/testdb/main.go +++ b/testdb/main.go @@ -74,8 +74,8 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Type: "local", Path: urlToPath(gofakeit.URL()), } - // Setup wallet - wallet := model.Wallet{ + // Setup actor + actor := model.Actor{ ID: fmt.Sprintf("f0%d", r.Intn(10000)), Address: "f1" + randomLetterString(39), } @@ -85,7 +85,7 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Name: gofakeit.AppName(), MaxSize: 30 << 30, PieceSize: 1 << 35, - Wallets: []model.Wallet{wallet}, + Actors: []model.Actor{actor}, SourceStorages: []model.Storage{source}, } @@ -342,7 +342,7 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Price: "0", Verified: true, ScheduleID: ptr.Of(schedule.ID), - ClientID: wallet.ID, + ClientActorID: actor.ID, } if state == model.DealActive { //nolint:gosec // G115: Safe conversion, max int32 epoch won't occur until year 4062 diff --git a/util/keystore/keystore.go b/util/keystore/keystore.go index d5bfbbb4..87cd86ac 100644 --- a/util/keystore/keystore.go +++ b/util/keystore/keystore.go @@ -6,7 +6,7 @@ import ( "path/filepath" "github.com/filecoin-project/go-address" - "github.com/jsign/go-filsigner/wallet" + filwallet "github.com/jsign/go-filsigner/wallet" ) type KeyStore interface { @@ -36,7 +36,7 @@ func NewLocalKeyStore(dir string) (*LocalKeyStore, error) { // lotus/go-filsigner export format expected func (ks *LocalKeyStore) Put(privateKey string) (string, address.Address, error) { - addr, err := wallet.PublicKey(privateKey) + addr, err := filwallet.PublicKey(privateKey) if err != nil { return "", address.Undef, fmt.Errorf("failed to derive address from private key: %w", err) } @@ -79,7 +79,7 @@ func (ks *LocalKeyStore) List() ([]KeyInfo, error) { } // verify valid key by deriving address - addr, err := wallet.PublicKey(string(data)) + addr, err := filwallet.PublicKey(string(data)) if err != nil { continue // skip invalid } From de6203d5e92442f74b38169964b4f6e4570408c7 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Mon, 10 Nov 2025 14:26:54 +0100 Subject: [PATCH 03/15] complete wallet-agent migration --- api/api_test.go | 2 +- cmd/deal/send-manual.go | 10 +++- cmd/deal_test.go | 6 +-- handler/deal/interface.go | 6 ++- handler/deal/send-manual.go | 4 +- handler/deal/send-manual_test.go | 79 ++++++++++++++++++-------------- handler/wallet/attach.go | 59 ++++++++++++++++-------- handler/wallet/detach.go | 19 ++------ model/replication.go | 12 ++--- replication/makedeal.go | 32 ++++++++----- service/dealpusher/dealpusher.go | 32 ++++++++----- testdb/main.go | 2 +- 12 files changed, 158 insertions(+), 105 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index ac63411b..9e96a4c4 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -97,7 +97,7 @@ func setupMockDeal() deal.Handler { m := new(deal.MockDeal) m.On("ListHandler", mock.Anything, mock.Anything, mock.Anything). Return([]model.Deal{{}}, nil) - m.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + m.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(&model.Deal{}, nil) return m } diff --git a/cmd/deal/send-manual.go b/cmd/deal/send-manual.go index f01538d7..16ba2c4c 100644 --- a/cmd/deal/send-manual.go +++ b/cmd/deal/send-manual.go @@ -8,9 +8,11 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/deal" + "github.com/data-preservation-programs/singularity/handler/wallet" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service/epochutil" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/urfave/cli/v2" ) @@ -176,13 +178,19 @@ Notes: return errors.Wrap(err, "failed to init host") } defer h.Close() + + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return errors.Wrap(err, "failed to init keystore") + } + dealMaker := replication.NewDealMaker( util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")), h, 10*timeout, timeout, ) - dealModel, err := deal.Default.SendManualHandler(ctx, db, dealMaker, proposal) + dealModel, err := deal.Default.SendManualHandler(ctx, db, ks, dealMaker, proposal) if err != nil { return errors.WithStack(err) } diff --git a/cmd/deal_test.go b/cmd/deal_test.go index 4430543b..c1a32a56 100644 --- a/cmd/deal_test.go +++ b/cmd/deal_test.go @@ -24,13 +24,13 @@ func swapDealHandler(mockHandler deal.Handler) func() { func TestSendDealHandler(t *testing.T) { testutil.One(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Wallet{ID: "client_id"}).Error + err := db.Create(&model.Actor{ID: "client_id"}).Error require.NoError(t, err) runner := NewRunner() defer runner.Save(t) mockHandler := new(deal.MockDeal) defer swapDealHandler(mockHandler)() - mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ + mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ State: "proposed", Provider: "f01", ProposalID: "proposal_id", @@ -46,7 +46,7 @@ func TestSendDealHandler(t *testing.T) { }, nil).Once() _, _, err = runner.Run(ctx, "singularity deal send-manual --client client --provider provider --piece-cid piece_cid --piece-size 1024 --save") require.NoError(t, err) - mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ + mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ State: "proposed", Provider: "f01", ProposalID: "proposal_id", diff --git a/handler/deal/interface.go b/handler/deal/interface.go index e6f92ce8..4efc3268 100644 --- a/handler/deal/interface.go +++ b/handler/deal/interface.go @@ -6,6 +6,7 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/stretchr/testify/mock" "gorm.io/gorm" ) @@ -15,6 +16,7 @@ type Handler interface { SendManualHandler( ctx context.Context, db *gorm.DB, + ks keystore.KeyStore, dealMaker replication.DealMaker, request Proposal, ) (*model.Deal, error) @@ -35,7 +37,7 @@ func (m *MockDeal) ListHandler(ctx context.Context, db *gorm.DB, request ListDea return args.Get(0).([]model.Deal), args.Error(1) } -func (m *MockDeal) SendManualHandler(ctx context.Context, db *gorm.DB, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) { - args := m.Called(ctx, db, dealMaker, request) +func (m *MockDeal) SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) { + args := m.Called(ctx, db, ks, dealMaker, request) return args.Get(0).(*model.Deal), args.Error(1) } diff --git a/handler/deal/send-manual.go b/handler/deal/send-manual.go index bde60a54..a45df05f 100644 --- a/handler/deal/send-manual.go +++ b/handler/deal/send-manual.go @@ -11,6 +11,7 @@ import ( "github.com/data-preservation-programs/singularity/handler/handlererror" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/dustin/go-humanize" "github.com/ipfs/go-cid" "gorm.io/gorm" @@ -66,6 +67,7 @@ func argToDuration(s string) (time.Duration, error) { func (DefaultHandler) SendManualHandler( ctx context.Context, db *gorm.DB, + ks keystore.KeyStore, dealMaker replication.DealMaker, request Proposal, ) (*model.Deal, error) { @@ -136,7 +138,7 @@ func (DefaultHandler) SendManualHandler( Duration: duration, } - dealModel, err := dealMaker.MakeDeal(ctx, actor, car, dealConfig) + dealModel, err := dealMaker.MakeDeal(ctx, db, ks, actor, car, dealConfig) if err != nil { return nil, errors.WithStack(err) } diff --git a/handler/deal/send-manual_test.go b/handler/deal/send-manual_test.go index 0d55f66a..bc911932 100644 --- a/handler/deal/send-manual_test.go +++ b/handler/deal/send-manual_test.go @@ -8,6 +8,7 @@ import ( "github.com/data-preservation-programs/singularity/handler/handlererror" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -18,8 +19,8 @@ type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, walletObj, car, dealConfig) +func (m *MockDealMaker) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { + args := m.Called(ctx, db, ks, walletObj, car, dealConfig) return args.Get(0).(*model.Deal), args.Error(1) } @@ -40,158 +41,166 @@ var proposal = Proposal{ } func TestSendManualHandler_WalletNotFound(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f09999", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, proposal) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, proposal) require.ErrorIs(t, err, handlererror.ErrNotFound) require.ErrorContains(t, err, "client address") }) } func TestSendManualHandler_InvalidPieceCID(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceCID = "bad" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece CID") }) } func TestSendManualHandler_InvalidPieceCID_NOTCOMMP(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceCID = proposal.RootCID - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be commp") }) } func TestSendManualHandler_InvalidPieceSize(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceSize = "aaa" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece size") }) } func TestSendManualHandler_InvalidPieceSize_NotPowerOfTwo(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceSize = "31GiB" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be a power of 2") }) } func TestSendManualHandler_InvalidRootCID(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.RootCID = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid root CID") }) } func TestSendManualHandler_InvalidDuration(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.Duration = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid duration") }) } func TestSendManualHandler_InvalidStartDelay(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + mockKS := keystore.NewMemoryKeyStore() mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.StartDelay = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid start delay") }) @@ -221,7 +230,7 @@ func TestSendManualHandler(t *testing.T) { PricePerGB: proposal.PricePerGB, PricePerGBEpoch: proposal.PricePerGBEpoch, }).Return(&model.Deal{}, nil) - resp, err := Default.SendManualHandler(ctx, db, mockDealMaker, proposal) + resp, err := Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, proposal) mockDealMaker.AssertExpectations(t) require.NoError(t, err) require.NotNil(t, resp) diff --git a/handler/wallet/attach.go b/handler/wallet/attach.go index e2b374bf..b7419ccd 100644 --- a/handler/wallet/attach.go +++ b/handler/wallet/attach.go @@ -10,44 +10,67 @@ import ( "gorm.io/gorm" ) -// AttachHandler associates a wallet with a specific preparation based on given preparationID and wallet address or ID. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - preparationID: The ID or name of the preparation to which the wallet will be attached. -// - wallet: The address or ID of the wallet to be attached to the preparation. -// -// Returns: -// - A pointer to the updated Preparation instance. -// - An error, if any occurred during the association operation. +// attaches actor to preparation for deal-making +// accepts actor ID (f0...) or wallet address/ID +// wallet must already be linked to on-chain actor func (DefaultHandler) AttachHandler( ctx context.Context, db *gorm.DB, preparationID string, - wallet string, + actorOrWallet string, ) (*model.Preparation, error) { db = db.WithContext(ctx) var preparation model.Preparation - err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Wallets") + err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Actors") if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", preparationID) + return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %s not found", preparationID) } if err != nil { return nil, errors.WithStack(err) } - var w model.Wallet - err = db.Where("address = ? OR id = ?", wallet, wallet).First(&w).Error + // try to find as actor ID first + var actor model.Actor + err = db.Where("id = ?", actorOrWallet).First(&actor).Error + if err == nil { + // found actor directly + err = database.DoRetry(ctx, func() error { + return db.Model(&preparation).Association("Actors").Append(&actor) + }) + if err != nil { + return nil, errors.WithStack(err) + } + return &preparation, nil + } + + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.WithStack(err) + } + + // not found as actor, try as wallet address or ID + var wallet model.Wallet + err = db.Where("address = ? OR id = ?", actorOrWallet, actorOrWallet).First(&wallet).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", wallet) + return nil, errors.Wrapf(handlererror.ErrNotFound, "actor or wallet %s not found", actorOrWallet) } if err != nil { return nil, errors.WithStack(err) } + // wallet found - check if it has an actor + if wallet.ActorID == nil || *wallet.ActorID == "" { + return nil, errors.Wrapf(handlererror.ErrInvalidParameter, + "wallet %s not yet linked to on-chain actor - fund the wallet first", wallet.Address) + } + + // get the actor + err = db.Where("id = ?", *wallet.ActorID).First(&actor).Error + if err != nil { + return nil, errors.Wrapf(err, "actor %s not found", *wallet.ActorID) + } + err = database.DoRetry(ctx, func() error { - return db.Model(&preparation).Association("Wallets").Append(&w) + return db.Model(&preparation).Association("Actors").Append(&actor) }) if err != nil { return nil, errors.WithStack(err) diff --git a/handler/wallet/detach.go b/handler/wallet/detach.go index ce32d4bf..a037e220 100644 --- a/handler/wallet/detach.go +++ b/handler/wallet/detach.go @@ -11,22 +11,13 @@ import ( "gorm.io/gorm" ) -// DetachHandler removes the association of a wallet from a specific preparation based on the given preparationID and wallet address or ID. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - preparationID: The ID or name of the preparation from which the wallet will be removed. -// - wallet: The address or ID of the wallet to be removed from the preparation. -// -// Returns: -// - A pointer to the updated Preparation instance. -// - An error, if any occurred during the removal operation. +// detaches actor from preparation +// accepts actor ID or address func (DefaultHandler) DetachHandler( ctx context.Context, db *gorm.DB, preparationID string, - wallet string, + actorIDOrAddress string, ) (*model.Preparation, error) { db = db.WithContext(ctx) var preparation model.Preparation @@ -39,10 +30,10 @@ func (DefaultHandler) DetachHandler( } found, err := underscore.Find(preparation.Actors, func(a model.Actor) bool { - return a.ID == wallet || a.Address == wallet + return a.ID == actorIDOrAddress || a.Address == actorIDOrAddress }) if err != nil { - return nil, errors.Wrapf(handlererror.ErrNotFound, "actor %s not attached to preparation %d", wallet, preparationID) + return nil, errors.Wrapf(handlererror.ErrNotFound, "actor %s not attached to preparation %s", actorIDOrAddress, preparationID) } err = database.DoRetry(ctx, func() error { diff --git a/model/replication.go b/model/replication.go index 4f76c2e1..386fddb6 100644 --- a/model/replication.go +++ b/model/replication.go @@ -93,7 +93,7 @@ type DealID uint64 // Deal is the deal model for all deals made by deal pusher or tracked by the tracker. // index on PieceCID tracks replication of same piece -// index on State and ClientActorID calculates pending deals +// index on State and ClientID calculates pending deals type Deal struct { ID DealID `gorm:"primaryKey" json:"id" table:"verbose"` CreatedAt time.Time `json:"createdAt" table:"verbose;format:2006-01-02 15:04:05"` @@ -120,15 +120,15 @@ type Deal struct { NextChallengeEpoch *int32 `json:"nextChallengeEpoch,omitempty" table:"verbose"` // NextChallengeEpoch is the next epoch when a challenge proof is due // Associations - ScheduleID *ScheduleID `json:"scheduleId" table:"verbose"` - Schedule *Schedule `gorm:"foreignKey:ScheduleID;constraint:OnDelete:SET NULL" json:"schedule,omitempty" swaggerignore:"true" table:"expand"` - ClientActorID string `gorm:"index:idx_pending;column:client_id" json:"clientActorId"` // TODO: rename column after migration - Actor *Actor `gorm:"foreignKey:ClientActorID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` + ScheduleID *ScheduleID `json:"scheduleId" table:"verbose"` + Schedule *Schedule `gorm:"foreignKey:ScheduleID;constraint:OnDelete:SET NULL" json:"schedule,omitempty" swaggerignore:"true" table:"expand"` + ClientID string `gorm:"index:idx_pending" json:"clientId"` + Actor *Actor `gorm:"foreignKey:ClientID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` } // Key returns a mostly unique key to match deal from locally proposed deals and deals from the chain. func (d Deal) Key() string { - return fmt.Sprintf("%s-%s-%s-%d-%d", d.ClientActorID, d.Provider, d.PieceCID.String(), d.StartEpoch, d.EndEpoch) + return fmt.Sprintf("%s-%s-%s-%d-%d", d.ClientID, d.Provider, d.PieceCID.String(), d.StartEpoch, d.EndEpoch) } type ScheduleID uint32 diff --git a/replication/makedeal.go b/replication/makedeal.go index 4209299b..110e5817 100644 --- a/replication/makedeal.go +++ b/replication/makedeal.go @@ -22,16 +22,18 @@ import ( "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/go-state-types/builtin/v9/market" "github.com/filecoin-shipyard/boostly" + "github.com/data-preservation-programs/singularity/handler/wallet" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/jellydator/ttlcache/v3" - filwallet "github.com/jsign/go-filsigner/wallet" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/core/protocol" "github.com/multiformats/go-multiaddr" "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" ) const ( @@ -55,7 +57,7 @@ type DealProviderCollateralBound struct { } type DealMaker interface { - MakeDeal(ctx context.Context, walletObj model.Actor, car model.Car, dealConfig DealConfig) (*model.Deal, error) + MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig DealConfig) (*model.Deal, error) } // DealMakerImpl is an implementation of a deal-making component for a Filecoin-like network. @@ -491,7 +493,7 @@ func (d DealConfig) GetPrice(pieceSize int64, duration time.Duration) big.Int { // // Parameters: // - ctx context.Context: The context to use for timeouts and cancellation. -// - walletObj model.Wallet: The client's wallet, containing the client's addresses and private key. +// - actorObj model.Wallet: The client's wallet, containing the client's addresses and private key. // - car model.Car: The car file that contains the data to be stored. // - dealConfig DealConfig: The configuration for the deal, including price and duration. // @@ -515,14 +517,15 @@ func (d DealConfig) GetPrice(pieceSize int64, duration time.Duration) big.Int { // - Deal proposal rejected by the provider. // // - No supported protocol found between client and provider. -func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Actor, +func (d DealMakerImpl) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig DealConfig, ) (*model.Deal, error) { - logger.Infow("making deal", "client", walletObj.ID, "pieceCID", car.PieceCID.String(), "provider", dealConfig.Provider) + db = db.WithContext(ctx) + logger.Infow("making deal", "client", actorObj.ID, "pieceCID", car.PieceCID.String(), "provider", dealConfig.Provider) now := time.Now().UTC() - addr, err := address.NewFromString(walletObj.Address) + addr, err := address.NewFromString(actorObj.Address) if err != nil { - return nil, errors.Wrapf(err, "failed to parse wallet address %s", walletObj.Address) + return nil, errors.Wrapf(err, "failed to parse wallet address %s", actorObj.Address) } pvd, err := address.NewFromString(dealConfig.Provider) @@ -578,9 +581,14 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Actor, return nil, errors.Wrapf(err, "failed to serialize deal proposal %s", proposal) } - // TODO: update to use new keystore-based signing with handler/wallet.SignWithWallet() - // for now, PrivateKey column still exists in actors table as orphaned column - signature, err := filwallet.WalletSign(walletObj.PrivateKey, proposalBytes) + // load wallet that controls this actor + walletRecord, err := wallet.LoadWalletByActorID(ctx, db, actorObj.ID) + if err != nil { + return nil, errors.Wrapf(err, "failed to load wallet for actor %s", actorObj.ID) + } + + // sign using keystore-based signing + signature, err := wallet.SignWithWallet(ks, *walletRecord, proposalBytes) if err != nil { return nil, errors.Wrap(err, "failed to sign deal proposal") } @@ -592,7 +600,7 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Actor, dealModel := &model.Deal{ State: model.DealProposed, - ClientActorID: walletObj.ID, + ClientID: actorObj.ID, Provider: dealConfig.Provider, Label: cid.Cid(car.RootCID).String(), PieceCID: car.PieceCID, @@ -637,7 +645,7 @@ func queueDealEvent(deal model.Deal) { DataCID: deal.Label, PieceSize: deal.PieceSize, Provider: deal.Provider, - Client: deal.ClientActorID, + Client: deal.ClientID, Verified: deal.Verified, StartEpoch: deal.StartEpoch, EndEpoch: deal.EndEpoch - deal.StartEpoch, diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index dce85f63..61064971 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -10,10 +10,12 @@ import ( "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/analytics" "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/wallet" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service/healthcheck" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/ipfs/go-log/v2" @@ -35,11 +37,12 @@ var waitPendingInterval = time.Minute // DealPusher represents a struct that encapsulates the data and functionality related to pushing deals in a replication process. type DealPusher struct { - dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. - walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. - dealMaker replication.DealMaker // Object responsible for making a deal in replication. - pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. - pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. + dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. + keyStore keystore.KeyStore // Keystore for loading private keys + walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. + dealMaker replication.DealMaker // Object responsible for making a deal in replication. + pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. + pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. // Resolver is injected so tests and future wiring can switch deal type behavior without coupling DealPusher to config storage. scheduleDealTypeResolver func(schedule *model.Schedule) model.DealType workerID uuid.UUID // UUID identifying the associated worker. @@ -290,7 +293,7 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) } var car model.Car var dealModel *model.Deal - var walletObj model.Actor + var actorObj model.Actor if schedule.MaxPendingDealNumber > 0 && pending.DealNumber >= schedule.MaxPendingDealNumber { Logger.Infow("skipping this time since the max pending deal is reached", "schedule_id", schedule.ID) goto waitForPending @@ -360,7 +363,7 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) return model.ScheduleError, errors.Wrap(err, "failed to find car") } - walletObj, err = d.walletChooser.Choose(ctx, schedule.Preparation.Actors) + actorObj, err = d.walletChooser.Choose(ctx, schedule.Preparation.Actors) if err != nil { return model.ScheduleError, errors.Wrap(err, "failed to choose wallet") } @@ -368,7 +371,9 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) err = retry.Do(func() error { dealModel, err = d.dealMaker.MakeDeal( ctx, - walletObj, + d.dbNoContext, + d.keyStore, + actorObj, car, replication.DealConfig{ Provider: schedule.Provider, @@ -455,13 +460,18 @@ func NewDealPusher(db *gorm.DB, lotusURL string, if err != nil { return nil, errors.Wrap(err, "failed to init host") } - lotusClient := util.NewLotusClient(lotusURL, lotusToken) - dealMaker := replication.NewDealMaker(lotusClient, h, time.Hour, time.Minute) + + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) if err != nil { - return nil, errors.Wrap(err, "failed to init deal maker") + return nil, errors.Wrap(err, "failed to init keystore") } + + lotusClient := util.NewLotusClient(lotusURL, lotusToken) + dealMaker := replication.NewDealMaker(lotusClient, h, time.Hour, time.Minute) + return &DealPusher{ dbNoContext: db, + keyStore: ks, activeScheduleCancelFunc: make(map[model.ScheduleID]context.CancelFunc), activeSchedule: make(map[model.ScheduleID]*model.Schedule), cronEntries: make(map[model.ScheduleID]cron.EntryID), diff --git a/testdb/main.go b/testdb/main.go index 840bbb7e..56700f3d 100644 --- a/testdb/main.go +++ b/testdb/main.go @@ -342,7 +342,7 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Price: "0", Verified: true, ScheduleID: ptr.Of(schedule.ID), - ClientActorID: actor.ID, + ClientID: actor.ID, } if state == model.DealActive { //nolint:gosec // G115: Safe conversion, max int32 epoch won't occur until year 4062 From d8bca701ce03e6f3cdefe8a77258ed8dd7bfd729 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Mon, 10 Nov 2025 15:04:34 +0100 Subject: [PATCH 04/15] fix actor/wallet references in tests --- api/api_test.go | 7 ++-- cmd/dataprep_test.go | 4 +-- cmd/wallet_test.go | 15 ++++---- handler/dataprep/delete_piece_test.go | 4 +-- handler/deal/list_test.go | 2 +- handler/deal/schedule/create_test.go | 8 ++--- handler/deal/schedule/list_test.go | 2 +- handler/deal/schedule/pause_test.go | 2 +- handler/deal/schedule/remove_test.go | 4 +-- handler/deal/schedule/resume_test.go | 2 +- handler/deal/send-manual_test.go | 25 ++++++------- handler/file/deals_test.go | 6 ++-- handler/file/retrieve_test.go | 8 ++--- handler/wallet/attach_test.go | 6 ++-- handler/wallet/detach_test.go | 6 ++-- handler/wallet/import_test.go | 47 +++---------------------- handler/wallet/listattached_test.go | 6 ++-- handler/wallet/remove.go | 18 +++++++--- handler/wallet/remove_test.go | 2 +- replication/makedeal_test.go | 6 ++-- replication/wallet_test.go | 10 +++--- service/dealpusher/dealpusher_test.go | 19 +++++----- service/dealtracker/dealtracker_test.go | 2 +- 23 files changed, 92 insertions(+), 119 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 9e96a4c4..5e5d9fab 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -34,6 +34,7 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/gotidy/ptr" @@ -50,8 +51,8 @@ type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, walletObj, car, dealConfig) +func (m *MockDealMaker) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { + args := m.Called(ctx, actorObj, car, dealConfig) return args.Get(0).(*model.Deal), args.Error(1) } @@ -191,7 +192,7 @@ func setupMockWallet() wallet.Handler { m.On("ListHandler", mock.Anything, mock.Anything). Return([]model.Wallet{{}}, nil) m.On("ListAttachedHandler", mock.Anything, mock.Anything, "id"). - Return([]model.Wallet{{}}, nil) + Return([]model.Actor{{}}, nil) m.On("RemoveHandler", mock.Anything, mock.Anything, "wallet"). Return(nil) return m diff --git a/cmd/dataprep_test.go b/cmd/dataprep_test.go index 85db643a..2d5bb49e 100644 --- a/cmd/dataprep_test.go +++ b/cmd/dataprep_test.go @@ -23,7 +23,7 @@ var testPreparation = model.Preparation{ DeleteAfterExport: false, MaxSize: 100, PieceSize: 200, - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "client_id", Address: "client_address", PrivateKey: "private_key", @@ -225,7 +225,7 @@ func TestDataPreparationListAttachedWalletHandler(t *testing.T) { mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("ListAttachedHandler", mock.Anything, mock.Anything, mock.Anything).Return(testPreparation.Wallets, nil) + mockHandler.On("ListAttachedHandler", mock.Anything, mock.Anything, mock.Anything).Return(testPreparation.Actors, nil) _, _, err := runner.Run(ctx, "singularity prep list-wallets 1") require.NoError(t, err) diff --git a/cmd/wallet_test.go b/cmd/wallet_test.go index ff2892cd..36d0cfff 100644 --- a/cmd/wallet_test.go +++ b/cmd/wallet_test.go @@ -33,9 +33,8 @@ func TestWalletImport(t *testing.T) { mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() mockHandler.On("ImportHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ - ID: "id", - Address: "address", - PrivateKey: "private", + ID: 1, + Address: "address", }, nil) _, _, err = runner.Run(ctx, "singularity wallet import "+testutil.EscapePath(filepath.Join(tmp, "private"))) require.NoError(t, err) @@ -51,13 +50,11 @@ func TestWalletList(t *testing.T) { mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() mockHandler.On("ListHandler", mock.Anything, mock.Anything).Return([]model.Wallet{{ - ID: "id1", - Address: "address1", - PrivateKey: "private1", + ID: 1, + Address: "address1", }, { - ID: "id2", - Address: "address2", - PrivateKey: "private2", + ID: 2, + Address: "address2", }}, nil) _, _, err := runner.Run(ctx, "singularity wallet list") require.NoError(t, err) diff --git a/handler/dataprep/delete_piece_test.go b/handler/dataprep/delete_piece_test.go index 72d16bbe..5fbad85c 100644 --- a/handler/dataprep/delete_piece_test.go +++ b/handler/dataprep/delete_piece_test.go @@ -66,7 +66,7 @@ func TestDeletePieceHandler_DealExistsWithoutForce(t *testing.T) { require.NoError(t, db.Create(&car).Error) // Create a wallet first to satisfy FK constraint - wallet := model.Wallet{ID: "f01234", Address: "f01234"} + wallet := model.Actor{ID: "f01234", Address: "f01234"} require.NoError(t, db.Create(&wallet).Error) deal := model.Deal{ @@ -96,7 +96,7 @@ func TestDeletePieceHandler_DealExistsWithForce(t *testing.T) { require.NoError(t, db.Create(&car).Error) // Create a wallet first to satisfy FK constraint - wallet := model.Wallet{ID: "f01234", Address: "f01234"} + wallet := model.Actor{ID: "f01234", Address: "f01234"} require.NoError(t, db.Create(&wallet).Error) deal := model.Deal{ diff --git a/handler/deal/list_test.go b/handler/deal/list_test.go index 96005ebf..17a5c568 100644 --- a/handler/deal/list_test.go +++ b/handler/deal/list_test.go @@ -13,7 +13,7 @@ import ( func TestListHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, SourceStorages: []model.Storage{{ diff --git a/handler/deal/schedule/create_test.go b/handler/deal/schedule/create_test.go index 4e1da85f..32059465 100644 --- a/handler/deal/schedule/create_test.go +++ b/handler/deal/schedule/create_test.go @@ -189,7 +189,7 @@ func TestCreateHandler_NoAssociatedWallet(t *testing.T) { func TestCreateHandler_InvalidProvider(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error @@ -206,7 +206,7 @@ func TestCreateHandler_InvalidProvider(t *testing.T) { func TestCreateHandler_DealSizeNotSetForCron(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error @@ -226,7 +226,7 @@ func TestCreateHandler_DealSizeNotSetForCron(t *testing.T) { func TestCreateHandler_ScheduleDealSizeSetForNonCron(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error @@ -247,7 +247,7 @@ func TestCreateHandler_Success(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ Name: "name", - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error diff --git a/handler/deal/schedule/list_test.go b/handler/deal/schedule/list_test.go index e0c906f2..035d012d 100644 --- a/handler/deal/schedule/list_test.go +++ b/handler/deal/schedule/list_test.go @@ -13,7 +13,7 @@ import ( func TestListHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error diff --git a/handler/deal/schedule/pause_test.go b/handler/deal/schedule/pause_test.go index 17090f82..ea8e5625 100644 --- a/handler/deal/schedule/pause_test.go +++ b/handler/deal/schedule/pause_test.go @@ -14,7 +14,7 @@ import ( func TestPauseHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error diff --git a/handler/deal/schedule/remove_test.go b/handler/deal/schedule/remove_test.go index afd825d3..82721085 100644 --- a/handler/deal/schedule/remove_test.go +++ b/handler/deal/schedule/remove_test.go @@ -15,7 +15,7 @@ import ( func TestRemoveSchedule_Success(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error @@ -56,7 +56,7 @@ func TestRemoveSchedule_NotExist(t *testing.T) { func TestRemoveSchedule_StillActive(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error diff --git a/handler/deal/schedule/resume_test.go b/handler/deal/schedule/resume_test.go index 85d87b95..daecdc31 100644 --- a/handler/deal/schedule/resume_test.go +++ b/handler/deal/schedule/resume_test.go @@ -14,7 +14,7 @@ import ( func TestResumeHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "f01", }}, }).Error diff --git a/handler/deal/send-manual_test.go b/handler/deal/send-manual_test.go index bc911932..9f86d799 100644 --- a/handler/deal/send-manual_test.go +++ b/handler/deal/send-manual_test.go @@ -20,7 +20,7 @@ type MockDealMaker struct { } func (m *MockDealMaker) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, db, ks, walletObj, car, dealConfig) + args := m.Called(ctx, db, ks, actorObj, car, dealConfig) return args.Get(0).(*model.Deal), args.Error(1) } @@ -50,7 +50,7 @@ func TestSendManualHandler_WalletNotFound(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, proposal) @@ -69,7 +69,7 @@ func TestSendManualHandler_InvalidPieceCID(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal @@ -90,7 +90,7 @@ func TestSendManualHandler_InvalidPieceCID_NOTCOMMP(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal @@ -111,7 +111,7 @@ func TestSendManualHandler_InvalidPieceSize(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal @@ -132,7 +132,7 @@ func TestSendManualHandler_InvalidPieceSize_NotPowerOfTwo(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal @@ -153,7 +153,7 @@ func TestSendManualHandler_InvalidRootCID(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal @@ -174,7 +174,7 @@ func TestSendManualHandler_InvalidDuration(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal @@ -195,7 +195,7 @@ func TestSendManualHandler_InvalidStartDelay(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - mockKS := keystore.NewMemoryKeyStore() + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal @@ -207,17 +207,18 @@ func TestSendManualHandler_InvalidStartDelay(t *testing.T) { } func TestSendManualHandler(t *testing.T) { - wallet := model.Wallet{ + actor := model.Actor{ ID: "f01000", Address: "f10000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error + err := db.Create(&actor).Error require.NoError(t, err) + var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, replication.DealConfig{ + mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, replication.DealConfig{ Provider: proposal.ProviderID, StartDelay: 24 * time.Hour, Duration: 2400 * time.Hour, diff --git a/handler/file/deals_test.go b/handler/file/deals_test.go index 63895340..84ea2c11 100644 --- a/handler/file/deals_test.go +++ b/handler/file/deals_test.go @@ -77,13 +77,13 @@ func TestGetFileDealsHandler(t *testing.T) { deals := []model.Deal{{ PieceCID: model.CID(testCid1), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, }, { PieceCID: model.CID(testCid2), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, }, { PieceCID: model.CID(testCid2), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, }} err = db.Create(deals).Error require.NoError(t, err) diff --git a/handler/file/retrieve_test.go b/handler/file/retrieve_test.go index f214fa81..8114b50c 100644 --- a/handler/file/retrieve_test.go +++ b/handler/file/retrieve_test.go @@ -144,7 +144,7 @@ func TestRetrieveFileHandler(t *testing.T) { State: model.DealActive, PieceCID: model.CID(testCid), Provider: "apples" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(t, err) @@ -158,7 +158,7 @@ func TestRetrieveFileHandler(t *testing.T) { State: state, PieceCID: model.CID(testCid), Provider: "oranges" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(t, err) @@ -489,7 +489,7 @@ func BenchmarkFilecoinRetrieve(b *testing.B) { State: model.DealActive, PieceCID: model.CID(testCid), Provider: "apples" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(b, err) @@ -502,7 +502,7 @@ func BenchmarkFilecoinRetrieve(b *testing.B) { State: state, PieceCID: model.CID(testCid), Provider: "oranges" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(b, err) diff --git a/handler/wallet/attach_test.go b/handler/wallet/attach_test.go index 1a0ee203..90946f87 100644 --- a/handler/wallet/attach_test.go +++ b/handler/wallet/attach_test.go @@ -13,7 +13,7 @@ import ( func TestAttachHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Wallet{ + err := db.Create(&model.Actor{ ID: "test", }).Error require.NoError(t, err) @@ -25,7 +25,7 @@ func TestAttachHandler(t *testing.T) { require.ErrorIs(t, err, handlererror.ErrNotFound) }) - t.Run("wallet not found", func(t *testing.T) { + t.Run("actor not found", func(t *testing.T) { _, err := Default.AttachHandler(ctx, db, "1", "invalid") require.ErrorIs(t, err, handlererror.ErrNotFound) }) @@ -33,7 +33,7 @@ func TestAttachHandler(t *testing.T) { t.Run("success", func(t *testing.T) { preparation, err := Default.AttachHandler(ctx, db, "1", "test") require.NoError(t, err) - require.Len(t, preparation.Wallets, 1) + require.Len(t, preparation.Actors, 1) }) }) } diff --git a/handler/wallet/detach_test.go b/handler/wallet/detach_test.go index 268da587..90a6df65 100644 --- a/handler/wallet/detach_test.go +++ b/handler/wallet/detach_test.go @@ -14,7 +14,7 @@ import ( func TestDetachHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "test", }}, }).Error @@ -25,7 +25,7 @@ func TestDetachHandler(t *testing.T) { require.ErrorIs(t, err, handlererror.ErrNotFound) }) - t.Run("wallet not found", func(t *testing.T) { + t.Run("actor not found", func(t *testing.T) { _, err := Default.DetachHandler(ctx, db, "1", "invalid") require.ErrorIs(t, err, handlererror.ErrNotFound) }) @@ -33,7 +33,7 @@ func TestDetachHandler(t *testing.T) { t.Run("success", func(t *testing.T) { preparation, err := Default.DetachHandler(ctx, db, "1", "test") require.NoError(t, err) - require.Len(t, preparation.Wallets, 0) + require.Len(t, preparation.Actors, 0) }) }) } diff --git a/handler/wallet/import_test.go b/handler/wallet/import_test.go index 614de4d8..e49030af 100644 --- a/handler/wallet/import_test.go +++ b/handler/wallet/import_test.go @@ -1,50 +1,13 @@ package wallet import ( - "context" "testing" - "time" - - "github.com/data-preservation-programs/singularity/handler/handlererror" - "github.com/data-preservation-programs/singularity/util" - "github.com/data-preservation-programs/singularity/util/testutil" - "github.com/stretchr/testify/require" - "gorm.io/gorm" ) +// TODO: ImportHandler was removed as part of wallet/actor separation (#590) +// This test needs to be replaced with tests for ImportKeystoreHandler which uses +// the new keystore-based approach instead of storing private keys in the database. +// See handler/wallet/import_keystore.go for the new implementation. func TestImportHandler(t *testing.T) { - testutil.SkipIfNotExternalAPI(t) - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - lotusClient := util.NewLotusClient(testutil.TestLotusAPI, "") - - t.Run("success", func(t *testing.T) { - w, err := Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: testutil.TestPrivateKeyHex, - }) - require.NoError(t, err) - require.Equal(t, testutil.TestWalletAddr, w.Address) - - _, err = Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: testutil.TestPrivateKeyHex, - }) - require.ErrorIs(t, err, handlererror.ErrDuplicateRecord) - }) - - t.Run("invalid key", func(t *testing.T) { - _, err := Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: "xxxx", - }) - require.ErrorIs(t, err, handlererror.ErrInvalidParameter) - }) - - t.Run("invalid response", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - lotusClient := util.NewLotusClient("http://127.0.0.1", "") - _, err := Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: testutil.TestPrivateKeyHex, - }) - require.ErrorIs(t, err, handlererror.ErrInvalidParameter) - }) - }) + t.Skip("ImportHandler removed - needs replacement with ImportKeystoreHandler tests") } diff --git a/handler/wallet/listattached_test.go b/handler/wallet/listattached_test.go index 26fb17fc..89de17d6 100644 --- a/handler/wallet/listattached_test.go +++ b/handler/wallet/listattached_test.go @@ -14,7 +14,7 @@ import ( func TestListAttachedHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ + Actors: []model.Actor{{ ID: "test", }}, }).Error @@ -26,9 +26,9 @@ func TestListAttachedHandler(t *testing.T) { }) t.Run("success", func(t *testing.T) { - wallets, err := Default.ListAttachedHandler(ctx, db, "1") + actors, err := Default.ListAttachedHandler(ctx, db, "1") require.NoError(t, err) - require.Len(t, wallets, 1) + require.Len(t, actors, 1) }) }) } diff --git a/handler/wallet/remove.go b/handler/wallet/remove.go index cb5ecbfd..cab2d89a 100644 --- a/handler/wallet/remove.go +++ b/handler/wallet/remove.go @@ -10,12 +10,22 @@ import ( "gorm.io/gorm" ) -// RemoveHandler deletes a wallet from the database based on its address or ID. +// TODO(#590): Clarify semantics of wallet remove after wallet/actor separation +// Before separation: removed Wallet (which contained both key and actor ID) +// After separation: Wallet = keystore entry, Actor = on-chain identity +// Should this: +// - Remove Wallet record only (keystore reference)? +// - Remove Actor record only (stop tracking deals)? +// - Remove both? +// - Delete the actual keystore file? +// Currently: Removes Actor record (temporary fix to match test expectations) +// +// RemoveHandler deletes an actor from the database based on its address or ID. // // Parameters: // - ctx: The context for database transactions and other operations. // - db: A pointer to the gorm.DB instance representing the database connection. -// - address: The address or ID of the wallet to be deleted. +// - address: The address or ID of the actor to be deleted. // // Returns: // - An error, if any occurred during the database deletion operation. @@ -27,7 +37,7 @@ func (DefaultHandler) RemoveHandler( db = db.WithContext(ctx) var affected int64 err := database.DoRetry(ctx, func() error { - tx := db.Where("address = ? OR id = ?", address, address).Delete(&model.Wallet{}) + tx := db.Where("address = ? OR id = ?", address, address).Delete(&model.Actor{}) affected = tx.RowsAffected return tx.Error }) @@ -35,7 +45,7 @@ func (DefaultHandler) RemoveHandler( return errors.WithStack(err) } if affected == 0 { - return errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", address) + return errors.Wrapf(handlererror.ErrNotFound, "actor %s not found", address) } return nil } diff --git a/handler/wallet/remove_test.go b/handler/wallet/remove_test.go index dd52ecc0..bf6b1580 100644 --- a/handler/wallet/remove_test.go +++ b/handler/wallet/remove_test.go @@ -14,7 +14,7 @@ import ( func TestRemoveHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { t.Run("success", func(t *testing.T) { - err := db.Create(&model.Wallet{ + err := db.Create(&model.Actor{ ID: "test", }).Error require.NoError(t, err) diff --git a/replication/makedeal_test.go b/replication/makedeal_test.go index b1496c2f..4bff1308 100644 --- a/replication/makedeal_test.go +++ b/replication/makedeal_test.go @@ -115,7 +115,7 @@ func TestDealMaker_MakeDeal(t *testing.T) { defer client.Close() maker := NewDealMaker(nil, client, time.Hour, time.Second) defer maker.Close() - wallet := model.Wallet{ + wallet := model.Actor{ ID: "f047684", Address: addr, PrivateKey: key, @@ -154,14 +154,14 @@ func TestDealMaker_MakeDeal(t *testing.T) { StorageProposalV120, }, ttlcache.DefaultTTL) - _, err = maker.MakeDeal(ctx, wallet, car, dealConfig) + _, err = maker.MakeDeal(ctx, nil, nil, wallet, car, dealConfig) require.NoError(t, err) maker.protocolsCache.Set(server.ID(), []protocol.ID{ StorageProposalV111, }, ttlcache.DefaultTTL) - _, err = maker.MakeDeal(ctx, wallet, car, dealConfig) + _, err = maker.MakeDeal(ctx, nil, nil, wallet, car, dealConfig) require.NoError(t, err) } diff --git a/replication/wallet_test.go b/replication/wallet_test.go index cc41d3d9..55af81b5 100644 --- a/replication/wallet_test.go +++ b/replication/wallet_test.go @@ -47,7 +47,7 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { lotusClient := new(MockRPCClient) // Set up the test data - wallets := []model.Wallet{ + wallets := []model.Actor{ {ID: "1", Address: "address1"}, {ID: "2", Address: "address2"}, {ID: "3", Address: "address3"}, @@ -90,18 +90,18 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { require.NoError(t, err) t.Run("Choose wallet with empty wallet", func(t *testing.T) { - _, err := chooser.Choose(context.Background(), []model.Wallet{}) + _, err := chooser.Choose(context.Background(), []model.Actor{}) require.ErrorAs(t, err, &ErrNoWallet) }) t.Run("Choose wallet with sufficient datacap", func(t *testing.T) { - chosenWallet, err := chooser.Choose(context.Background(), []model.Wallet{wallets[0], wallets[1]}) + chosenWallet, err := chooser.Choose(context.Background(), []model.Actor{wallets[0], wallets[1]}) require.NoError(t, err) require.Equal(t, "address1", chosenWallet.Address) }) t.Run("Choose wallet with insufficient datacap", func(t *testing.T) { - _, err := chooser.Choose(context.Background(), []model.Wallet{wallets[2], wallets[3]}) + _, err := chooser.Choose(context.Background(), []model.Actor{wallets[2], wallets[3]}) require.ErrorAs(t, err, &ErrNoDatacap) }) }) @@ -110,7 +110,7 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { func TestRandomWalletChooser(t *testing.T) { chooser := &RandomWalletChooser{} ctx := context.Background() - wallet, err := chooser.Choose(ctx, []model.Wallet{ + wallet, err := chooser.Choose(ctx, []model.Actor{ {ID: "1", Address: "address1"}, {ID: "2", Address: "address2"}, }) diff --git a/service/dealpusher/dealpusher_test.go b/service/dealpusher/dealpusher_test.go index 84202535..5b1f8a79 100644 --- a/service/dealpusher/dealpusher_test.go +++ b/service/dealpusher/dealpusher_test.go @@ -13,6 +13,7 @@ import ( "github.com/data-preservation-programs/singularity/pack" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service/epochutil" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" commp "github.com/filecoin-project/go-fil-commp-hashhash" "github.com/google/uuid" @@ -32,8 +33,8 @@ type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, walletObj, car, dealConfig) +func (m *MockDealMaker) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { + args := m.Called(ctx, actorObj, car, dealConfig) if args.Get(0) == nil { return nil, args.Error(1) } @@ -41,7 +42,7 @@ func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, ca deal.ID = 0 deal.PieceCID = car.PieceCID deal.PieceSize = car.PieceSize - deal.ClientID = walletObj.ID + deal.ClientID = actorObj.ID deal.Provider = dealConfig.Provider deal.Verified = dealConfig.Verified deal.ProposalID = uuid.NewString() @@ -110,7 +111,7 @@ func TestDealMakerService_FailtoSend(t *testing.T) { schedule := model.Schedule{ Preparation: &model.Preparation{ SourceStorages: []model.Storage{{}}, - Wallets: []model.Wallet{ + Actors: []model.Actor{ { ID: client, Address: "f0xx", }, @@ -166,7 +167,7 @@ func TestDealMakerService_Cron(t *testing.T) { schedule := model.Schedule{ Preparation: &model.Preparation{ SourceStorages: []model.Storage{{}}, - Wallets: []model.Wallet{ + Actors: []model.Actor{ { ID: client, Address: "f0xx", }, @@ -261,7 +262,7 @@ func TestDealMakerService_ScheduleWithConstraints(t *testing.T) { schedule := model.Schedule{ Preparation: &model.Preparation{ SourceStorages: []model.Storage{{}}, - Wallets: []model.Wallet{ + Actors: []model.Actor{ { ID: client, Address: "f0xx", }, @@ -370,7 +371,7 @@ func TestDealmakerService_Force(t *testing.T) { client := "f0client" schedule := model.Schedule{ Preparation: &model.Preparation{ - Wallets: []model.Wallet{ + Actors: []model.Actor{ { ID: client, Address: "f0xx", }, @@ -429,7 +430,7 @@ func TestDealMakerService_MaxReplica(t *testing.T) { client := "f0client" schedule := model.Schedule{ Preparation: &model.Preparation{ - Wallets: []model.Wallet{ + Actors: []model.Actor{ { ID: client, Address: "f0xx", }, @@ -495,7 +496,7 @@ func TestDealMakerService_NewScheduleOneOff(t *testing.T) { client := "f0client" schedule := model.Schedule{ Preparation: &model.Preparation{ - Wallets: []model.Wallet{ + Actors: []model.Actor{ { ID: client, Address: "f0xx", }, diff --git a/service/dealtracker/dealtracker_test.go b/service/dealtracker/dealtracker_test.go index fd688810..a896d425 100644 --- a/service/dealtracker/dealtracker_test.go +++ b/service/dealtracker/dealtracker_test.go @@ -152,7 +152,7 @@ func TestTrackDeal(t *testing.T) { func TestRunOnce(t *testing.T) { testutil.SkipIfNotExternalAPI(t) testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Wallet{ + err := db.Create(&model.Actor{ ID: "t0100", Address: "t3xxx", }).Error From 60187f7a9f491df43b3711a4edf844fc14448e48 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Fri, 20 Feb 2026 18:29:10 +0100 Subject: [PATCH 05/15] wire go-synapse signer into keystore, drop go-filsigner --- go.mod | 10 ++-- go.sum | 84 ++----------------------------- handler/wallet/import_keystore.go | 14 ++---- handler/wallet/sign.go | 11 ++-- util/keystore/keystore.go | 18 +++++-- util/keystore/signer.go | 37 ++++++++++++++ 6 files changed, 64 insertions(+), 110 deletions(-) create mode 100644 util/keystore/signer.go diff --git a/go.mod b/go.mod index 63dfea19..8eedd7cb 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/bcicen/jstream v1.0.1 github.com/brianvoe/gofakeit/v6 v6.23.2 github.com/cockroachdb/errors v1.11.3 - github.com/data-preservation-programs/go-synapse v0.0.0-20260206105716-b6a5e7e6808e + github.com/data-preservation-programs/go-synapse v0.0.0-20260220134035-b27d67ac9095 github.com/data-preservation-programs/table v0.0.3 github.com/dustin/go-humanize v1.0.1 github.com/ethereum/go-ethereum v1.14.12 @@ -52,7 +52,6 @@ require ( github.com/jackc/pgx/v5 v5.7.6 github.com/jellydator/ttlcache/v3 v3.0.1 github.com/joho/godotenv v1.5.1 - github.com/jsign/go-filsigner v0.4.1 github.com/klauspost/compress v1.18.1 github.com/labstack/echo/v4 v4.10.2 github.com/libp2p/go-libp2p v0.44.0 @@ -152,15 +151,12 @@ require ( github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect - github.com/dchest/blake2b v1.0.0 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/drand/kyber v1.3.1 // indirect - github.com/drand/kyber-bls12381 v0.3.3 // indirect github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 // indirect github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 // indirect github.com/emersion/go-message v0.18.2 // indirect @@ -259,7 +255,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7 // indirect github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect - github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988 // indirect github.com/koofr/go-koofrclient v0.0.0-20221207135200-cbd7fc9ad6a6 // indirect @@ -285,6 +280,7 @@ require ( github.com/miekg/dns v1.1.68 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -359,7 +355,7 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/supranational/blst v0.3.13 // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect diff --git a/go.sum b/go.sum index 045fe540..7bd85a34 100644 --- a/go.sum +++ b/go.sum @@ -251,8 +251,8 @@ github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+ github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= -github.com/data-preservation-programs/go-synapse v0.0.0-20260206105716-b6a5e7e6808e h1:xqTd8FN2XAxKVwbkNmkSeemTFkuGp6eDMTvTDhumCxQ= -github.com/data-preservation-programs/go-synapse v0.0.0-20260206105716-b6a5e7e6808e/go.mod h1:5pXdfN2ywCsZK5gbhtmR0Nv6ttEAeUgHIq1gW3QCMPg= +github.com/data-preservation-programs/go-synapse v0.0.0-20260220134035-b27d67ac9095 h1:pFMqjtl0phxSg6L3EKkD6OGDwUha7ReterF09v2f184= +github.com/data-preservation-programs/go-synapse v0.0.0-20260220134035-b27d67ac9095/go.mod h1:qgzPsiGWjTPT/oACA6Uj1+WsASwsYFW/iJ8AWacJdjc= github.com/data-preservation-programs/table v0.0.3 h1:hboeauxPXybE8KlMA+RjDXz/J4xaG5CAFCcxyOm8yWo= github.com/data-preservation-programs/table v0.0.3/go.mod h1:sRGP/IuuqFc/y9QfmDyb5h6Q2wrnhhnBofEOj9aDRJg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -261,15 +261,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= -github.com/dchest/blake2b v1.0.0 h1:KK9LimVmE0MjRl9095XJmKqZ+iLxWATvlcpVFRtaw6s= -github.com/dchest/blake2b v1.0.0/go.mod h1:U034kXgbJpCle2wSk5ybGIVhOSHCVLMDqOzcPEA0F7s= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210507181900-4e0be8d2fbb4/go.mod h1:UkVqoxmJlLgUvBjJD+GdJz6mgdSdf3UjX83xfwUAYDk= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -280,16 +275,6 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/drand/bls12-381 v0.3.2/go.mod h1:dtcLgPtYT38L3NO6mPDYH0nbpc5tjPassDqiniuAt4Y= -github.com/drand/kyber v1.0.1-0.20200110225416-8de27ed8c0e2/go.mod h1:UpXoA0Upd1N9l4TvRPHr1qAUBBERj6JQ/mnKI3BPEmw= -github.com/drand/kyber v1.0.2/go.mod h1:x6KOpK7avKj0GJ4emhXFP5n7M7W7ChAPmnQh/OL6vRw= -github.com/drand/kyber v1.1.4/go.mod h1:9+IgTq7kadePhZg7eRwSD7+bA+bmvqRK+8DtmoV5a3U= -github.com/drand/kyber v1.3.1 h1:E0p6M3II+loMVwTlAp5zu4+GGZFNiRfq02qZxzw2T+Y= -github.com/drand/kyber v1.3.1/go.mod h1:f+mNHjiGT++CuueBrpeMhFNdKZAsy0tu03bKq9D5LPA= -github.com/drand/kyber-bls12381 v0.2.0/go.mod h1:zQip/bHdeEB6HFZSU3v+d3cQE0GaBVQw9aR2E7AdoeI= -github.com/drand/kyber-bls12381 v0.2.1/go.mod h1:JwWn4nHO9Mp4F5qCie5sVIPQZ0X6cw8XAeMRvc/GXBE= -github.com/drand/kyber-bls12381 v0.3.3 h1:sLl0ILJtB4+POHAKq6tdnWyg+iXADE0LjVKN91RI8JI= -github.com/drand/kyber-bls12381 v0.3.3/go.mod h1:uVRWtcZDAApOWFMwoJVcTfC4csVxXmpkdoSCUZJ5QOY= github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXIiWJETBpRgERYTGlmMd7HU= github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M= github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= @@ -320,12 +305,9 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/filecoin-project/dagstore v0.5.2 h1:Nd6oXdnolbbVhpMpkYT5PJHOjQp4OBSntHpMV5pxj3c= github.com/filecoin-project/dagstore v0.5.2/go.mod h1:mdqKzYrRBHf1pRMthYfMv3n37oOw0Tkx7+TxPt240M0= -github.com/filecoin-project/filecoin-ffi v0.30.4-0.20200910194244-f640612a1a1f/go.mod h1:+If3s2VxyjZn+KGGZIoRXBDSFQ9xL404JBJGf4WhEj0= github.com/filecoin-project/filecoin-ffi v1.34.0 h1:OvcsvsFUCwzLOGT949dsJEqSLyGx4d8TPPRrmrzlQbk= github.com/filecoin-project/filecoin-ffi v1.34.0/go.mod h1:AXLJk1PscWAwEa9CdqdiFwj1ttVJ+UIm8YQDPpTqBjg= -github.com/filecoin-project/go-address v0.0.3/go.mod h1:jr8JxKsYx+lQlQZmF5i2U0Z+cGQ59wMIps/8YW/lDj8= github.com/filecoin-project/go-address v0.0.5/go.mod h1:jr8JxKsYx+lQlQZmF5i2U0Z+cGQ59wMIps/8YW/lDj8= -github.com/filecoin-project/go-address v1.1.0/go.mod h1:5t3z6qPmIADZBtuE9EIzi0EwzcRy2nVhpo0I/c1r0OA= github.com/filecoin-project/go-address v1.2.0 h1:NHmWUE/J7Pi2JZX3gZt32XuY69o9StVZeJxdBodIwOE= github.com/filecoin-project/go-address v1.2.0/go.mod h1:kQEQ4qZ99a51X7DjT9HiMT4yR6UwLJ9kznlxsOIeDAg= github.com/filecoin-project/go-amt-ipld/v2 v2.1.0 h1:t6qDiuGYYngDqaLc2ZUvdtAg4UNxPeOYaXhBWSNsVaM= @@ -333,7 +315,6 @@ github.com/filecoin-project/go-amt-ipld/v2 v2.1.0/go.mod h1:nfFPoGyX0CU9SkXX8EoC github.com/filecoin-project/go-amt-ipld/v4 v4.0.0/go.mod h1:gF053YQ4BIpzTNDoEwHZas7U3oAwncDVGvOHyY8oDpE= github.com/filecoin-project/go-amt-ipld/v4 v4.4.0 h1:6kvvMeSpIy4GTU5t3vPHZgWYIMRzGRKLJ73s/cltsoc= github.com/filecoin-project/go-amt-ipld/v4 v4.4.0/go.mod h1:msgmUxTyRBZ6iXt+5dnUDnIb7SEFqdPsbB1wyo/G3ts= -github.com/filecoin-project/go-bitfield v0.2.0/go.mod h1:CNl9WG8hgR5mttCnUErjcQjGvuiZjRqK9rHVBsQF4oM= github.com/filecoin-project/go-bitfield v0.2.4 h1:uZ7MeE+XfM5lqrHJZ93OnhQKc/rveW8p9au0C68JPgk= github.com/filecoin-project/go-bitfield v0.2.4/go.mod h1:CNl9WG8hgR5mttCnUErjcQjGvuiZjRqK9rHVBsQF4oM= github.com/filecoin-project/go-cbor-util v0.0.0-20191219014500-08c40a1e63a2/go.mod h1:pqTiPHobNkOVM5thSRsHYjyQfq7O5QSCMhvuu9JoDlg= @@ -341,10 +322,8 @@ github.com/filecoin-project/go-cbor-util v0.0.2 h1:vljF+a+NBwv89VfPvy5lJEtrZWe8k github.com/filecoin-project/go-cbor-util v0.0.2/go.mod h1:96OIHk38Y1IV+KCXkGjE2WjjIxfpIanz2rWIIy5kKkQ= github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= -github.com/filecoin-project/go-commp-utils v0.1.3/go.mod h1:3ENlD1pZySaUout0p9ANQrY3fDFoXdqyX04J+dWpK30= github.com/filecoin-project/go-commp-utils v0.1.4 h1:/WSsrAb0xupo+aRWRyD80lRUXAXJvYoTgDQS1pYZ1Mk= github.com/filecoin-project/go-commp-utils v0.1.4/go.mod h1:Sekocu5q9b4ECAUFu853GFUbm8I7upAluummHFe2kFo= -github.com/filecoin-project/go-commp-utils/nonffi v0.0.0-20220905160352-62059082a837/go.mod h1:e2YBjSblNVoBckkbv3PPqsq71q98oFkFqL7s1etViGo= github.com/filecoin-project/go-commp-utils/v2 v2.1.0 h1:KWNRalUp2bhN1SW7STsJS2AHs9mnfGKk9LnQgzDe+gI= github.com/filecoin-project/go-commp-utils/v2 v2.1.0/go.mod h1:NbxJYlhxtWaNhlVCj/gysLNu26kYII83IV5iNrAO9iI= github.com/filecoin-project/go-crypto v0.0.0-20191218222705-effae4ea9f03/go.mod h1:+viYnvGtUTgJRdy6oaeF4MTFKAfatX071MPDPBL11EQ= @@ -354,9 +333,6 @@ github.com/filecoin-project/go-data-transfer/v2 v2.0.0-rc8 h1:EWC89lM/tJAjyzaxZ6 github.com/filecoin-project/go-data-transfer/v2 v2.0.0-rc8/go.mod h1:mK3/NbSljx3Kr335+IXEe8gcdEPA2eZXJaNhodK9bAI= github.com/filecoin-project/go-ds-versioning v0.1.2 h1:to4pTadv3IeV1wvgbCbN6Vqd+fu+7tveXgv/rCEZy6w= github.com/filecoin-project/go-ds-versioning v0.1.2/go.mod h1:C9/l9PnB1+mwPa26BBVpCjG/XQCB0yj/q5CK2J8X1I4= -github.com/filecoin-project/go-fil-commcid v0.0.0-20200716160307-8f644712406f/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= -github.com/filecoin-project/go-fil-commcid v0.0.0-20201016201715-d41df56b4f6a/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= -github.com/filecoin-project/go-fil-commcid v0.1.0/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= github.com/filecoin-project/go-fil-commcid v0.2.0 h1:B+5UX8XGgdg/XsdUpST4pEBviKkFOw+Fvl2bLhSKGpI= github.com/filecoin-project/go-fil-commcid v0.2.0/go.mod h1:8yigf3JDIil+/WpqR5zoKyP0jBPCOGtEqq/K1CcMy9Q= github.com/filecoin-project/go-fil-commp-hashhash v0.2.1-0.20230811065821-2e9c683db589 h1:PP5FU5JVVDb7zODWZlgzbdmQDtwu3Mm0bK9Bg/Om5yc= @@ -368,17 +344,12 @@ github.com/filecoin-project/go-hamt-ipld v0.1.5/go.mod h1:6Is+ONR5Cd5R6XZoCse1CW github.com/filecoin-project/go-hamt-ipld/v3 v3.1.0/go.mod h1:bxmzgT8tmeVQA1/gvBwFmYdT8SOFUwB3ovSUfG1Ux0g= github.com/filecoin-project/go-hamt-ipld/v3 v3.4.1 h1:wl+ZHruCcE9LvwU7blpwWn35XOcRS6+IBg75G7ZzxzY= github.com/filecoin-project/go-hamt-ipld/v3 v3.4.1/go.mod h1:AqjryNfkxffpnqsa5mwnJHlazhVqF6W2nilu+VYKIq8= -github.com/filecoin-project/go-padreader v0.0.0-20200903213702-ed5fae088b20/go.mod h1:mPn+LRRd5gEKNAtc+r3ScpW2JRU/pj4NBKdADYWHiak= github.com/filecoin-project/go-padreader v0.0.1 h1:8h2tVy5HpoNbr2gBRr+WD6zV6VD6XHig+ynSGJg8ZOs= github.com/filecoin-project/go-padreader v0.0.1/go.mod h1:VYVPJqwpsfmtoHnAmPx6MUwmrK6HIcDqZJiuZhtmfLQ= github.com/filecoin-project/go-retrieval-types v1.2.0 h1:fz6DauLVP3GRg7UuW7HZ6sE+GTmaUW70DTXBF1r9cK0= github.com/filecoin-project/go-retrieval-types v1.2.0/go.mod h1:ojW6wSw2GPyoRDBGqw1K6JxUcbfa5NOSIiyQEeh7KK0= github.com/filecoin-project/go-state-types v0.0.0-20200903145444-247639ffa6ad/go.mod h1:IQ0MBPnonv35CJHtWSN3YY1Hz2gkPru1Q9qoaYLxx9I= -github.com/filecoin-project/go-state-types v0.0.0-20200904021452-1883f36ca2f4/go.mod h1:IQ0MBPnonv35CJHtWSN3YY1Hz2gkPru1Q9qoaYLxx9I= -github.com/filecoin-project/go-state-types v0.0.0-20201102161440-c8033295a1fc/go.mod h1:ezYnPf0bNkTsDibL/psSz5dy4B5awOJ/E7P2Saeep8g= github.com/filecoin-project/go-state-types v0.1.6/go.mod h1:UwGVoMsULoCK+bWjEdd/xLCvLAQFBC7EDT477SKml+Q= -github.com/filecoin-project/go-state-types v0.1.10/go.mod h1:UwGVoMsULoCK+bWjEdd/xLCvLAQFBC7EDT477SKml+Q= -github.com/filecoin-project/go-state-types v0.10.0/go.mod h1:aLIas+W8BWAfpLWEPUOGMPBdhcVwoCG4pIQSQk26024= github.com/filecoin-project/go-state-types v0.17.0 h1:HpBb6G+VSOOI6rQFSnvPVyRsnms8je94cwvU69DJ+9Y= github.com/filecoin-project/go-state-types v0.17.0/go.mod h1:em4yo9mglrdyHbcsxelHCSKMjLdJLddLERWQe6J8vYc= github.com/filecoin-project/go-statemachine v0.0.0-20200925024713-05bd7c71fbfe/go.mod h1:FGwQgZAt2Gh5mjlwJUlVB62JeYdo+if0xWxSEfBD9ig= @@ -387,7 +358,6 @@ github.com/filecoin-project/go-statemachine v1.0.3/go.mod h1:jZdXXiHa61n4NmgWFG4 github.com/filecoin-project/go-statestore v0.1.0/go.mod h1:LFc9hD+fRxPqiHiaqUEZOinUJB4WARkRfNl10O7kTnI= github.com/filecoin-project/go-statestore v0.2.0 h1:cRRO0aPLrxKQCZ2UOQbzFGn4WDNdofHZoGPjfNaAo5Q= github.com/filecoin-project/go-statestore v0.2.0/go.mod h1:8sjBYbS35HwPzct7iT4lIXjLlYyPor80aU7t7a/Kspo= -github.com/filecoin-project/specs-actors v0.9.4/go.mod h1:BStZQzx5x7TmCkLv0Bpa07U6cPKol6fd3w9KjMPZ6Z4= github.com/filecoin-project/specs-actors v0.9.15 h1:3VpKP5/KaDUHQKAMOg4s35g/syDaEBueKLws0vbsjMc= github.com/filecoin-project/specs-actors v0.9.15/go.mod h1:pjGEe3QlWtK20ju/aFRsiArbMX6Cn8rqEhhsiCM9xYE= github.com/filecoin-shipyard/boostly v0.0.0-20230813165216-a449c35ece79 h1:MdF/QWskzWeBRpnffuJk+qjQWhwJMLSyHpoj679Xsdo= @@ -683,7 +653,6 @@ github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67Fexh github.com/ipfs/go-cid v0.0.6-0.20200501230655-7c82f3b81c00/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= -github.com/ipfs/go-cid v0.2.0/go.mod h1:P+HXFDF4CVhaVayiEb4wkAy7zBHxBwsJyt0Y5U6MLro= github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= github.com/ipfs/go-cidutil v0.1.0 h1:RW5hO7Vcf16dplUU60Hs0AKDkQAVPVplr7lk97CFL+Q= @@ -699,7 +668,6 @@ github.com/ipfs/go-dsqueue v0.0.5 h1:TUOk15TlCJ/NKV8Yk2W5wgkEjDa44Nem7a7FGIjsMNU github.com/ipfs/go-dsqueue v0.0.5/go.mod h1:i/jAlpZjBbQJLioN+XKbFgnd+u9eAhGZs9IrqIzTd9g= github.com/ipfs/go-graphsync v0.18.0 h1:b+DNJ4lWsCKKaVKYgqgt4rrqshvVcTphN7Rl0JfFiD4= github.com/ipfs/go-graphsync v0.18.0/go.mod h1:+7SU0L6thSFFTo1pbDcwCqi+gN0L7UQP8eVm897Mg0s= -github.com/ipfs/go-hamt-ipld v0.1.1/go.mod h1:1EZCr2v0jlCnhpa+aZ0JZYp8Tt2w16+JJOAVz17YcDk= github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= github.com/ipfs/go-ipfs-blocksutil v0.0.2 h1:hoAV68CQOzUa/e1egCME3lbrsyEGO0pY7Bb26T+8/Zc= @@ -729,9 +697,7 @@ github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCm github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= github.com/ipfs/go-ipld-cbor v0.0.3/go.mod h1:wTBtrQZA3SoFKMVkp6cn6HMRteIB1VsmHA0AQFOn7Nc= github.com/ipfs/go-ipld-cbor v0.0.4/go.mod h1:BkCduEx3XBCO6t2Sfo5BaHzuok7hbhdMm9Oh8B2Ftq4= -github.com/ipfs/go-ipld-cbor v0.0.5/go.mod h1:BkCduEx3XBCO6t2Sfo5BaHzuok7hbhdMm9Oh8B2Ftq4= github.com/ipfs/go-ipld-cbor v0.0.6-0.20211211231443-5d9b9e1f6fa8/go.mod h1:ssdxxaLJPXH7OjF5V4NSjBbcfh+evoR4ukuru0oPXMA= -github.com/ipfs/go-ipld-cbor v0.0.6/go.mod h1:ssdxxaLJPXH7OjF5V4NSjBbcfh+evoR4ukuru0oPXMA= github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= github.com/ipfs/go-ipld-format v0.0.1/go.mod h1:kyJtbkDALmFHv3QR6et67i35QzO3S0dCDnkOJhcZkms= @@ -749,7 +715,6 @@ github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= github.com/ipfs/go-log/v2 v2.0.1/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0= github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= -github.com/ipfs/go-log/v2 v2.1.2-0.20200626104915-0016c0b4b3e4/go.mod h1:2v2nsGfZsvvAJz13SyFzf9ObaqwHiHxsPLEHntrv9KM= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.8.2 h1:nVG4nNHUwwI/sTs9Bi5iE8sXFQwXs3AjkkuWhg7+Y2I= github.com/ipfs/go-log/v2 v2.8.2/go.mod h1:UhIYAwMV7Nb4ZmihUxfIRM2Istw/y9cAk3xaK+4Zs2c= @@ -831,13 +796,10 @@ github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uT github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jsign/go-filsigner v0.4.1 h1:3OfSU851aMRmJCcgZrnepIEUJ1sfYV3/+lRVMspOEiA= -github.com/jsign/go-filsigner v0.4.1/go.mod h1:1vBLymj4qKJt1Ixkna83Peof9kjY/TA5uCJ/7167iuk= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -852,11 +814,6 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/kilic/bls12-381 v0.0.0-20200607163746-32e1441c8a9f/go.mod h1:XXfR6YFCRSrkEXbNlIyDsgXVNJWVUV30m/ebkVy9n6s= -github.com/kilic/bls12-381 v0.0.0-20200731194930-64c428e1bff5/go.mod h1:XXfR6YFCRSrkEXbNlIyDsgXVNJWVUV30m/ebkVy9n6s= -github.com/kilic/bls12-381 v0.0.0-20200820230200-6b2c19996391/go.mod h1:XXfR6YFCRSrkEXbNlIyDsgXVNJWVUV30m/ebkVy9n6s= -github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4 h1:xWK4TZ4bRL05WQUU/3x6TG1l+IYAqdXpAeSLt/zZJc4= -github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4/go.mod h1:tlkavyke+Ac7h8R3gZIjI5LKBcvMlSWnXNMgT3vZXo8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -864,7 +821,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -961,7 +917,6 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -1018,13 +973,11 @@ github.com/multiformats/go-multihash v0.0.9/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= -github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= -github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -1277,8 +1230,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= -github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/swaggo/echo-swagger v1.4.0 h1:RCxLKySw1SceHLqnmc41pKyiIeE+OiD7NSI7FUOBlLo= github.com/swaggo/echo-swagger v1.4.0/go.mod h1:Wh3VlwjZGZf/LH0s81tz916JokuPG7y/ZqaqnckYqoQ= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= @@ -1291,7 +1244,6 @@ github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c h1:BLopNCyqewbE8+ github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c/go.mod h1:ykucQyiE9Q2qx1wLlEtZkkNn1IURib/2O+Mvd25i1Fo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= @@ -1331,7 +1283,6 @@ github.com/whyrusleeping/cbor-gen v0.0.0-20200810223238-211df3b9e24c/go.mod h1:f github.com/whyrusleeping/cbor-gen v0.0.0-20200812213548-958ddffe352c/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.0.0-20200826160007-0b9f6c5fb163/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.0.0-20210118024343-169e9d70c0c2/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= -github.com/whyrusleeping/cbor-gen v0.0.0-20210303213153-67a261a1d291/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.3.2-0.20250409092040-76796969edea h1:/uOIA87OS8z0NyGuTXnWq7Z4qCzsNrUh1AojK943zQE= github.com/whyrusleeping/cbor-gen v0.3.2-0.20250409092040-76796969edea/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= @@ -1383,13 +1334,6 @@ gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRyS gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= -go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= -go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= -go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= -go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= -go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= -go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= -go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= @@ -1460,7 +1404,6 @@ go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEb golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1469,15 +1412,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= @@ -1597,7 +1535,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1616,13 +1553,11 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1635,13 +1570,10 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1666,7 +1598,6 @@ golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20251022145735-5be28d707443 h1:eE5IhBiTMPgrcTS6Mlh7IG4MdydRrXr2y60Jn/JC6kM= golang.org/x/telemetry v0.0.0-20251022145735-5be28d707443/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -1745,7 +1676,6 @@ golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200711155855-7342f9734a7d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -1877,7 +1807,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1913,19 +1842,14 @@ kr.dev/errorfmt v0.1.1 h1:0YA5N2yV0xKxJ4eD5cX2S9wEnJHDHOZzerKbrZqtRrQ= kr.dev/errorfmt v0.1.1/go.mod h1:X5EQZa3qf6c/l1DMjhflAbKGAGvlP6/ByWnaOpfbJME= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= -modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY= modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw= -modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU= modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= diff --git a/handler/wallet/import_keystore.go b/handler/wallet/import_keystore.go index 82688412..278afcb1 100644 --- a/handler/wallet/import_keystore.go +++ b/handler/wallet/import_keystore.go @@ -12,7 +12,6 @@ import ( "github.com/data-preservation-programs/singularity/util" "github.com/data-preservation-programs/singularity/util/keystore" "github.com/ipfs/go-log/v2" - filwallet "github.com/jsign/go-filsigner/wallet" "gorm.io/gorm" ) @@ -34,18 +33,11 @@ func (DefaultHandler) ImportKeystoreHandler( ) (*model.Wallet, error) { db = db.WithContext(ctx) - // validate private key by deriving address - addr, err := filwallet.PublicKey(request.PrivateKey) - if err != nil { - logger.Errorw("failed to derive address from private key", "err", err) - return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid private key") - } - - // save to keystore - keyPath, _, err := ks.Put(request.PrivateKey) + // save to keystore (validates key and derives address) + keyPath, addr, err := ks.Put(request.PrivateKey) if err != nil { logger.Errorw("failed to save key to keystore", "err", err) - return nil, errors.Wrap(err, "failed to save key to keystore") + return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid private key or keystore error") } logger.Infow("saved key to keystore", "address", addr.String(), "path", keyPath) diff --git a/handler/wallet/sign.go b/handler/wallet/sign.go index b0517cd5..1d3a7bdf 100644 --- a/handler/wallet/sign.go +++ b/handler/wallet/sign.go @@ -7,21 +7,18 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util/keystore" "github.com/filecoin-project/go-state-types/crypto" - filwallet "github.com/jsign/go-filsigner/wallet" "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) -// loads private key from keystore and signs message -// new signing flow - loads keys from disk instead of database +// loads private key from keystore and signs a filecoin message func SignWithWallet(ks keystore.KeyStore, wallet model.Wallet, msg []byte) (*crypto.Signature, error) { - privateKey, err := ks.Get(wallet.KeyPath) + s, err := keystore.Signer(ks, wallet) if err != nil { - return nil, errors.Wrap(err, "failed to load private key from keystore") + return nil, errors.Wrap(err, "failed to load signer from keystore") } - // filwallet.WalletSign automatically detects key type (secp256k1 or BLS) - signature, err := filwallet.WalletSign(privateKey, msg) + signature, err := s.Sign(msg) if err != nil { return nil, errors.Wrap(err, "failed to sign message") } diff --git a/util/keystore/keystore.go b/util/keystore/keystore.go index 87cd86ac..8cd1c526 100644 --- a/util/keystore/keystore.go +++ b/util/keystore/keystore.go @@ -5,8 +5,8 @@ import ( "os" "path/filepath" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/filecoin-project/go-address" - filwallet "github.com/jsign/go-filsigner/wallet" ) type KeyStore interface { @@ -34,9 +34,9 @@ func NewLocalKeyStore(dir string) (*LocalKeyStore, error) { return &LocalKeyStore{dir: dir}, nil } -// lotus/go-filsigner export format expected +// lotus wallet export format expected (hex-encoded JSON with Type and PrivateKey) func (ks *LocalKeyStore) Put(privateKey string) (string, address.Address, error) { - addr, err := filwallet.PublicKey(privateKey) + addr, err := addressFromExport(privateKey) if err != nil { return "", address.Undef, fmt.Errorf("failed to derive address from private key: %w", err) } @@ -78,8 +78,7 @@ func (ks *LocalKeyStore) List() ([]KeyInfo, error) { continue // skip unreadable } - // verify valid key by deriving address - addr, err := filwallet.PublicKey(string(data)) + addr, err := addressFromExport(string(data)) if err != nil { continue // skip invalid } @@ -104,3 +103,12 @@ func (ks *LocalKeyStore) Has(path string) bool { _, err := os.Stat(path) return err == nil } + +// derives filecoin address from a lotus wallet export string +func addressFromExport(exported string) (address.Address, error) { + s, err := signer.FromLotusExport(exported) + if err != nil { + return address.Undef, err + } + return s.FilecoinAddress(), nil +} diff --git a/util/keystore/signer.go b/util/keystore/signer.go new file mode 100644 index 00000000..60ee0bca --- /dev/null +++ b/util/keystore/signer.go @@ -0,0 +1,37 @@ +package keystore + +import ( + "fmt" + + "github.com/data-preservation-programs/go-synapse/signer" + "github.com/data-preservation-programs/singularity/model" +) + +// Signer loads a wallet's private key from the keystore and returns +// a go-synapse Signer. For secp256k1 keys the result also satisfies +// signer.EVMSigner (check with signer.AsEVM). +func Signer(ks KeyStore, w model.Wallet) (signer.Signer, error) { + exported, err := ks.Get(w.KeyPath) + if err != nil { + return nil, fmt.Errorf("loading key for wallet %d: %w", w.ID, err) + } + s, err := signer.FromLotusExport(exported) + if err != nil { + return nil, fmt.Errorf("parsing key for wallet %d: %w", w.ID, err) + } + return s, nil +} + +// EVMSigner loads a wallet's key and returns an EVMSigner for Ethereum/FEVM +// transaction signing. Returns an error if the key type is BLS. +func EVMSigner(ks KeyStore, w model.Wallet) (signer.EVMSigner, error) { + s, err := Signer(ks, w) + if err != nil { + return nil, err + } + evm, ok := signer.AsEVM(s) + if !ok { + return nil, fmt.Errorf("wallet %d (%s) is not an EVM-capable key", w.ID, w.Address) + } + return evm, nil +} From 1d8c81587cc2aaf99ef165ac501171324639cc68 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Tue, 24 Feb 2026 16:12:48 +0100 Subject: [PATCH 06/15] =?UTF-8?q?fix=20Wallet=E2=86=92Actor=20references?= =?UTF-8?q?=20in=20shovel=20and=20pdp=20scheduling=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- replication/makedeal.go | 2 +- service/dealpusher/pdp_api.go | 2 +- service/dealpusher/pdp_wiring_test.go | 2 +- service/pdptracker/eventprocessor.go | 6 +++--- service/pdptracker/eventprocessor_test.go | 8 ++++---- service/pdptracker/integration_test.go | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/replication/makedeal.go b/replication/makedeal.go index 110e5817..133a431e 100644 --- a/replication/makedeal.go +++ b/replication/makedeal.go @@ -493,7 +493,7 @@ func (d DealConfig) GetPrice(pieceSize int64, duration time.Duration) big.Int { // // Parameters: // - ctx context.Context: The context to use for timeouts and cancellation. -// - actorObj model.Wallet: The client's wallet, containing the client's addresses and private key. +// - actorObj model.Actor: The on-chain actor identity for deal signing. // - car model.Car: The car file that contains the data to be stored. // - dealConfig DealConfig: The configuration for the deal, including price and duration. // diff --git a/service/dealpusher/pdp_api.go b/service/dealpusher/pdp_api.go index 2634213b..6ec18d49 100644 --- a/service/dealpusher/pdp_api.go +++ b/service/dealpusher/pdp_api.go @@ -38,7 +38,7 @@ func (c PDPSchedulingConfig) Validate() error { // PDPProofSetManager defines proof set lifecycle operations needed by scheduling. type PDPProofSetManager interface { // EnsureProofSet returns an existing proof set ID or creates one for this client/provider pair. - EnsureProofSet(ctx context.Context, wallet model.Wallet, provider string) (uint64, error) + EnsureProofSet(ctx context.Context, actor model.Actor, provider string) (uint64, error) // QueueAddRoots submits root additions for a proof set and returns the queued tx reference. QueueAddRoots(ctx context.Context, proofSetID uint64, pieceCIDs []cid.Cid, cfg PDPSchedulingConfig) (*PDPQueuedTx, error) } diff --git a/service/dealpusher/pdp_wiring_test.go b/service/dealpusher/pdp_wiring_test.go index 5a7dbaa5..a4f82c32 100644 --- a/service/dealpusher/pdp_wiring_test.go +++ b/service/dealpusher/pdp_wiring_test.go @@ -12,7 +12,7 @@ import ( type noopPDPProofSetManager struct{} -func (noopPDPProofSetManager) EnsureProofSet(_ context.Context, _ model.Wallet, _ string) (uint64, error) { +func (noopPDPProofSetManager) EnsureProofSet(_ context.Context, _ model.Actor, _ string) (uint64, error) { return 1, nil } diff --git a/service/pdptracker/eventprocessor.go b/service/pdptracker/eventprocessor.go index 4d11a2e8..aafe9ea8 100644 --- a/service/pdptracker/eventprocessor.go +++ b/service/pdptracker/eventprocessor.go @@ -207,8 +207,8 @@ func reconcileProofSetPieces(ctx context.Context, db *gorm.DB, rpcClient *ChainP return nil } - var wallet model.Wallet - if err := db.Where("address = ?", ps.ClientAddress).First(&wallet).Error; err != nil { + var actor model.Actor + if err := db.Where("address = ?", ps.ClientAddress).First(&actor).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { Logger.Debugw("pieces changed for untracked client", "setId", setID, "client", ps.ClientAddress) return nil @@ -255,7 +255,7 @@ func reconcileProofSetPieces(ctx context.Context, db *gorm.DB, rpcClient *ChainP return db.Create(&model.Deal{ DealType: model.DealTypePDP, State: initialState, - ClientID: wallet.ID, + ClientID: actor.ID, Provider: ps.Provider, PieceCID: modelCID, ProofSetID: ptr.Of(setID), diff --git a/service/pdptracker/eventprocessor_test.go b/service/pdptracker/eventprocessor_test.go index 49ae328a..e77d869d 100644 --- a/service/pdptracker/eventprocessor_test.go +++ b/service/pdptracker/eventprocessor_test.go @@ -82,7 +82,7 @@ func pgTest(t *testing.T, fn func(t *testing.T, e pgTestEnv)) { func (e pgTestEnv) setupFixtures(t *testing.T) { t.Helper() - require.NoError(t, e.db.Create(&model.Wallet{ + require.NoError(t, e.db.Create(&model.Actor{ ID: "f0100", Address: e.listenerFil.String(), }).Error) require.NoError(t, e.db.Create(&model.PDPProofSet{ @@ -427,8 +427,8 @@ func TestDeleteByKeys(t *testing.T) { func TestPiecesRetainedWhenProofSetMissing(t *testing.T) { pgTest(t, func(t *testing.T, e pgTestEnv) { - // only create wallet, no proof set yet - require.NoError(t, e.db.Create(&model.Wallet{ + // only create actor, no proof set yet + require.NoError(t, e.db.Create(&model.Actor{ ID: "f0100", Address: e.listenerFil.String(), }).Error) @@ -470,7 +470,7 @@ func TestProcessNewEvents_EmptyTables(t *testing.T) { func TestProcessNewEvents_FullLifecycle(t *testing.T) { pgTest(t, func(t *testing.T, e pgTestEnv) { - require.NoError(t, e.db.Create(&model.Wallet{ + require.NoError(t, e.db.Create(&model.Actor{ ID: "f0100", Address: e.listenerFil.String(), }).Error) diff --git a/service/pdptracker/integration_test.go b/service/pdptracker/integration_test.go index 1023bc6c..e8bc97e4 100644 --- a/service/pdptracker/integration_test.go +++ b/service/pdptracker/integration_test.go @@ -159,7 +159,7 @@ func TestIntegration_FullResync(t *testing.T) { t.Logf("data rows before resync: task_updates=%d, dataset_created=%d", cursorCount, dataCount) // seed derived PDP state that must survive full resync - require.NoError(t, db.Create(&model.Wallet{ + require.NoError(t, db.Create(&model.Actor{ ID: "f0100", Address: "f410faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaakrfc62sy", }).Error) require.NoError(t, db.Create(&model.PDPProofSet{ From cbc5a6c45b592477801bae65ec1a995888ae06d8 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Tue, 24 Feb 2026 16:35:20 +0100 Subject: [PATCH 07/15] add ImportKeystoreHandler integration tests --- handler/wallet/import_test.go | 92 ++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/handler/wallet/import_test.go b/handler/wallet/import_test.go index e49030af..108f0f5e 100644 --- a/handler/wallet/import_test.go +++ b/handler/wallet/import_test.go @@ -1,13 +1,93 @@ package wallet import ( + "context" "testing" + + "github.com/data-preservation-programs/singularity/handler/handlererror" + "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/stretchr/testify/require" + "gorm.io/gorm" ) -// TODO: ImportHandler was removed as part of wallet/actor separation (#590) -// This test needs to be replaced with tests for ImportKeystoreHandler which uses -// the new keystore-based approach instead of storing private keys in the database. -// See handler/wallet/import_keystore.go for the new implementation. -func TestImportHandler(t *testing.T) { - t.Skip("ImportHandler removed - needs replacement with ImportKeystoreHandler tests") +func TestImportKeystoreHandler_Success(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + w, err := h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + Name: "test-wallet", + }) + require.NoError(t, err) + require.NotNil(t, w) + require.Equal(t, testutil.TestWalletAddr, w.Address) + require.Equal(t, "test-wallet", w.Name) + require.Equal(t, "local", w.KeyStore) + require.NotEmpty(t, w.KeyPath) + require.Nil(t, w.ActorID) + require.True(t, ks.Has(w.KeyPath)) + }) +} + +func TestImportKeystoreHandler_NoName(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + w, err := h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + }) + require.NoError(t, err) + require.Equal(t, "", w.Name) + require.Equal(t, testutil.TestWalletAddr, w.Address) + }) +} + +func TestImportKeystoreHandler_Duplicate(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + }) + require.NoError(t, err) + + // second import of same key should fail + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + }) + require.ErrorIs(t, err, handlererror.ErrDuplicateRecord) + }) +} + +func TestImportKeystoreHandler_InvalidKey(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: "not-a-valid-key", + }) + require.ErrorIs(t, err, handlererror.ErrInvalidParameter) + }) +} + +func TestImportKeystoreHandler_EmptyKey(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: "", + }) + require.ErrorIs(t, err, handlererror.ErrInvalidParameter) + }) } From af5cdb459919420d7f76dee05cfcc9ed158a340c Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Tue, 24 Feb 2026 17:03:28 +0100 Subject: [PATCH 08/15] refactor MakeDeal to accept ProposalSigner instead of db+keystore Callers (dealpusher, send-manual handler) now resolve the wallet and build a signing closure before calling MakeDeal, keeping the deal maker free of keystore and database concerns. --- api/api_test.go | 3 +- handler/deal/interface.go | 8 +--- handler/deal/send-manual.go | 13 ++++++- handler/deal/send-manual_test.go | 53 +++++++++++---------------- replication/makedeal.go | 25 +++++-------- replication/makedeal_test.go | 23 ++++++++---- service/dealpusher/dealpusher.go | 15 ++++++-- service/dealpusher/dealpusher_test.go | 19 +++++++++- 8 files changed, 90 insertions(+), 69 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 5e5d9fab..b4ddb698 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -34,7 +34,6 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service" - "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/gotidy/ptr" @@ -51,7 +50,7 @@ type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { +func (m *MockDealMaker) MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig, signer replication.ProposalSigner) (*model.Deal, error) { args := m.Called(ctx, actorObj, car, dealConfig) return args.Get(0).(*model.Deal), args.Error(1) } diff --git a/handler/deal/interface.go b/handler/deal/interface.go index 4efc3268..6bf71a3c 100644 --- a/handler/deal/interface.go +++ b/handler/deal/interface.go @@ -13,13 +13,7 @@ import ( type Handler interface { ListHandler(ctx context.Context, db *gorm.DB, request ListDealRequest) ([]model.Deal, error) - SendManualHandler( - ctx context.Context, - db *gorm.DB, - ks keystore.KeyStore, - dealMaker replication.DealMaker, - request Proposal, - ) (*model.Deal, error) + SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) } type DefaultHandler struct{} diff --git a/handler/deal/send-manual.go b/handler/deal/send-manual.go index a45df05f..ff87207e 100644 --- a/handler/deal/send-manual.go +++ b/handler/deal/send-manual.go @@ -9,10 +9,12 @@ import ( "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/handler/handlererror" + "github.com/data-preservation-programs/singularity/handler/wallet" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/util/keystore" "github.com/dustin/go-humanize" + "github.com/filecoin-project/go-state-types/crypto" "github.com/ipfs/go-cid" "gorm.io/gorm" ) @@ -138,7 +140,16 @@ func (DefaultHandler) SendManualHandler( Duration: duration, } - dealModel, err := dealMaker.MakeDeal(ctx, db, ks, actor, car, dealConfig) + // resolve wallet for signing + walletRecord, err := wallet.LoadWalletByActorID(ctx, db, actor.ID) + if err != nil { + return nil, errors.Wrapf(err, "failed to load wallet for actor %s", actor.ID) + } + signer := replication.ProposalSigner(func(msg []byte) (*crypto.Signature, error) { + return wallet.SignWithWallet(ks, *walletRecord, msg) + }) + + dealModel, err := dealMaker.MakeDeal(ctx, actor, car, dealConfig, signer) if err != nil { return nil, errors.WithStack(err) } diff --git a/handler/deal/send-manual_test.go b/handler/deal/send-manual_test.go index 9f86d799..db69bbc1 100644 --- a/handler/deal/send-manual_test.go +++ b/handler/deal/send-manual_test.go @@ -8,7 +8,6 @@ import ( "github.com/data-preservation-programs/singularity/handler/handlererror" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" - "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -19,8 +18,8 @@ type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, db, ks, actorObj, car, dealConfig) +func (m *MockDealMaker) MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig, signer replication.ProposalSigner) (*model.Deal, error) { + args := m.Called(ctx, actorObj, car, dealConfig) return args.Get(0).(*model.Deal), args.Error(1) } @@ -50,10 +49,9 @@ func TestSendManualHandler_WalletNotFound(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, proposal) + mockDealMaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, proposal) require.ErrorIs(t, err, handlererror.ErrNotFound) require.ErrorContains(t, err, "client address") }) @@ -69,12 +67,10 @@ func TestSendManualHandler_InvalidPieceCID(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceCID = "bad" - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece CID") }) @@ -90,12 +86,10 @@ func TestSendManualHandler_InvalidPieceCID_NOTCOMMP(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceCID = proposal.RootCID - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be commp") }) @@ -111,12 +105,10 @@ func TestSendManualHandler_InvalidPieceSize(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceSize = "aaa" - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece size") }) @@ -132,12 +124,10 @@ func TestSendManualHandler_InvalidPieceSize_NotPowerOfTwo(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceSize = "31GiB" - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be a power of 2") }) @@ -153,12 +143,10 @@ func TestSendManualHandler_InvalidRootCID(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.RootCID = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid root CID") }) @@ -174,12 +162,10 @@ func TestSendManualHandler_InvalidDuration(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.Duration = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid duration") }) @@ -195,20 +181,19 @@ func TestSendManualHandler_InvalidStartDelay(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.StartDelay = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, badProposal) + _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid start delay") }) } func TestSendManualHandler(t *testing.T) { + actorID := "f01000" actor := model.Actor{ - ID: "f01000", + ID: actorID, Address: "f10000", } @@ -216,9 +201,15 @@ func TestSendManualHandler(t *testing.T) { err := db.Create(&actor).Error require.NoError(t, err) - var mockKS keystore.KeyStore + // wallet record needed for signer resolution + err = db.Create(&model.Wallet{ + KeyPath: "/tmp/fake-key", KeyStore: "local", + Address: "f10000", ActorID: &actorID, + }).Error + require.NoError(t, err) + mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, db, mockKS, actor, mock.Anything, replication.DealConfig{ + mockDealMaker.On("MakeDeal", mock.Anything, actor, mock.Anything, replication.DealConfig{ Provider: proposal.ProviderID, StartDelay: 24 * time.Hour, Duration: 2400 * time.Hour, @@ -231,7 +222,7 @@ func TestSendManualHandler(t *testing.T) { PricePerGB: proposal.PricePerGB, PricePerGBEpoch: proposal.PricePerGBEpoch, }).Return(&model.Deal{}, nil) - resp, err := Default.SendManualHandler(ctx, db, mockKS, mockDealMaker, proposal) + resp, err := Default.SendManualHandler(ctx, db, nil, mockDealMaker, proposal) mockDealMaker.AssertExpectations(t) require.NoError(t, err) require.NotNil(t, resp) diff --git a/replication/makedeal.go b/replication/makedeal.go index 133a431e..39781b3d 100644 --- a/replication/makedeal.go +++ b/replication/makedeal.go @@ -21,9 +21,8 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/go-state-types/builtin/v9/market" + "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-shipyard/boostly" - "github.com/data-preservation-programs/singularity/handler/wallet" - "github.com/data-preservation-programs/singularity/util/keystore" "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/jellydator/ttlcache/v3" @@ -33,7 +32,6 @@ import ( "github.com/libp2p/go-libp2p/core/protocol" "github.com/multiformats/go-multiaddr" "github.com/ybbus/jsonrpc/v3" - "gorm.io/gorm" ) const ( @@ -43,6 +41,11 @@ const ( var ErrNoSupportedProtocols = errors.New("no supported protocols") +// ProposalSigner signs deal proposal bytes and returns a filecoin signature. +// Callers construct this from a keystore + wallet before calling MakeDeal, +// keeping the deal maker decoupled from database and key storage. +type ProposalSigner func([]byte) (*crypto.Signature, error) + //nolint:tagliatelle type MinerInfo struct { PeerIDEncoded string `json:"PeerID"` @@ -57,7 +60,7 @@ type DealProviderCollateralBound struct { } type DealMaker interface { - MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig DealConfig) (*model.Deal, error) + MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig DealConfig, signer ProposalSigner) (*model.Deal, error) } // DealMakerImpl is an implementation of a deal-making component for a Filecoin-like network. @@ -517,10 +520,9 @@ func (d DealConfig) GetPrice(pieceSize int64, duration time.Duration) big.Int { // - Deal proposal rejected by the provider. // // - No supported protocol found between client and provider. -func (d DealMakerImpl) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, - car model.Car, dealConfig DealConfig, +func (d DealMakerImpl) MakeDeal(ctx context.Context, actorObj model.Actor, + car model.Car, dealConfig DealConfig, signer ProposalSigner, ) (*model.Deal, error) { - db = db.WithContext(ctx) logger.Infow("making deal", "client", actorObj.ID, "pieceCID", car.PieceCID.String(), "provider", dealConfig.Provider) now := time.Now().UTC() addr, err := address.NewFromString(actorObj.Address) @@ -581,14 +583,7 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.Ke return nil, errors.Wrapf(err, "failed to serialize deal proposal %s", proposal) } - // load wallet that controls this actor - walletRecord, err := wallet.LoadWalletByActorID(ctx, db, actorObj.ID) - if err != nil { - return nil, errors.Wrapf(err, "failed to load wallet for actor %s", actorObj.ID) - } - - // sign using keystore-based signing - signature, err := wallet.SignWithWallet(ks, *walletRecord, proposalBytes) + signature, err := signer(proposalBytes) if err != nil { return nil, errors.Wrap(err, "failed to sign deal proposal") } diff --git a/replication/makedeal_test.go b/replication/makedeal_test.go index 4bff1308..471c885e 100644 --- a/replication/makedeal_test.go +++ b/replication/makedeal_test.go @@ -6,8 +6,10 @@ import ( "testing" "time" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/testutil" "github.com/filecoin-project/go-address" cborutil "github.com/filecoin-project/go-cbor-util" "github.com/filecoin-project/go-fil-markets/storagemarket/network" @@ -105,8 +107,6 @@ func setupBasicHost(t *testing.T, ctx context.Context, port string) host.Host { } func TestDealMaker_MakeDeal(t *testing.T) { - addr := "f1fib3pv7jua2ockdugtz7viz3cyy6lkhh7rfx3sa" - key := "7b2254797065223a22736563703235366b31222c22507269766174654b6579223a226b35507976337148327349586343595a58594f5775453149326e32554539436861556b6c4e36695a5763453d227d" ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := setupBasicHost(t, ctx, "10001") @@ -115,10 +115,17 @@ func TestDealMaker_MakeDeal(t *testing.T) { defer client.Close() maker := NewDealMaker(nil, client, time.Hour, time.Second) defer maker.Close() - wallet := model.Actor{ - ID: "f047684", - Address: addr, - PrivateKey: key, + + // build a real signer from the test key + s, err := signer.FromLotusExport(testutil.TestPrivateKeyHex) + require.NoError(t, err) + proposalSigner := ProposalSigner(func(msg []byte) (*crypto.Signature, error) { + return s.Sign(msg) + }) + + actor := model.Actor{ + ID: "f047684", + Address: testutil.TestWalletAddr, } rootCID, err := cid.Decode("bafy2bzaceczlclcg4notjmrz4ayenf7fi4mngnqbgjs27r3resyhzwxjnviay") require.NoError(t, err) @@ -154,14 +161,14 @@ func TestDealMaker_MakeDeal(t *testing.T) { StorageProposalV120, }, ttlcache.DefaultTTL) - _, err = maker.MakeDeal(ctx, nil, nil, wallet, car, dealConfig) + _, err = maker.MakeDeal(ctx, actor, car, dealConfig, proposalSigner) require.NoError(t, err) maker.protocolsCache.Set(server.ID(), []protocol.ID{ StorageProposalV111, }, ttlcache.DefaultTTL) - _, err = maker.MakeDeal(ctx, nil, nil, wallet, car, dealConfig) + _, err = maker.MakeDeal(ctx, actor, car, dealConfig, proposalSigner) require.NoError(t, err) } diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index 61064971..c7ff798d 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -16,6 +16,7 @@ import ( "github.com/data-preservation-programs/singularity/service/healthcheck" "github.com/data-preservation-programs/singularity/util" "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/filecoin-project/go-state-types/crypto" "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/ipfs/go-log/v2" @@ -368,11 +369,18 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) return model.ScheduleError, errors.Wrap(err, "failed to choose wallet") } + // resolve wallet for signing before entering retry loop + walletRecord, err := wallet.LoadWalletByActorID(ctx, d.dbNoContext, actorObj.ID) + if err != nil { + return model.ScheduleError, errors.Wrapf(err, "failed to load wallet for actor %s", actorObj.ID) + } + proposalSigner := replication.ProposalSigner(func(msg []byte) (*crypto.Signature, error) { + return wallet.SignWithWallet(d.keyStore, *walletRecord, msg) + }) + err = retry.Do(func() error { dealModel, err = d.dealMaker.MakeDeal( ctx, - d.dbNoContext, - d.keyStore, actorObj, car, replication.DealConfig{ @@ -387,7 +395,8 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) PricePerDeal: schedule.PricePerDeal, PricePerGB: schedule.PricePerGB, PricePerGBEpoch: schedule.PricePerGBEpoch, - }) + }, + proposalSigner) if err != nil { Logger.Errorw("failed to send deal", "error", err, "provider", schedule.Provider) if strings.Contains(err.Error(), "deal proposal is identical") { diff --git a/service/dealpusher/dealpusher_test.go b/service/dealpusher/dealpusher_test.go index 5b1f8a79..bc26c259 100644 --- a/service/dealpusher/dealpusher_test.go +++ b/service/dealpusher/dealpusher_test.go @@ -13,7 +13,6 @@ import ( "github.com/data-preservation-programs/singularity/pack" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service/epochutil" - "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" commp "github.com/filecoin-project/go-fil-commp-hashhash" "github.com/google/uuid" @@ -29,11 +28,21 @@ func init() { analytics.Enabled = false } +// creates a wallet record so that signer resolution in runSchedule succeeds. +// must be called after the actor record exists (e.g. after schedule creation). +func createTestWallet(t *testing.T, db *gorm.DB, actorID string) { + t.Helper() + require.NoError(t, db.Create(&model.Wallet{ + KeyPath: "/tmp/fake-key", KeyStore: "local", + Address: "f0xx", ActorID: &actorID, + }).Error) +} + type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { +func (m *MockDealMaker) MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig, signer replication.ProposalSigner) (*model.Deal, error) { args := m.Called(ctx, actorObj, car, dealConfig) if args.Get(0) == nil { return nil, args.Error(1) @@ -124,6 +133,7 @@ func TestDealMakerService_FailtoSend(t *testing.T) { } err = db.Create(&schedule).Error require.NoError(t, err) + createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("send deal error")) pieceCIDs := []model.CID{ model.CID(calculateCommp(t, generateRandomBytes(1000), 1024)), @@ -179,6 +189,7 @@ func TestDealMakerService_Cron(t *testing.T) { } err = db.Create(&schedule).Error require.NoError(t, err) + createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, @@ -275,6 +286,7 @@ func TestDealMakerService_ScheduleWithConstraints(t *testing.T) { } err = db.Create(&schedule).Error require.NoError(t, err) + createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, }, nil) @@ -384,6 +396,7 @@ func TestDealmakerService_Force(t *testing.T) { } err = db.Create(&schedule).Error require.NoError(t, err) + createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, }, nil) @@ -442,6 +455,7 @@ func TestDealMakerService_MaxReplica(t *testing.T) { } err = db.Create(&schedule).Error require.NoError(t, err) + createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, }, nil) @@ -509,6 +523,7 @@ func TestDealMakerService_NewScheduleOneOff(t *testing.T) { } err = db.Create(&schedule).Error require.NoError(t, err) + createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, From 033d760963749d369934c20aeeaf411187aba2ba Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Tue, 24 Feb 2026 17:13:36 +0100 Subject: [PATCH 09/15] harmonize wallet/actor dichotomy --- api/api_test.go | 8 ++- .../http/wallet/import_wallet_parameters.go | 6 +- .../list_attached_wallets_responses.go | 4 +- client/swagger/models/model_actor.go | 56 +++++++++++++++++++ client/swagger/models/model_wallet.go | 19 +++++-- .../models/wallet_import_keystore_request.go | 53 ++++++++++++++++++ .../swagger/models/wallet_import_request.go | 50 ----------------- docs/en/cli-reference/wallet/README.md | 2 +- docs/en/cli-reference/wallet/import.md | 4 +- docs/swagger/docs.go | 48 +++++++++++++--- docs/swagger/swagger.json | 48 +++++++++++++--- docs/swagger/swagger.yaml | 39 ++++++++++--- handler/dataprep/remove.go | 2 +- handler/deal/schedule/create.go | 2 +- handler/deal/schedule/create_test.go | 2 +- handler/wallet/import_keystore.go | 12 ++++ handler/wallet/listattached.go | 2 +- 17 files changed, 264 insertions(+), 93 deletions(-) create mode 100644 client/swagger/models/model_actor.go create mode 100644 client/swagger/models/wallet_import_keystore_request.go delete mode 100644 client/swagger/models/wallet_import_request.go diff --git a/api/api_test.go b/api/api_test.go index b4ddb698..ec666e8a 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -35,6 +35,7 @@ import ( "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/gotidy/ptr" "github.com/ipfs/go-log/v2" @@ -186,7 +187,7 @@ func setupMockWallet() wallet.Handler { Return(&model.Preparation{}, nil) m.On("DetachHandler", mock.Anything, mock.Anything, "id", "wallet"). Return(&model.Preparation{}, nil) - m.On("ImportHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + m.On("ImportKeystoreHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(&model.Wallet{}, nil) m.On("ListHandler", mock.Anything, mock.Anything). Return([]model.Wallet{{}}, nil) @@ -215,11 +216,14 @@ func TestAllAPIs(t *testing.T) { require.NoError(t, err) testutil.One(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) s := Server{ db: db, listener: listener, lotusClient: util.NewLotusClient("", ""), dealMaker: mockDealMaker, + keyStore: ks, closer: io.NopCloser(nil), host: h, adminHandler: mockAdmin, @@ -303,7 +307,7 @@ func TestAllAPIs(t *testing.T) { t.Run("wallet", func(t *testing.T) { t.Run("ImportWallet", func(t *testing.T) { resp, err := client.Wallet.ImportWallet(&wallet2.ImportWalletParams{ - Request: &models.WalletImportRequest{}, + Request: &models.WalletImportKeystoreRequest{}, Context: ctx, }) require.NoError(t, err) diff --git a/client/swagger/http/wallet/import_wallet_parameters.go b/client/swagger/http/wallet/import_wallet_parameters.go index c33f7623..604dcf31 100644 --- a/client/swagger/http/wallet/import_wallet_parameters.go +++ b/client/swagger/http/wallet/import_wallet_parameters.go @@ -67,7 +67,7 @@ type ImportWalletParams struct { Request body */ - Request *models.WalletImportRequest + Request *models.WalletImportKeystoreRequest timeout time.Duration Context context.Context @@ -123,13 +123,13 @@ func (o *ImportWalletParams) SetHTTPClient(client *http.Client) { } // WithRequest adds the request to the import wallet params -func (o *ImportWalletParams) WithRequest(request *models.WalletImportRequest) *ImportWalletParams { +func (o *ImportWalletParams) WithRequest(request *models.WalletImportKeystoreRequest) *ImportWalletParams { o.SetRequest(request) return o } // SetRequest adds the request to the import wallet params -func (o *ImportWalletParams) SetRequest(request *models.WalletImportRequest) { +func (o *ImportWalletParams) SetRequest(request *models.WalletImportKeystoreRequest) { o.Request = request } diff --git a/client/swagger/http/wallet_association/list_attached_wallets_responses.go b/client/swagger/http/wallet_association/list_attached_wallets_responses.go index a8e4582e..2ef2fbe6 100644 --- a/client/swagger/http/wallet_association/list_attached_wallets_responses.go +++ b/client/swagger/http/wallet_association/list_attached_wallets_responses.go @@ -59,7 +59,7 @@ ListAttachedWalletsOK describes a response with status code 200, with default he OK */ type ListAttachedWalletsOK struct { - Payload []*models.ModelWallet + Payload []*models.ModelActor } // IsSuccess returns true when this list attached wallets o k response has a 2xx status code @@ -102,7 +102,7 @@ func (o *ListAttachedWalletsOK) String() string { return fmt.Sprintf("[GET /preparation/{id}/wallet][%d] listAttachedWalletsOK %s", 200, payload) } -func (o *ListAttachedWalletsOK) GetPayload() []*models.ModelWallet { +func (o *ListAttachedWalletsOK) GetPayload() []*models.ModelActor { return o.Payload } diff --git a/client/swagger/models/model_actor.go b/client/swagger/models/model_actor.go new file mode 100644 index 00000000..81e89bf1 --- /dev/null +++ b/client/swagger/models/model_actor.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ModelActor model actor +// +// swagger:model model.Actor +type ModelActor struct { + + // filecoin address + Address string `json:"address,omitempty"` + + // actor ID (f0...) + ID string `json:"id,omitempty"` + + // TODO: orphaned column, will be dropped by export-keys command + PrivateKey string `json:"privateKey,omitempty"` +} + +// Validate validates this model actor +func (m *ModelActor) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this model actor based on context it is used +func (m *ModelActor) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ModelActor) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ModelActor) UnmarshalBinary(b []byte) error { + var res ModelActor + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/client/swagger/models/model_wallet.go b/client/swagger/models/model_wallet.go index dc2ff9fe..335f3129 100644 --- a/client/swagger/models/model_wallet.go +++ b/client/swagger/models/model_wallet.go @@ -17,14 +17,23 @@ import ( // swagger:model model.Wallet type ModelWallet struct { - // Address is the Filecoin full address of the wallet + // nullable, links to on-chain actor f0... + ActorID string `json:"actorId,omitempty"` + + // filecoin address (f1.../f3...) Address string `json:"address,omitempty"` - // ID is the short ID of the wallet - ID string `json:"id,omitempty"` + // id + ID int64 `json:"id,omitempty"` + + // absolute path to key file + KeyPath string `json:"keyPath,omitempty"` + + // local, yubikey, aws-kms, etc + KeyStore string `json:"keyStore,omitempty"` - // PrivateKey is the private key of the wallet - PrivateKey string `json:"privateKey,omitempty"` + // optional label + Name string `json:"name,omitempty"` } // Validate validates this model wallet diff --git a/client/swagger/models/wallet_import_keystore_request.go b/client/swagger/models/wallet_import_keystore_request.go new file mode 100644 index 00000000..086fc600 --- /dev/null +++ b/client/swagger/models/wallet_import_keystore_request.go @@ -0,0 +1,53 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// WalletImportKeystoreRequest wallet import keystore request +// +// swagger:model wallet.ImportKeystoreRequest +type WalletImportKeystoreRequest struct { + + // optional human-readable name + Name string `json:"name,omitempty"` + + // lotus wallet export format + PrivateKey string `json:"privateKey,omitempty"` +} + +// Validate validates this wallet import keystore request +func (m *WalletImportKeystoreRequest) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this wallet import keystore request based on context it is used +func (m *WalletImportKeystoreRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *WalletImportKeystoreRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *WalletImportKeystoreRequest) UnmarshalBinary(b []byte) error { + var res WalletImportKeystoreRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/client/swagger/models/wallet_import_request.go b/client/swagger/models/wallet_import_request.go deleted file mode 100644 index 66d55e15..00000000 --- a/client/swagger/models/wallet_import_request.go +++ /dev/null @@ -1,50 +0,0 @@ -// Code generated by go-swagger; DO NOT EDIT. - -package models - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -import ( - "context" - - "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag" -) - -// WalletImportRequest wallet import request -// -// swagger:model wallet.ImportRequest -type WalletImportRequest struct { - - // This is the exported private key from lotus wallet export - PrivateKey string `json:"privateKey,omitempty"` -} - -// Validate validates this wallet import request -func (m *WalletImportRequest) Validate(formats strfmt.Registry) error { - return nil -} - -// ContextValidate validates this wallet import request based on context it is used -func (m *WalletImportRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (m *WalletImportRequest) MarshalBinary() ([]byte, error) { - if m == nil { - return nil, nil - } - return swag.WriteJSON(m) -} - -// UnmarshalBinary interface implementation -func (m *WalletImportRequest) UnmarshalBinary(b []byte) error { - var res WalletImportRequest - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *m = res - return nil -} diff --git a/docs/en/cli-reference/wallet/README.md b/docs/en/cli-reference/wallet/README.md index 7096416a..0e4e3b18 100644 --- a/docs/en/cli-reference/wallet/README.md +++ b/docs/en/cli-reference/wallet/README.md @@ -9,7 +9,7 @@ USAGE: singularity wallet command [command options] COMMANDS: - import Import a wallet from exported private key + import Import a wallet from a private key file into the keystore list List all imported wallets remove Remove a wallet help, h Shows a list of commands or help for one command diff --git a/docs/en/cli-reference/wallet/import.md b/docs/en/cli-reference/wallet/import.md index 9685a604..6d5d99f5 100644 --- a/docs/en/cli-reference/wallet/import.md +++ b/docs/en/cli-reference/wallet/import.md @@ -1,9 +1,9 @@ -# Import a wallet from exported private key +# Import a wallet from a private key file into the keystore {% code fullWidth="true" %} ``` NAME: - singularity wallet import - Import a wallet from exported private key + singularity wallet import - Import a wallet from a private key file into the keystore USAGE: singularity wallet import [command options] [path, or stdin if omitted] diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 364b2c6f..0756d34a 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -1404,7 +1404,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/model.Wallet" + "$ref": "#/definitions/model.Actor" } } }, @@ -5892,7 +5892,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/wallet.ImportRequest" + "$ref": "#/definitions/wallet.ImportKeystoreRequest" } } ], @@ -6336,6 +6336,23 @@ const docTemplate = `{ } } }, + "model.Actor": { + "type": "object", + "properties": { + "address": { + "description": "filecoin address", + "type": "string" + }, + "id": { + "description": "actor ID (f0...)", + "type": "string" + }, + "privateKey": { + "description": "TODO: orphaned column, will be dropped by export-keys command", + "type": "string" + } + } + }, "model.Car": { "type": "object", "properties": { @@ -6936,16 +6953,27 @@ const docTemplate = `{ "model.Wallet": { "type": "object", "properties": { + "actorId": { + "description": "nullable, links to on-chain actor f0...", + "type": "string" + }, "address": { - "description": "Address is the Filecoin full address of the wallet", + "description": "filecoin address (f1.../f3...)", "type": "string" }, "id": { - "description": "ID is the short ID of the wallet", + "type": "integer" + }, + "keyPath": { + "description": "absolute path to key file", "type": "string" }, - "privateKey": { - "description": "PrivateKey is the private key of the wallet", + "keyStore": { + "description": "local, yubikey, aws-kms, etc", + "type": "string" + }, + "name": { + "description": "optional label", "type": "string" } } @@ -20328,11 +20356,15 @@ const docTemplate = `{ "store.PieceReader": { "type": "object" }, - "wallet.ImportRequest": { + "wallet.ImportKeystoreRequest": { "type": "object", "properties": { + "name": { + "description": "optional human-readable name", + "type": "string" + }, "privateKey": { - "description": "This is the exported private key from lotus wallet export", + "description": "lotus wallet export format", "type": "string" } } diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 2c27e1b6..4ea0a935 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -1397,7 +1397,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/model.Wallet" + "$ref": "#/definitions/model.Actor" } } }, @@ -5885,7 +5885,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/wallet.ImportRequest" + "$ref": "#/definitions/wallet.ImportKeystoreRequest" } } ], @@ -6329,6 +6329,23 @@ } } }, + "model.Actor": { + "type": "object", + "properties": { + "address": { + "description": "filecoin address", + "type": "string" + }, + "id": { + "description": "actor ID (f0...)", + "type": "string" + }, + "privateKey": { + "description": "TODO: orphaned column, will be dropped by export-keys command", + "type": "string" + } + } + }, "model.Car": { "type": "object", "properties": { @@ -6929,16 +6946,27 @@ "model.Wallet": { "type": "object", "properties": { + "actorId": { + "description": "nullable, links to on-chain actor f0...", + "type": "string" + }, "address": { - "description": "Address is the Filecoin full address of the wallet", + "description": "filecoin address (f1.../f3...)", "type": "string" }, "id": { - "description": "ID is the short ID of the wallet", + "type": "integer" + }, + "keyPath": { + "description": "absolute path to key file", "type": "string" }, - "privateKey": { - "description": "PrivateKey is the private key of the wallet", + "keyStore": { + "description": "local, yubikey, aws-kms, etc", + "type": "string" + }, + "name": { + "description": "optional label", "type": "string" } } @@ -20321,11 +20349,15 @@ "store.PieceReader": { "type": "object" }, - "wallet.ImportRequest": { + "wallet.ImportKeystoreRequest": { "type": "object", "properties": { + "name": { + "description": "optional human-readable name", + "type": "string" + }, "privateKey": { - "description": "This is the exported private key from lotus wallet export", + "description": "lotus wallet export format", "type": "string" } } diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index d6c215da..83cbdee2 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -278,6 +278,18 @@ definitions: storageId: type: integer type: object + model.Actor: + properties: + address: + description: filecoin address + type: string + id: + description: actor ID (f0...) + type: string + privateKey: + description: 'TODO: orphaned column, will be dropped by export-keys command' + type: string + type: object model.Car: properties: attachmentId: @@ -712,14 +724,22 @@ definitions: type: object model.Wallet: properties: + actorId: + description: nullable, links to on-chain actor f0... + type: string address: - description: Address is the Filecoin full address of the wallet + description: filecoin address (f1.../f3...) type: string id: - description: ID is the short ID of the wallet + type: integer + keyPath: + description: absolute path to key file type: string - privateKey: - description: PrivateKey is the private key of the wallet + keyStore: + description: local, yubikey, aws-kms, etc + type: string + name: + description: optional label type: string type: object schedule.CreateRequest: @@ -11103,10 +11123,13 @@ definitions: type: object store.PieceReader: type: object - wallet.ImportRequest: + wallet.ImportKeystoreRequest: properties: + name: + description: optional human-readable name + type: string privateKey: - description: This is the exported private key from lotus wallet export + description: lotus wallet export format type: string type: object externalDocs: @@ -12042,7 +12065,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/model.Wallet' + $ref: '#/definitions/model.Actor' type: array "400": description: Bad Request @@ -14987,7 +15010,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/wallet.ImportRequest' + $ref: '#/definitions/wallet.ImportKeystoreRequest' produces: - application/json responses: diff --git a/handler/dataprep/remove.go b/handler/dataprep/remove.go index 993521f6..07126b62 100644 --- a/handler/dataprep/remove.go +++ b/handler/dataprep/remove.go @@ -58,7 +58,7 @@ func (DefaultHandler) RemovePreparationHandler(ctx context.Context, db *gorm.DB, // by the healthcheck worker every 5 minutes. err = database.DoRetry(ctx, func() error { return db.Transaction(func(db *gorm.DB) error { - return db.Select("Wallets", "SourceStorages", "OutputStorages").Delete(&preparation).Error + return db.Select("Actors", "SourceStorages", "OutputStorages").Delete(&preparation).Error }) }) if err != nil { diff --git a/handler/deal/schedule/create.go b/handler/deal/schedule/create.go index ee44ba93..0a3b0d45 100644 --- a/handler/deal/schedule/create.go +++ b/handler/deal/schedule/create.go @@ -101,7 +101,7 @@ func (DefaultHandler) CreateHandler( request.MaxPendingDealSize = "0" } var preparation model.Preparation - err := preparation.FindByIDOrName(db, request.Preparation, "Wallets") + err := preparation.FindByIDOrName(db, request.Preparation, "Actors") if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", request.Preparation) } diff --git a/handler/deal/schedule/create_test.go b/handler/deal/schedule/create_test.go index 32059465..7e692313 100644 --- a/handler/deal/schedule/create_test.go +++ b/handler/deal/schedule/create_test.go @@ -182,7 +182,7 @@ func TestCreateHandler_NoAssociatedWallet(t *testing.T) { require.NoError(t, err) _, err = Default.CreateHandler(ctx, db, getMockLotusClient(), createRequest) require.ErrorIs(t, err, handlererror.ErrNotFound) - require.ErrorContains(t, err, "no wallet") + require.ErrorContains(t, err, "no actor") }) } diff --git a/handler/wallet/import_keystore.go b/handler/wallet/import_keystore.go index 278afcb1..de00e52b 100644 --- a/handler/wallet/import_keystore.go +++ b/handler/wallet/import_keystore.go @@ -22,6 +22,18 @@ type ImportKeystoreRequest struct { Name string `json:"name"` // optional human-readable name } +// @ID ImportWallet +// @Summary Import a private key +// @Tags Wallet +// @Accept json +// @Produce json +// @Param request body ImportKeystoreRequest true "Request body" +// @Success 200 {object} model.Wallet +// @Failure 400 {object} api.HTTPError +// @Failure 500 {object} api.HTTPError +// @Router /wallet [post] +func _() {} + // imports wallet by saving private key to keystore and creating wallet record // does not require actor to exist on-chain - wallet can be imported offline // uses external keystore instead of storing keys in database diff --git a/handler/wallet/listattached.go b/handler/wallet/listattached.go index 74b21b61..65165c83 100644 --- a/handler/wallet/listattached.go +++ b/handler/wallet/listattached.go @@ -47,7 +47,7 @@ func (DefaultHandler) ListAttachedHandler( // @Produce json // @Accept json // @Param id path string true "Preparation ID or name" -// @Success 200 {array} model.Wallet +// @Success 200 {array} model.Actor // @Failure 400 {object} api.HTTPError // @Failure 500 {object} api.HTTPError // @Router /preparation/{id}/wallet [get] From ee0f83b80a03c765ce8b38a75cc63646f69c878f Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Tue, 24 Feb 2026 17:20:18 +0100 Subject: [PATCH 10/15] PDPProofSetManager takes EVMSigner, not model.Actor --- service/dealpusher/pdp_api.go | 8 +++++--- service/dealpusher/pdp_wiring_test.go | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/service/dealpusher/pdp_api.go b/service/dealpusher/pdp_api.go index 6ec18d49..73efc3e5 100644 --- a/service/dealpusher/pdp_api.go +++ b/service/dealpusher/pdp_api.go @@ -6,7 +6,7 @@ import ( "math/big" "time" - "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/ipfs/go-cid" ) @@ -36,11 +36,13 @@ func (c PDPSchedulingConfig) Validate() error { } // PDPProofSetManager defines proof set lifecycle operations needed by scheduling. +// Both methods take an EVMSigner because they submit FEVM transactions. type PDPProofSetManager interface { // EnsureProofSet returns an existing proof set ID or creates one for this client/provider pair. - EnsureProofSet(ctx context.Context, actor model.Actor, provider string) (uint64, error) + // The signer's EVMAddress identifies the client on-chain. + EnsureProofSet(ctx context.Context, evmSigner signer.EVMSigner, provider string) (uint64, error) // QueueAddRoots submits root additions for a proof set and returns the queued tx reference. - QueueAddRoots(ctx context.Context, proofSetID uint64, pieceCIDs []cid.Cid, cfg PDPSchedulingConfig) (*PDPQueuedTx, error) + QueueAddRoots(ctx context.Context, evmSigner signer.EVMSigner, proofSetID uint64, pieceCIDs []cid.Cid, cfg PDPSchedulingConfig) (*PDPQueuedTx, error) } // PDPTransactionConfirmer defines confirmation checks for queued on-chain transactions. diff --git a/service/dealpusher/pdp_wiring_test.go b/service/dealpusher/pdp_wiring_test.go index a4f82c32..005fc9c1 100644 --- a/service/dealpusher/pdp_wiring_test.go +++ b/service/dealpusher/pdp_wiring_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/data-preservation-programs/singularity/model" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" @@ -12,11 +13,11 @@ import ( type noopPDPProofSetManager struct{} -func (noopPDPProofSetManager) EnsureProofSet(_ context.Context, _ model.Actor, _ string) (uint64, error) { +func (noopPDPProofSetManager) EnsureProofSet(_ context.Context, _ signer.EVMSigner, _ string) (uint64, error) { return 1, nil } -func (noopPDPProofSetManager) QueueAddRoots(_ context.Context, _ uint64, _ []cid.Cid, _ PDPSchedulingConfig) (*PDPQueuedTx, error) { +func (noopPDPProofSetManager) QueueAddRoots(_ context.Context, _ signer.EVMSigner, _ uint64, _ []cid.Cid, _ PDPSchedulingConfig) (*PDPQueuedTx, error) { return &PDPQueuedTx{Hash: "0x1"}, nil } From 21bbd98dc01a854e09532da13cd654877f90eb1c Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Tue, 24 Feb 2026 17:37:19 +0100 Subject: [PATCH 11/15] gofmt --- handler/wallet/remove.go | 1 + model/wallet.go | 6 +++--- replication/makedeal.go | 8 ++++---- service/dealpusher/dealpusher.go | 12 ++++++------ service/dealtracker/dealtracker.go | 16 ++++++++-------- testdb/main.go | 2 +- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/handler/wallet/remove.go b/handler/wallet/remove.go index cab2d89a..2953b958 100644 --- a/handler/wallet/remove.go +++ b/handler/wallet/remove.go @@ -18,6 +18,7 @@ import ( // - Remove Actor record only (stop tracking deals)? // - Remove both? // - Delete the actual keystore file? +// // Currently: Removes Actor record (temporary fix to match test expectations) // // RemoveHandler deletes an actor from the database based on its address or ID. diff --git a/model/wallet.go b/model/wallet.go index 7f1a5c43..556f16a5 100644 --- a/model/wallet.go +++ b/model/wallet.go @@ -5,10 +5,10 @@ package model type Wallet struct { ID uint `gorm:"primaryKey" json:"id"` - KeyPath string `gorm:"uniqueIndex;not null" json:"keyPath"` // absolute path to key file + KeyPath string `gorm:"uniqueIndex;not null" json:"keyPath"` // absolute path to key file KeyStore string `gorm:"default:'local';not null" json:"keyStore"` // local, yubikey, aws-kms, etc - Address string `gorm:"index;not null" json:"address"` // filecoin address (f1.../f3...) - Name string `json:"name,omitempty"` // optional label + Address string `gorm:"index;not null" json:"address"` // filecoin address (f1.../f3...) + Name string `json:"name,omitempty"` // optional label ActorID *string `gorm:"index;size:15" json:"actorId,omitempty"` // nullable, links to on-chain actor f0... diff --git a/replication/makedeal.go b/replication/makedeal.go index 39781b3d..a8877e92 100644 --- a/replication/makedeal.go +++ b/replication/makedeal.go @@ -594,10 +594,10 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, actorObj model.Actor, } dealModel := &model.Deal{ - State: model.DealProposed, - ClientID: actorObj.ID, - Provider: dealConfig.Provider, - Label: cid.Cid(car.RootCID).String(), + State: model.DealProposed, + ClientID: actorObj.ID, + Provider: dealConfig.Provider, + Label: cid.Cid(car.RootCID).String(), PieceCID: car.PieceCID, PieceSize: car.PieceSize, //nolint:gosec // G115: Safe conversion, max int32 epoch won't occur until year 4062 diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index c7ff798d..b552eeaf 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -38,12 +38,12 @@ var waitPendingInterval = time.Minute // DealPusher represents a struct that encapsulates the data and functionality related to pushing deals in a replication process. type DealPusher struct { - dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. - keyStore keystore.KeyStore // Keystore for loading private keys - walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. - dealMaker replication.DealMaker // Object responsible for making a deal in replication. - pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. - pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. + dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. + keyStore keystore.KeyStore // Keystore for loading private keys + walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. + dealMaker replication.DealMaker // Object responsible for making a deal in replication. + pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. + pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. // Resolver is injected so tests and future wiring can switch deal type behavior without coupling DealPusher to config storage. scheduleDealTypeResolver func(schedule *model.Schedule) model.DealType workerID uuid.UUID // UUID identifying the associated worker. diff --git a/service/dealtracker/dealtracker.go b/service/dealtracker/dealtracker.go index b48be228..26b6f2ce 100644 --- a/service/dealtracker/dealtracker.go +++ b/service/dealtracker/dealtracker.go @@ -381,10 +381,10 @@ type KnownDeal struct { State model.DealState } type UnknownDeal struct { - ID model.DealID - ClientID string - Provider string - PieceCID model.CID + ID model.DealID + ClientID string + Provider string + PieceCID model.CID StartEpoch int32 EndEpoch int32 } @@ -467,10 +467,10 @@ func (d *DealTracker) runOnce(ctx context.Context) error { } key := deal.Key() unknownDeals[key] = append(unknownDeals[key], UnknownDeal{ - ID: deal.ID, - ClientID: deal.ClientID, - Provider: deal.Provider, - PieceCID: deal.PieceCID, + ID: deal.ID, + ClientID: deal.ClientID, + Provider: deal.Provider, + PieceCID: deal.PieceCID, StartEpoch: deal.StartEpoch, EndEpoch: deal.EndEpoch, }) diff --git a/testdb/main.go b/testdb/main.go index 56700f3d..7859330e 100644 --- a/testdb/main.go +++ b/testdb/main.go @@ -342,7 +342,7 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Price: "0", Verified: true, ScheduleID: ptr.Of(schedule.ID), - ClientID: actor.ID, + ClientID: actor.ID, } if state == model.DealActive { //nolint:gosec // G115: Safe conversion, max int32 epoch won't occur until year 4062 From 8955bb062e56a2b6ffb7478b8ab212fe3acc95cc Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Wed, 25 Feb 2026 09:17:12 +0100 Subject: [PATCH 12/15] prepare for easier rebase for 621 --- service/dealpusher/dealpusher.go | 53 ++++++++++++++++++++++----- service/dealpusher/options.go | 30 +++++++++++++++ service/dealpusher/pdp_wiring_test.go | 10 +++++ 3 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 service/dealpusher/options.go diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index b552eeaf..9a0fb4db 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -16,6 +16,7 @@ import ( "github.com/data-preservation-programs/singularity/service/healthcheck" "github.com/data-preservation-programs/singularity/util" "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/crypto" "github.com/google/uuid" "github.com/ipfs/go-cid" @@ -38,12 +39,13 @@ var waitPendingInterval = time.Minute // DealPusher represents a struct that encapsulates the data and functionality related to pushing deals in a replication process. type DealPusher struct { - dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. - keyStore keystore.KeyStore // Keystore for loading private keys - walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. - dealMaker replication.DealMaker // Object responsible for making a deal in replication. - pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. - pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. + dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. + keyStore keystore.KeyStore // Keystore for loading private keys + walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. + dealMaker replication.DealMaker // Object responsible for making a deal in replication. + pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. + pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. + pdpSchedulingConfig PDPSchedulingConfig // PDP scheduling config for root batching and tx confirmation. // Resolver is injected so tests and future wiring can switch deal type behavior without coupling DealPusher to config storage. scheduleDealTypeResolver func(schedule *model.Schedule) model.DealType workerID uuid.UUID // UUID identifying the associated worker. @@ -447,11 +449,37 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) func (d *DealPusher) resolveScheduleDealType(schedule *model.Schedule) model.DealType { if d.scheduleDealTypeResolver == nil { - return model.DealTypeMarket + return inferScheduleDealType(schedule) } return d.scheduleDealTypeResolver(schedule) } +func defaultPDPSchedulingConfig() PDPSchedulingConfig { + return PDPSchedulingConfig{ + BatchSize: 128, + GasLimit: 5_000_000, + ConfirmationDepth: 5, + PollingInterval: 30 * time.Second, + } +} + +// inferScheduleDealType uses the provider address protocol as the discriminator: +// delegated (f4) addresses are FEVM contracts that speak PDP, everything else +// is a traditional miner actor that speaks market deals. +func inferScheduleDealType(schedule *model.Schedule) model.DealType { + if schedule == nil { + return model.DealTypeMarket + } + providerAddr, err := address.NewFromString(schedule.Provider) + if err != nil { + return model.DealTypeMarket + } + if providerAddr.Protocol() == address.Delegated { + return model.DealTypePDP + } + return model.DealTypeMarket +} + func (d *DealPusher) runPDPSchedule(_ context.Context, _ *model.Schedule) (model.ScheduleState, error) { if d.pdpProofSetManager == nil || d.pdpTxConfirmer == nil { return model.ScheduleError, errors.New("pdp scheduling dependencies are not configured") @@ -460,7 +488,7 @@ func (d *DealPusher) runPDPSchedule(_ context.Context, _ *model.Schedule) (model } func NewDealPusher(db *gorm.DB, lotusURL string, - lotusToken string, numAttempts uint, maxReplicas uint, + lotusToken string, numAttempts uint, maxReplicas uint, opts ...Option, ) (*DealPusher, error) { if numAttempts <= 1 { numAttempts = 1 @@ -478,7 +506,7 @@ func NewDealPusher(db *gorm.DB, lotusURL string, lotusClient := util.NewLotusClient(lotusURL, lotusToken) dealMaker := replication.NewDealMaker(lotusClient, h, time.Hour, time.Minute) - return &DealPusher{ + dp := &DealPusher{ dbNoContext: db, keyStore: ks, activeScheduleCancelFunc: make(map[model.ScheduleID]context.CancelFunc), @@ -486,13 +514,18 @@ func NewDealPusher(db *gorm.DB, lotusURL string, cronEntries: make(map[model.ScheduleID]cron.EntryID), walletChooser: &replication.RandomWalletChooser{}, dealMaker: dealMaker, + pdpSchedulingConfig: defaultPDPSchedulingConfig(), workerID: uuid.New(), cron: cron.New(cron.WithLogger(&cronLogger{}), cron.WithLocation(time.UTC), cron.WithParser(cron.NewParser(cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor))), sendDealAttempts: numAttempts, host: h, maxReplicas: maxReplicas, - }, nil + } + for _, opt := range opts { + opt(dp) + } + return dp, nil } // runOnce is a method of the DealPusher type that runs a single iteration of the deal pushing logic. diff --git a/service/dealpusher/options.go b/service/dealpusher/options.go new file mode 100644 index 00000000..4b986153 --- /dev/null +++ b/service/dealpusher/options.go @@ -0,0 +1,30 @@ +package dealpusher + +import "github.com/data-preservation-programs/singularity/model" + +// Option customizes DealPusher initialization. +type Option func(*DealPusher) + +func WithPDPProofSetManager(manager PDPProofSetManager) Option { + return func(d *DealPusher) { + d.pdpProofSetManager = manager + } +} + +func WithPDPTransactionConfirmer(confirmer PDPTransactionConfirmer) Option { + return func(d *DealPusher) { + d.pdpTxConfirmer = confirmer + } +} + +func WithPDPSchedulingConfig(cfg PDPSchedulingConfig) Option { + return func(d *DealPusher) { + d.pdpSchedulingConfig = cfg + } +} + +func WithScheduleDealTypeResolver(resolver func(schedule *model.Schedule) model.DealType) Option { + return func(d *DealPusher) { + d.scheduleDealTypeResolver = resolver + } +} diff --git a/service/dealpusher/pdp_wiring_test.go b/service/dealpusher/pdp_wiring_test.go index 005fc9c1..2001d1bf 100644 --- a/service/dealpusher/pdp_wiring_test.go +++ b/service/dealpusher/pdp_wiring_test.go @@ -7,6 +7,7 @@ import ( "github.com/data-preservation-programs/go-synapse/signer" "github.com/data-preservation-programs/singularity/model" + "github.com/filecoin-project/go-address" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" ) @@ -32,6 +33,15 @@ func TestDealPusher_ResolveScheduleDealType_DefaultsToMarket(t *testing.T) { require.Equal(t, model.DealTypeMarket, d.resolveScheduleDealType(&model.Schedule{})) } +func TestDealPusher_ResolveScheduleDealType_DelegatedProviderInfersPDP(t *testing.T) { + subaddr := make([]byte, 20) + subaddr[19] = 1 + providerAddr, err := address.NewDelegatedAddress(10, subaddr) + require.NoError(t, err) + d := &DealPusher{} + require.Equal(t, model.DealTypePDP, d.resolveScheduleDealType(&model.Schedule{Provider: providerAddr.String()})) +} + func TestDealPusher_RunSchedule_PDPWithoutDependenciesReturnsConfiguredError(t *testing.T) { d := &DealPusher{ scheduleDealTypeResolver: func(_ *model.Schedule) model.DealType { From 2ded76b8623d175631ddcb69978980e013d068ac Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Wed, 25 Feb 2026 09:26:54 +0100 Subject: [PATCH 13/15] fix duplicate key and keystore I/O error handling --- handler/wallet/import_keystore.go | 16 ++++++++++----- handler/wallet/import_test.go | 33 +++++++++++++++++++++++++++++-- util/keystore/keystore.go | 8 ++++---- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/handler/wallet/import_keystore.go b/handler/wallet/import_keystore.go index de00e52b..e6450954 100644 --- a/handler/wallet/import_keystore.go +++ b/handler/wallet/import_keystore.go @@ -45,11 +45,17 @@ func (DefaultHandler) ImportKeystoreHandler( ) (*model.Wallet, error) { db = db.WithContext(ctx) - // save to keystore (validates key and derives address) - keyPath, addr, err := ks.Put(request.PrivateKey) + // validate key before touching keystore: derive address to distinguish + // bad input (400) from keystore I/O failures (500) + addr, err := keystore.AddressFromExport(request.PrivateKey) + if err != nil { + return nil, errors.Wrap(handlererror.ErrInvalidParameter, err.Error()) + } + + keyPath, _, err := ks.Put(request.PrivateKey) if err != nil { logger.Errorw("failed to save key to keystore", "err", err) - return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid private key or keystore error") + return nil, errors.WithStack(err) } logger.Infow("saved key to keystore", "address", addr.String(), "path", keyPath) @@ -67,12 +73,12 @@ func (DefaultHandler) ImportKeystoreHandler( }) if util.IsDuplicateKeyError(err) { - ks.Delete(keyPath) // cleanup + // don't delete the key file — it belongs to the existing wallet record return nil, errors.Wrap(handlererror.ErrDuplicateRecord, "wallet already imported") } if err != nil { - ks.Delete(keyPath) // cleanup on failure + ks.Delete(keyPath) // cleanup only for non-duplicate failures return nil, errors.WithStack(err) } diff --git a/handler/wallet/import_test.go b/handler/wallet/import_test.go index 108f0f5e..8334d961 100644 --- a/handler/wallet/import_test.go +++ b/handler/wallet/import_test.go @@ -2,8 +2,10 @@ package wallet import ( "context" + "os" "testing" + "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/handler/handlererror" "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" @@ -53,16 +55,22 @@ func TestImportKeystoreHandler_Duplicate(t *testing.T) { require.NoError(t, err) h := DefaultHandler{} - _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + w, err := h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ PrivateKey: testutil.TestPrivateKeyHex, }) require.NoError(t, err) - // second import of same key should fail + // second import of same key should fail but not delete the key file _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ PrivateKey: testutil.TestPrivateKeyHex, }) require.ErrorIs(t, err, handlererror.ErrDuplicateRecord) + require.True(t, ks.Has(w.KeyPath), "key file must survive duplicate import") + + // original key must still be readable + key, err := ks.Get(w.KeyPath) + require.NoError(t, err) + require.Equal(t, testutil.TestPrivateKeyHex, key) }) } @@ -79,6 +87,27 @@ func TestImportKeystoreHandler_InvalidKey(t *testing.T) { }) } +func TestImportKeystoreHandler_KeystoreWriteFailure(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + dir := t.TempDir() + ks, err := keystore.NewLocalKeyStore(dir) + require.NoError(t, err) + + // make keystore directory read-only to force write failure + require.NoError(t, os.Chmod(dir, 0500)) + t.Cleanup(func() { os.Chmod(dir, 0700) }) + + h := DefaultHandler{} + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + }) + require.Error(t, err) + // must NOT be a 400 client error — this is a server-side I/O failure + require.False(t, errors.Is(err, handlererror.ErrInvalidParameter), + "keystore I/O failure must not be classified as invalid parameter") + }) +} + func TestImportKeystoreHandler_EmptyKey(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { ks, err := keystore.NewLocalKeyStore(t.TempDir()) diff --git a/util/keystore/keystore.go b/util/keystore/keystore.go index 8cd1c526..3fdcf5e5 100644 --- a/util/keystore/keystore.go +++ b/util/keystore/keystore.go @@ -36,7 +36,7 @@ func NewLocalKeyStore(dir string) (*LocalKeyStore, error) { // lotus wallet export format expected (hex-encoded JSON with Type and PrivateKey) func (ks *LocalKeyStore) Put(privateKey string) (string, address.Address, error) { - addr, err := addressFromExport(privateKey) + addr, err := AddressFromExport(privateKey) if err != nil { return "", address.Undef, fmt.Errorf("failed to derive address from private key: %w", err) } @@ -78,7 +78,7 @@ func (ks *LocalKeyStore) List() ([]KeyInfo, error) { continue // skip unreadable } - addr, err := addressFromExport(string(data)) + addr, err := AddressFromExport(string(data)) if err != nil { continue // skip invalid } @@ -104,8 +104,8 @@ func (ks *LocalKeyStore) Has(path string) bool { return err == nil } -// derives filecoin address from a lotus wallet export string -func addressFromExport(exported string) (address.Address, error) { +// AddressFromExport derives filecoin address from a lotus wallet export string +func AddressFromExport(exported string) (address.Address, error) { s, err := signer.FromLotusExport(exported) if err != nil { return address.Undef, err From b24c709294632cbe4ac0081f92c850c84c53aa02 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Wed, 25 Feb 2026 10:37:17 +0100 Subject: [PATCH 14/15] fix Wallet/Actor semantic inversion on Preparation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The model had Preparation associated with Actors (on-chain identities), but the correct invariant is: any deal we make requires a Wallet (private key), and a Wallet optionally has an Actor (for legacy market deals). PDP deals need only a Wallet. Imported/tracked deals may have neither. Core change: Preparation.Actors []Actor → Preparation.Wallets []Wallet with wallet_assignments join table (was actor_assignments with a TODO). Dealpusher market deal path now: choose wallet from Preparation.Wallets, require wallet.ActorID, load actor from DB, sign with wallet key, pass actor to MakeDeal. Previously it went through actors and reverse-looked up wallets. WalletChooser interface unchanged (already took []model.Wallet from prior commit) but DatacapWalletChooser now dereferences wallet.ActorID to find the on-chain client for datacap and pending deal queries. Handler changes: - attach/detach/listattached: operate on Preparation.Wallets directly, find wallet by address or uint ID, return []model.Wallet - schedule/create: preload Wallets, check len(preparation.Wallets) - dataprep/remove: cascade through Wallets association Swagger regenerated: ListAttachedWallets returns Wallet not Actor, ModelActor removed from generated client (unused). All tests updated: fixtures create Actor records for FK constraints (Deal.ClientID references actors table), use Wallets in Preparation fixtures, create wallets individually to avoid gorm batch uniqueIndex conflicts on KeyPath. --- api/api_test.go | 2 +- .../list_attached_wallets_responses.go | 4 +- client/swagger/models/model_actor.go | 56 ------------- cmd/dataprep_test.go | 8 +- cmd/wallet_test.go | 2 +- docs/swagger/docs.go | 19 +---- docs/swagger/swagger.json | 19 +---- docs/swagger/swagger.yaml | 14 +--- handler/dataprep/remove.go | 2 +- handler/deal/list_test.go | 8 +- handler/deal/schedule/create.go | 6 +- handler/deal/schedule/create_test.go | 18 ++--- handler/deal/schedule/list_test.go | 4 +- handler/deal/schedule/pause_test.go | 4 +- handler/deal/schedule/remove_test.go | 12 +-- handler/deal/schedule/resume_test.go | 4 +- handler/wallet/attach.go | 51 +++--------- handler/wallet/attach_test.go | 12 +-- handler/wallet/detach.go | 17 ++-- handler/wallet/detach_test.go | 12 +-- handler/wallet/interface.go | 6 +- handler/wallet/listattached.go | 23 +----- handler/wallet/listattached_test.go | 8 +- model/preparation.go | 2 +- replication/wallet.go | 78 ++++++++++--------- replication/wallet_test.go | 53 +++++++------ service/dealpusher/dealpusher.go | 19 +++-- service/dealpusher/dealpusher_test.go | 53 +++++++------ testdb/main.go | 19 ++++- util/keystore/keystore_test.go | 7 -- 30 files changed, 211 insertions(+), 331 deletions(-) delete mode 100644 client/swagger/models/model_actor.go diff --git a/api/api_test.go b/api/api_test.go index ec666e8a..459f4e6f 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -192,7 +192,7 @@ func setupMockWallet() wallet.Handler { m.On("ListHandler", mock.Anything, mock.Anything). Return([]model.Wallet{{}}, nil) m.On("ListAttachedHandler", mock.Anything, mock.Anything, "id"). - Return([]model.Actor{{}}, nil) + Return([]model.Wallet{{}}, nil) m.On("RemoveHandler", mock.Anything, mock.Anything, "wallet"). Return(nil) return m diff --git a/client/swagger/http/wallet_association/list_attached_wallets_responses.go b/client/swagger/http/wallet_association/list_attached_wallets_responses.go index 2ef2fbe6..a8e4582e 100644 --- a/client/swagger/http/wallet_association/list_attached_wallets_responses.go +++ b/client/swagger/http/wallet_association/list_attached_wallets_responses.go @@ -59,7 +59,7 @@ ListAttachedWalletsOK describes a response with status code 200, with default he OK */ type ListAttachedWalletsOK struct { - Payload []*models.ModelActor + Payload []*models.ModelWallet } // IsSuccess returns true when this list attached wallets o k response has a 2xx status code @@ -102,7 +102,7 @@ func (o *ListAttachedWalletsOK) String() string { return fmt.Sprintf("[GET /preparation/{id}/wallet][%d] listAttachedWalletsOK %s", 200, payload) } -func (o *ListAttachedWalletsOK) GetPayload() []*models.ModelActor { +func (o *ListAttachedWalletsOK) GetPayload() []*models.ModelWallet { return o.Payload } diff --git a/client/swagger/models/model_actor.go b/client/swagger/models/model_actor.go deleted file mode 100644 index 81e89bf1..00000000 --- a/client/swagger/models/model_actor.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by go-swagger; DO NOT EDIT. - -package models - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -import ( - "context" - - "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag" -) - -// ModelActor model actor -// -// swagger:model model.Actor -type ModelActor struct { - - // filecoin address - Address string `json:"address,omitempty"` - - // actor ID (f0...) - ID string `json:"id,omitempty"` - - // TODO: orphaned column, will be dropped by export-keys command - PrivateKey string `json:"privateKey,omitempty"` -} - -// Validate validates this model actor -func (m *ModelActor) Validate(formats strfmt.Registry) error { - return nil -} - -// ContextValidate validates this model actor based on context it is used -func (m *ModelActor) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (m *ModelActor) MarshalBinary() ([]byte, error) { - if m == nil { - return nil, nil - } - return swag.WriteJSON(m) -} - -// UnmarshalBinary interface implementation -func (m *ModelActor) UnmarshalBinary(b []byte) error { - var res ModelActor - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *m = res - return nil -} diff --git a/cmd/dataprep_test.go b/cmd/dataprep_test.go index 2d5bb49e..57012f9d 100644 --- a/cmd/dataprep_test.go +++ b/cmd/dataprep_test.go @@ -23,10 +23,8 @@ var testPreparation = model.Preparation{ DeleteAfterExport: false, MaxSize: 100, PieceSize: 200, - Actors: []model.Actor{{ - ID: "client_id", - Address: "client_address", - PrivateKey: "private_key", + Wallets: []model.Wallet{{ + Address: "client_address", KeyPath: "/tmp/key", KeyStore: "local", }}, SourceStorages: []model.Storage{{ ID: 1, @@ -225,7 +223,7 @@ func TestDataPreparationListAttachedWalletHandler(t *testing.T) { mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("ListAttachedHandler", mock.Anything, mock.Anything, mock.Anything).Return(testPreparation.Actors, nil) + mockHandler.On("ListAttachedHandler", mock.Anything, mock.Anything, mock.Anything).Return(testPreparation.Wallets, nil) _, _, err := runner.Run(ctx, "singularity prep list-wallets 1") require.NoError(t, err) diff --git a/cmd/wallet_test.go b/cmd/wallet_test.go index 36d0cfff..c1f242e1 100644 --- a/cmd/wallet_test.go +++ b/cmd/wallet_test.go @@ -32,7 +32,7 @@ func TestWalletImport(t *testing.T) { defer runner.Save(t) mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("ImportHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ + mockHandler.On("ImportKeystoreHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ ID: 1, Address: "address", }, nil) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 0756d34a..52e9c221 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -1404,7 +1404,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/model.Actor" + "$ref": "#/definitions/model.Wallet" } } }, @@ -6336,23 +6336,6 @@ const docTemplate = `{ } } }, - "model.Actor": { - "type": "object", - "properties": { - "address": { - "description": "filecoin address", - "type": "string" - }, - "id": { - "description": "actor ID (f0...)", - "type": "string" - }, - "privateKey": { - "description": "TODO: orphaned column, will be dropped by export-keys command", - "type": "string" - } - } - }, "model.Car": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 4ea0a935..d8689f27 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -1397,7 +1397,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/model.Actor" + "$ref": "#/definitions/model.Wallet" } } }, @@ -6329,23 +6329,6 @@ } } }, - "model.Actor": { - "type": "object", - "properties": { - "address": { - "description": "filecoin address", - "type": "string" - }, - "id": { - "description": "actor ID (f0...)", - "type": "string" - }, - "privateKey": { - "description": "TODO: orphaned column, will be dropped by export-keys command", - "type": "string" - } - } - }, "model.Car": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 83cbdee2..d9d366c9 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -278,18 +278,6 @@ definitions: storageId: type: integer type: object - model.Actor: - properties: - address: - description: filecoin address - type: string - id: - description: actor ID (f0...) - type: string - privateKey: - description: 'TODO: orphaned column, will be dropped by export-keys command' - type: string - type: object model.Car: properties: attachmentId: @@ -12065,7 +12053,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/model.Actor' + $ref: '#/definitions/model.Wallet' type: array "400": description: Bad Request diff --git a/handler/dataprep/remove.go b/handler/dataprep/remove.go index 07126b62..993521f6 100644 --- a/handler/dataprep/remove.go +++ b/handler/dataprep/remove.go @@ -58,7 +58,7 @@ func (DefaultHandler) RemovePreparationHandler(ctx context.Context, db *gorm.DB, // by the healthcheck worker every 5 minutes. err = database.DoRetry(ctx, func() error { return db.Transaction(func(db *gorm.DB) error { - return db.Select("Actors", "SourceStorages", "OutputStorages").Delete(&preparation).Error + return db.Select("Wallets", "SourceStorages", "OutputStorages").Delete(&preparation).Error }) }) if err != nil { diff --git a/handler/deal/list_test.go b/handler/deal/list_test.go index 17a5c568..4592fe8f 100644 --- a/handler/deal/list_test.go +++ b/handler/deal/list_test.go @@ -12,9 +12,11 @@ import ( func TestListHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + err := db.Create(&model.Actor{ID: "f01", Address: "f01"}).Error + require.NoError(t, err) + err = db.Create(&model.Preparation{ + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, SourceStorages: []model.Storage{{ Name: "storage", diff --git a/handler/deal/schedule/create.go b/handler/deal/schedule/create.go index 0a3b0d45..6c5fc0ed 100644 --- a/handler/deal/schedule/create.go +++ b/handler/deal/schedule/create.go @@ -101,7 +101,7 @@ func (DefaultHandler) CreateHandler( request.MaxPendingDealSize = "0" } var preparation model.Preparation - err := preparation.FindByIDOrName(db, request.Preparation, "Actors") + err := preparation.FindByIDOrName(db, request.Preparation, "Wallets") if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", request.Preparation) } @@ -158,8 +158,8 @@ func (DefaultHandler) CreateHandler( } } - if len(preparation.Actors) == 0 { - return nil, errors.Wrap(handlererror.ErrNotFound, "no actor attached to preparation") + if len(preparation.Wallets) == 0 { + return nil, errors.Wrap(handlererror.ErrNotFound, "no wallet attached to preparation") } var providerActor string diff --git a/handler/deal/schedule/create_test.go b/handler/deal/schedule/create_test.go index 7e692313..7c3e8022 100644 --- a/handler/deal/schedule/create_test.go +++ b/handler/deal/schedule/create_test.go @@ -182,15 +182,15 @@ func TestCreateHandler_NoAssociatedWallet(t *testing.T) { require.NoError(t, err) _, err = Default.CreateHandler(ctx, db, getMockLotusClient(), createRequest) require.ErrorIs(t, err, handlererror.ErrNotFound) - require.ErrorContains(t, err, "no actor") + require.ErrorContains(t, err, "no wallet") }) } func TestCreateHandler_InvalidProvider(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -206,8 +206,8 @@ func TestCreateHandler_InvalidProvider(t *testing.T) { func TestCreateHandler_DealSizeNotSetForCron(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -226,8 +226,8 @@ func TestCreateHandler_DealSizeNotSetForCron(t *testing.T) { func TestCreateHandler_ScheduleDealSizeSetForNonCron(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -247,8 +247,8 @@ func TestCreateHandler_Success(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ Name: "name", - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/list_test.go b/handler/deal/schedule/list_test.go index 035d012d..98675352 100644 --- a/handler/deal/schedule/list_test.go +++ b/handler/deal/schedule/list_test.go @@ -13,8 +13,8 @@ import ( func TestListHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/pause_test.go b/handler/deal/schedule/pause_test.go index ea8e5625..155fa20a 100644 --- a/handler/deal/schedule/pause_test.go +++ b/handler/deal/schedule/pause_test.go @@ -14,8 +14,8 @@ import ( func TestPauseHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/remove_test.go b/handler/deal/schedule/remove_test.go index 82721085..c7ac30ba 100644 --- a/handler/deal/schedule/remove_test.go +++ b/handler/deal/schedule/remove_test.go @@ -14,9 +14,11 @@ import ( func TestRemoveSchedule_Success(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + err := db.Create(&model.Actor{ID: "f01", Address: "f01"}).Error + require.NoError(t, err) + err = db.Create(&model.Preparation{ + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -56,8 +58,8 @@ func TestRemoveSchedule_NotExist(t *testing.T) { func TestRemoveSchedule_StillActive(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/resume_test.go b/handler/deal/schedule/resume_test.go index daecdc31..e270d85b 100644 --- a/handler/deal/schedule/resume_test.go +++ b/handler/deal/schedule/resume_test.go @@ -14,8 +14,8 @@ import ( func TestResumeHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "f01", + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/wallet/attach.go b/handler/wallet/attach.go index b7419ccd..1fd6ac58 100644 --- a/handler/wallet/attach.go +++ b/handler/wallet/attach.go @@ -2,6 +2,7 @@ package wallet import ( "context" + "strconv" "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/database" @@ -10,18 +11,17 @@ import ( "gorm.io/gorm" ) -// attaches actor to preparation for deal-making -// accepts actor ID (f0...) or wallet address/ID -// wallet must already be linked to on-chain actor +// attaches wallet to preparation for deal-making +// accepts wallet address or wallet ID func (DefaultHandler) AttachHandler( ctx context.Context, db *gorm.DB, preparationID string, - actorOrWallet string, + walletAddressOrID string, ) (*model.Preparation, error) { db = db.WithContext(ctx) var preparation model.Preparation - err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Actors") + err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Wallets") if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %s not found", preparationID) } @@ -29,48 +29,21 @@ func (DefaultHandler) AttachHandler( return nil, errors.WithStack(err) } - // try to find as actor ID first - var actor model.Actor - err = db.Where("id = ?", actorOrWallet).First(&actor).Error - if err == nil { - // found actor directly - err = database.DoRetry(ctx, func() error { - return db.Model(&preparation).Association("Actors").Append(&actor) - }) - if err != nil { - return nil, errors.WithStack(err) - } - return &preparation, nil + var w model.Wallet + q := db.Where("address = ?", walletAddressOrID) + if id, parseErr := strconv.ParseUint(walletAddressOrID, 10, 32); parseErr == nil { + q = db.Where("address = ? OR id = ?", walletAddressOrID, id) } - - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.WithStack(err) - } - - // not found as actor, try as wallet address or ID - var wallet model.Wallet - err = db.Where("address = ? OR id = ?", actorOrWallet, actorOrWallet).First(&wallet).Error + err = q.First(&w).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(handlererror.ErrNotFound, "actor or wallet %s not found", actorOrWallet) + return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", walletAddressOrID) } if err != nil { return nil, errors.WithStack(err) } - // wallet found - check if it has an actor - if wallet.ActorID == nil || *wallet.ActorID == "" { - return nil, errors.Wrapf(handlererror.ErrInvalidParameter, - "wallet %s not yet linked to on-chain actor - fund the wallet first", wallet.Address) - } - - // get the actor - err = db.Where("id = ?", *wallet.ActorID).First(&actor).Error - if err != nil { - return nil, errors.Wrapf(err, "actor %s not found", *wallet.ActorID) - } - err = database.DoRetry(ctx, func() error { - return db.Model(&preparation).Association("Actors").Append(&actor) + return db.Model(&preparation).Association("Wallets").Append(&w) }) if err != nil { return nil, errors.WithStack(err) diff --git a/handler/wallet/attach_test.go b/handler/wallet/attach_test.go index 90946f87..847f3e35 100644 --- a/handler/wallet/attach_test.go +++ b/handler/wallet/attach_test.go @@ -13,27 +13,27 @@ import ( func TestAttachHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Actor{ - ID: "test", + err := db.Create(&model.Wallet{ + Address: "f0test", KeyPath: "/tmp/key", KeyStore: "local", }).Error require.NoError(t, err) err = db.Create(&model.Preparation{}).Error require.NoError(t, err) t.Run("preparation not found", func(t *testing.T) { - _, err := Default.AttachHandler(ctx, db, "2", "test") + _, err := Default.AttachHandler(ctx, db, "2", "f0test") require.ErrorIs(t, err, handlererror.ErrNotFound) }) - t.Run("actor not found", func(t *testing.T) { + t.Run("wallet not found", func(t *testing.T) { _, err := Default.AttachHandler(ctx, db, "1", "invalid") require.ErrorIs(t, err, handlererror.ErrNotFound) }) t.Run("success", func(t *testing.T) { - preparation, err := Default.AttachHandler(ctx, db, "1", "test") + preparation, err := Default.AttachHandler(ctx, db, "1", "f0test") require.NoError(t, err) - require.Len(t, preparation.Actors, 1) + require.Len(t, preparation.Wallets, 1) }) }) } diff --git a/handler/wallet/detach.go b/handler/wallet/detach.go index a037e220..2f3f5356 100644 --- a/handler/wallet/detach.go +++ b/handler/wallet/detach.go @@ -2,6 +2,7 @@ package wallet import ( "context" + "fmt" "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/database" @@ -11,17 +12,17 @@ import ( "gorm.io/gorm" ) -// detaches actor from preparation -// accepts actor ID or address +// detaches wallet from preparation +// accepts wallet address or ID func (DefaultHandler) DetachHandler( ctx context.Context, db *gorm.DB, preparationID string, - actorIDOrAddress string, + walletAddressOrID string, ) (*model.Preparation, error) { db = db.WithContext(ctx) var preparation model.Preparation - err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Actors") + err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Wallets") if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", preparationID) } @@ -29,15 +30,15 @@ func (DefaultHandler) DetachHandler( return nil, errors.WithStack(err) } - found, err := underscore.Find(preparation.Actors, func(a model.Actor) bool { - return a.ID == actorIDOrAddress || a.Address == actorIDOrAddress + found, err := underscore.Find(preparation.Wallets, func(w model.Wallet) bool { + return w.Address == walletAddressOrID || fmt.Sprint(w.ID) == walletAddressOrID }) if err != nil { - return nil, errors.Wrapf(handlererror.ErrNotFound, "actor %s not attached to preparation %s", actorIDOrAddress, preparationID) + return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not attached to preparation %s", walletAddressOrID, preparationID) } err = database.DoRetry(ctx, func() error { - return db.Model(&preparation).Association("Actors").Delete(&found) + return db.Model(&preparation).Association("Wallets").Delete(&found) }) if err != nil { return nil, errors.WithStack(err) diff --git a/handler/wallet/detach_test.go b/handler/wallet/detach_test.go index 90a6df65..1d943548 100644 --- a/handler/wallet/detach_test.go +++ b/handler/wallet/detach_test.go @@ -14,26 +14,26 @@ import ( func TestDetachHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "test", + Wallets: []model.Wallet{{ + Address: "f0test", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) t.Run("preparation not found", func(t *testing.T) { - _, err := Default.DetachHandler(ctx, db, "2", "test") + _, err := Default.DetachHandler(ctx, db, "2", "f0test") require.ErrorIs(t, err, handlererror.ErrNotFound) }) - t.Run("actor not found", func(t *testing.T) { + t.Run("wallet not found", func(t *testing.T) { _, err := Default.DetachHandler(ctx, db, "1", "invalid") require.ErrorIs(t, err, handlererror.ErrNotFound) }) t.Run("success", func(t *testing.T) { - preparation, err := Default.DetachHandler(ctx, db, "1", "test") + preparation, err := Default.DetachHandler(ctx, db, "1", "f0test") require.NoError(t, err) - require.Len(t, preparation.Actors, 0) + require.Len(t, preparation.Wallets, 0) }) }) } diff --git a/handler/wallet/interface.go b/handler/wallet/interface.go index c6fd405f..8c888400 100644 --- a/handler/wallet/interface.go +++ b/handler/wallet/interface.go @@ -37,7 +37,7 @@ type Handler interface { ctx context.Context, db *gorm.DB, preparation string, - ) ([]model.Actor, error) + ) ([]model.Wallet, error) RemoveHandler( ctx context.Context, db *gorm.DB, @@ -75,9 +75,9 @@ func (m *MockWallet) ListHandler(ctx context.Context, db *gorm.DB) ([]model.Wall return args.Get(0).([]model.Wallet), args.Error(1) } -func (m *MockWallet) ListAttachedHandler(ctx context.Context, db *gorm.DB, preparation string) ([]model.Actor, error) { +func (m *MockWallet) ListAttachedHandler(ctx context.Context, db *gorm.DB, preparation string) ([]model.Wallet, error) { args := m.Called(ctx, db, preparation) - return args.Get(0).([]model.Actor), args.Error(1) + return args.Get(0).([]model.Wallet), args.Error(1) } func (m *MockWallet) RemoveHandler(ctx context.Context, db *gorm.DB, address string) error { diff --git a/handler/wallet/listattached.go b/handler/wallet/listattached.go index 65165c83..ff8f3490 100644 --- a/handler/wallet/listattached.go +++ b/handler/wallet/listattached.go @@ -9,28 +9,13 @@ import ( "gorm.io/gorm" ) -// ListAttachedHandler fetches and returns a list of wallets associated with a given preparation, -// identified by either its ID or name. -// -// The function looks for the preparation with the specified ID or name in the database. If found, -// it retrieves all wallets associated with that preparation. If no such preparation is found, -// an error is returned. -// -// Parameters: -// - ctx: The context in which the handler function is executed, used for controlling cancellation. -// - db: A pointer to a gorm.DB object, which provides database access. -// - preparationID: The ID or name of the preparation whose attached wallets need to be fetched. -// -// Returns: -// - A slice of model.Wallet objects that are attached to the specified preparation. -// - An error if any issues arise during the process or if the preparation is not found, otherwise nil. func (DefaultHandler) ListAttachedHandler( ctx context.Context, db *gorm.DB, preparationID string, -) ([]model.Actor, error) { +) ([]model.Wallet, error) { var preparation model.Preparation - err := preparation.FindByIDOrName(db, preparationID, "Actors") + err := preparation.FindByIDOrName(db, preparationID, "Wallets") if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", preparationID) } @@ -38,7 +23,7 @@ func (DefaultHandler) ListAttachedHandler( return nil, errors.WithStack(err) } - return preparation.Actors, nil + return preparation.Wallets, nil } // @ID ListAttachedWallets @@ -47,7 +32,7 @@ func (DefaultHandler) ListAttachedHandler( // @Produce json // @Accept json // @Param id path string true "Preparation ID or name" -// @Success 200 {array} model.Actor +// @Success 200 {array} model.Wallet // @Failure 400 {object} api.HTTPError // @Failure 500 {object} api.HTTPError // @Router /preparation/{id}/wallet [get] diff --git a/handler/wallet/listattached_test.go b/handler/wallet/listattached_test.go index 89de17d6..94a7321b 100644 --- a/handler/wallet/listattached_test.go +++ b/handler/wallet/listattached_test.go @@ -14,8 +14,8 @@ import ( func TestListAttachedHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Actors: []model.Actor{{ - ID: "test", + Wallets: []model.Wallet{{ + Address: "f0test", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -26,9 +26,9 @@ func TestListAttachedHandler(t *testing.T) { }) t.Run("success", func(t *testing.T) { - actors, err := Default.ListAttachedHandler(ctx, db, "1") + wallets, err := Default.ListAttachedHandler(ctx, db, "1") require.NoError(t, err) - require.Len(t, actors, 1) + require.Len(t, wallets, 1) }) }) } diff --git a/model/preparation.go b/model/preparation.go index 87cec792..e1937c28 100644 --- a/model/preparation.go +++ b/model/preparation.go @@ -46,7 +46,7 @@ type Preparation struct { NoDag bool `json:"noDag"` // Associations - Actors []Actor `gorm:"many2many:actor_assignments;constraint:OnDelete:CASCADE;joinForeignKey:PreparationID;joinReferences:ActorID" json:"actors,omitempty" swaggerignore:"true" table:"expand"` // TODO: GORM will rename wallet_assignments→actor_assignments + Wallets []Wallet `gorm:"many2many:wallet_assignments;constraint:OnDelete:CASCADE" json:"wallets,omitempty" swaggerignore:"true" table:"expand"` SourceStorages []Storage `gorm:"many2many:source_attachments;constraint:OnDelete:CASCADE" json:"sourceStorages,omitempty" table:"expand;header:Source Storages:"` OutputStorages []Storage `gorm:"many2many:output_attachments;constraint:OnDelete:CASCADE" json:"outputStorages,omitempty" table:"expand;header:Output Storages:"` } diff --git a/replication/wallet.go b/replication/wallet.go index a099ea64..3a0aed0f 100644 --- a/replication/wallet.go +++ b/replication/wallet.go @@ -19,29 +19,29 @@ import ( var logger = logging.Logger("replication") type WalletChooser interface { - Choose(ctx context.Context, actors []model.Actor) (model.Actor, error) + Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) } type RandomWalletChooser struct{} -var ErrNoWallet = errors.New("no actors to choose from") +var ErrNoWallet = errors.New("no wallets to choose from") -var ErrNoDatacap = errors.New("no actors have enough datacap") +var ErrNoDatacap = errors.New("no wallets have enough datacap") -// randomly selects an actor using cryptographically secure random number generator -func (w RandomWalletChooser) Choose(ctx context.Context, actors []model.Actor) (model.Actor, error) { - if len(actors) == 0 { - return model.Actor{}, ErrNoWallet +func (w RandomWalletChooser) Choose(_ context.Context, wallets []model.Wallet) (model.Wallet, error) { + if len(wallets) == 0 { + return model.Wallet{}, ErrNoWallet } - randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(actors)))) + randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(wallets)))) if err != nil { - return model.Actor{}, errors.WithStack(err) + return model.Wallet{}, errors.WithStack(err) } - chosen := actors[randomPick.Int64()] - return chosen, nil + return wallets[randomPick.Int64()], nil } +// DatacapWalletChooser selects a wallet whose linked actor has sufficient datacap. +// only meaningful for market deals where datacap matters. type DatacapWalletChooser struct { db *gorm.DB cache *ttlcache.Cache[string, int64] @@ -50,7 +50,7 @@ type DatacapWalletChooser struct { } func NewDatacapWalletChooser(db *gorm.DB, cacheTTL time.Duration, - lotusAPI string, lotusToken string, min uint64, //nolint:predeclared // We're ok with using the same name as the predeclared identifier here + lotusAPI string, lotusToken string, min uint64, //nolint:predeclared ) DatacapWalletChooser { cache := ttlcache.New[string, int64]( ttlcache.WithTTL[string, int64](cacheTTL), @@ -65,77 +65,81 @@ func NewDatacapWalletChooser(db *gorm.DB, cacheTTL time.Duration, } } -func (w DatacapWalletChooser) getDatacap(ctx context.Context, actor model.Actor) (int64, error) { +func (w DatacapWalletChooser) getDatacap(ctx context.Context, wallet model.Wallet) (int64, error) { + if wallet.ActorID == nil { + return 0, errors.Newf("wallet %s has no linked actor", wallet.Address) + } var result string - err := w.lotusClient.CallFor(ctx, &result, "Filecoin.StateMarketBalance", actor.Address, nil) + err := w.lotusClient.CallFor(ctx, &result, "Filecoin.StateMarketBalance", wallet.Address, nil) if err != nil { return 0, errors.WithStack(err) } return strconv.ParseInt(result, 10, 64) } -func (w DatacapWalletChooser) getDatacapCached(ctx context.Context, actor model.Actor) (int64, error) { - file := w.cache.Get(actor.Address) +func (w DatacapWalletChooser) getDatacapCached(ctx context.Context, wallet model.Wallet) (int64, error) { + file := w.cache.Get(wallet.Address) if file != nil && !file.IsExpired() { return file.Value(), nil } - datacap, err := w.getDatacap(ctx, actor) + datacap, err := w.getDatacap(ctx, wallet) if err != nil { - logger.Errorf("failed to get datacap for actor %s: %s", actor.Address, err) + logger.Errorf("failed to get datacap for wallet %s: %s", wallet.Address, err) if file != nil { return file.Value(), nil } return 0, errors.WithStack(err) } - w.cache.Set(actor.Address, datacap, ttlcache.DefaultTTL) + w.cache.Set(wallet.Address, datacap, ttlcache.DefaultTTL) return datacap, nil } -func (w DatacapWalletChooser) getPendingDeals(ctx context.Context, actor model.Actor) (int64, error) { +func (w DatacapWalletChooser) getPendingDeals(ctx context.Context, wallet model.Wallet) (int64, error) { + if wallet.ActorID == nil { + return 0, nil + } var totalPieceSize int64 err := w.db.WithContext(ctx).Model(&model.Deal{}). Select("COALESCE(SUM(piece_size), 0)"). - Where("client_id = ? AND verified AND state = ?", actor.ID, model.DealProposed). + Where("client_id = ? AND verified AND state = ?", *wallet.ActorID, model.DealProposed). Scan(&totalPieceSize). Error if err != nil { - logger.Errorf("failed to get pending deals for actor %s: %s", actor.Address, err) + logger.Errorf("failed to get pending deals for wallet %s: %s", wallet.Address, err) return 0, errors.WithStack(err) } return totalPieceSize, nil } -// selects random actor with sufficient datacap (datacap - pending deals >= min threshold) -func (w DatacapWalletChooser) Choose(ctx context.Context, actors []model.Actor) (model.Actor, error) { - if len(actors) == 0 { - return model.Actor{}, ErrNoWallet +func (w DatacapWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) { + if len(wallets) == 0 { + return model.Wallet{}, ErrNoWallet } - var eligible []model.Actor - for _, actor := range actors { - datacap, err := w.getDatacapCached(ctx, actor) + var eligible []model.Wallet + for _, wallet := range wallets { + datacap, err := w.getDatacapCached(ctx, wallet) if err != nil { - logger.Errorw("failed to get datacap for actor", "actor", actor.Address, "error", err) + logger.Errorw("failed to get datacap for wallet", "address", wallet.Address, "error", err) continue } - pendingDeals, err := w.getPendingDeals(ctx, actor) + pendingDeals, err := w.getPendingDeals(ctx, wallet) if err != nil { - logger.Errorw("failed to get pending deals for actor", "actor", actor.Address, "error", err) + logger.Errorw("failed to get pending deals for wallet", "address", wallet.Address, "error", err) continue } if datacap-pendingDeals >= int64(w.min) { - eligible = append(eligible, actor) + eligible = append(eligible, wallet) } } if len(eligible) == 0 { - return model.Actor{}, ErrNoDatacap + return model.Wallet{}, ErrNoDatacap } randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(eligible)))) if err != nil { - return model.Actor{}, errors.WithStack(err) + return model.Wallet{}, errors.WithStack(err) } - chosen := eligible[randomPick.Int64()] - return chosen, nil + return eligible[randomPick.Int64()], nil } diff --git a/replication/wallet_test.go b/replication/wallet_test.go index 55af81b5..6fd5d820 100644 --- a/replication/wallet_test.go +++ b/replication/wallet_test.go @@ -8,6 +8,7 @@ import ( "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/gotidy/ptr" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/ybbus/jsonrpc/v3" @@ -46,31 +47,38 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { lotusClient := new(MockRPCClient) - // Set up the test data - wallets := []model.Actor{ - {ID: "1", Address: "address1"}, - {ID: "2", Address: "address2"}, - {ID: "3", Address: "address3"}, - {ID: "4", Address: "address4"}, + ids := []string{"a", "b", "c", "d"} + actors := make([]model.Actor, len(ids)) + for i, id := range ids { + actors[i] = model.Actor{ID: "actor" + id, Address: "address" + id} + } + require.NoError(t, db.Create(&actors).Error) + + wallets := make([]model.Wallet, len(ids)) + for i, id := range ids { + wallets[i] = model.Wallet{ + Address: "address" + id, KeyPath: "/tmp/key-" + id, + KeyStore: "local", ActorID: ptr.String("actor" + id), + } + require.NoError(t, db.Create(&wallets[i]).Error) } - // Set up expectations for the lotusClient mock - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address1", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressa", nil}). Return(nil).Run(func(args mock.Arguments) { resultPtr := args.Get(1).(*string) *resultPtr = "1000000" }) - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address2", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressb", nil}). Return(errors.New("failed to get datacap")) - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address3", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressc", nil}). Return(nil).Run(func(args mock.Arguments) { resultPtr := args.Get(1).(*string) *resultPtr = "1000000" }) - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address4", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressd", nil}). Return(nil).Run(func(args mock.Arguments) { resultPtr := args.Get(1).(*string) *resultPtr = "900000" @@ -79,29 +87,26 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { chooser := NewDatacapWalletChooser(db, time.Minute, "lotusAPI", "lotusToken", 900001) chooser.lotusClient = lotusClient - err := db.Create(&wallets).Error - require.NoError(t, err) - err = db.Create(&model.Deal{ - ClientID: "3", + require.NoError(t, db.Create(&model.Deal{ + ClientID: "actorc", Verified: true, State: model.DealProposed, PieceSize: 500000, - }).Error - require.NoError(t, err) + }).Error) t.Run("Choose wallet with empty wallet", func(t *testing.T) { - _, err := chooser.Choose(context.Background(), []model.Actor{}) + _, err := chooser.Choose(context.Background(), []model.Wallet{}) require.ErrorAs(t, err, &ErrNoWallet) }) t.Run("Choose wallet with sufficient datacap", func(t *testing.T) { - chosenWallet, err := chooser.Choose(context.Background(), []model.Actor{wallets[0], wallets[1]}) + chosenWallet, err := chooser.Choose(context.Background(), []model.Wallet{wallets[0], wallets[1]}) require.NoError(t, err) - require.Equal(t, "address1", chosenWallet.Address) + require.Equal(t, "addressa", chosenWallet.Address) }) t.Run("Choose wallet with insufficient datacap", func(t *testing.T) { - _, err := chooser.Choose(context.Background(), []model.Actor{wallets[2], wallets[3]}) + _, err := chooser.Choose(context.Background(), []model.Wallet{wallets[2], wallets[3]}) require.ErrorAs(t, err, &ErrNoDatacap) }) }) @@ -110,9 +115,9 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { func TestRandomWalletChooser(t *testing.T) { chooser := &RandomWalletChooser{} ctx := context.Background() - wallet, err := chooser.Choose(ctx, []model.Actor{ - {ID: "1", Address: "address1"}, - {ID: "2", Address: "address2"}, + wallet, err := chooser.Choose(ctx, []model.Wallet{ + {Address: "address1"}, + {Address: "address2"}, }) require.NoError(t, err) require.Contains(t, wallet.Address, "address") diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index 9a0fb4db..7cfaa78b 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -296,7 +296,7 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) } var car model.Car var dealModel *model.Deal - var actorObj model.Actor + var walletObj model.Wallet if schedule.MaxPendingDealNumber > 0 && pending.DealNumber >= schedule.MaxPendingDealNumber { Logger.Infow("skipping this time since the max pending deal is reached", "schedule_id", schedule.ID) goto waitForPending @@ -366,18 +366,23 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) return model.ScheduleError, errors.Wrap(err, "failed to find car") } - actorObj, err = d.walletChooser.Choose(ctx, schedule.Preparation.Actors) + walletObj, err = d.walletChooser.Choose(ctx, schedule.Preparation.Wallets) if err != nil { return model.ScheduleError, errors.Wrap(err, "failed to choose wallet") } - // resolve wallet for signing before entering retry loop - walletRecord, err := wallet.LoadWalletByActorID(ctx, d.dbNoContext, actorObj.ID) + // market deals need the on-chain actor for the deal proposal + if walletObj.ActorID == nil { + return model.ScheduleError, errors.Newf("wallet %s has no linked actor", walletObj.Address) + } + var actorObj model.Actor + err = db.Where("id = ?", *walletObj.ActorID).First(&actorObj).Error if err != nil { - return model.ScheduleError, errors.Wrapf(err, "failed to load wallet for actor %s", actorObj.ID) + return model.ScheduleError, errors.Wrapf(err, "failed to load actor %s for wallet %s", *walletObj.ActorID, walletObj.Address) } + proposalSigner := replication.ProposalSigner(func(msg []byte) (*crypto.Signature, error) { - return wallet.SignWithWallet(d.keyStore, *walletRecord, msg) + return wallet.SignWithWallet(d.keyStore, walletObj, msg) }) err = retry.Do(func() error { @@ -551,7 +556,7 @@ func (d *DealPusher) runOnce(ctx context.Context) { scheduleMap := map[model.ScheduleID]model.Schedule{} Logger.Debugw("getting schedules") db := d.dbNoContext.WithContext(ctx) - err := db.Preload("Preparation.Actors").Where("state = ?", + err := db.Preload("Preparation.Wallets").Where("state = ?", model.ScheduleActive).Find(&schedules).Error if err != nil { Logger.Errorw("failed to get schedules", "error", err) diff --git a/service/dealpusher/dealpusher_test.go b/service/dealpusher/dealpusher_test.go index bc26c259..3c92bec8 100644 --- a/service/dealpusher/dealpusher_test.go +++ b/service/dealpusher/dealpusher_test.go @@ -28,14 +28,11 @@ func init() { analytics.Enabled = false } -// creates a wallet record so that signer resolution in runSchedule succeeds. -// must be called after the actor record exists (e.g. after schedule creation). -func createTestWallet(t *testing.T, db *gorm.DB, actorID string) { +// creates actor record that wallets reference via ActorID FK. +// must be called before schedule creation since wallet.ActorID references actor. +func createTestActor(t *testing.T, db *gorm.DB, actorID string) { t.Helper() - require.NoError(t, db.Create(&model.Wallet{ - KeyPath: "/tmp/fake-key", KeyStore: "local", - Address: "f0xx", ActorID: &actorID, - }).Error) + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f0xx"}).Error) } type MockDealMaker struct { @@ -120,9 +117,10 @@ func TestDealMakerService_FailtoSend(t *testing.T) { schedule := model.Schedule{ Preparation: &model.Preparation{ SourceStorages: []model.Storage{{}}, - Actors: []model.Actor{ + Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }}, State: model.ScheduleActive, @@ -131,9 +129,9 @@ func TestDealMakerService_FailtoSend(t *testing.T) { MaxPendingDealSize: 2048, TotalDealNumber: 4, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) - createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("send deal error")) pieceCIDs := []model.CID{ model.CID(calculateCommp(t, generateRandomBytes(1000), 1024)), @@ -177,9 +175,10 @@ func TestDealMakerService_Cron(t *testing.T) { schedule := model.Schedule{ Preparation: &model.Preparation{ SourceStorages: []model.Storage{{}}, - Actors: []model.Actor{ + Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }}, State: model.ScheduleActive, @@ -187,9 +186,9 @@ func TestDealMakerService_Cron(t *testing.T) { ScheduleDealSize: 1, Provider: provider, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) - createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, @@ -273,9 +272,10 @@ func TestDealMakerService_ScheduleWithConstraints(t *testing.T) { schedule := model.Schedule{ Preparation: &model.Preparation{ SourceStorages: []model.Storage{{}}, - Actors: []model.Actor{ + Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }}, State: model.ScheduleActive, @@ -284,9 +284,9 @@ func TestDealMakerService_ScheduleWithConstraints(t *testing.T) { MaxPendingDealSize: 2048, TotalDealNumber: 4, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) - createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, }, nil) @@ -383,9 +383,10 @@ func TestDealmakerService_Force(t *testing.T) { client := "f0client" schedule := model.Schedule{ Preparation: &model.Preparation{ - Actors: []model.Actor{ + Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }, SourceStorages: []model.Storage{{}}, @@ -394,9 +395,9 @@ func TestDealmakerService_Force(t *testing.T) { Provider: provider, Force: true, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) - createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, }, nil) @@ -443,9 +444,10 @@ func TestDealMakerService_MaxReplica(t *testing.T) { client := "f0client" schedule := model.Schedule{ Preparation: &model.Preparation{ - Actors: []model.Actor{ + Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }, SourceStorages: []model.Storage{{}}, @@ -453,9 +455,9 @@ func TestDealMakerService_MaxReplica(t *testing.T) { State: model.ScheduleActive, Provider: provider, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) - createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, }, nil) @@ -510,9 +512,10 @@ func TestDealMakerService_NewScheduleOneOff(t *testing.T) { client := "f0client" schedule := model.Schedule{ Preparation: &model.Preparation{ - Actors: []model.Actor{ + Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }, SourceStorages: []model.Storage{{}}, @@ -521,9 +524,9 @@ func TestDealMakerService_NewScheduleOneOff(t *testing.T) { Provider: provider, AllowedPieceCIDs: underscore.Map(pieceCIDs[:5], func(cid model.CID) string { return cid.String() }), } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) - createTestWallet(t, db, client) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ ScheduleID: &schedule.ID, diff --git a/testdb/main.go b/testdb/main.go index 7859330e..388af7e5 100644 --- a/testdb/main.go +++ b/testdb/main.go @@ -74,22 +74,33 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Type: "local", Path: urlToPath(gofakeit.URL()), } - // Setup actor + // Setup wallet (with optional actor for market deals) + actorID := fmt.Sprintf("f0%d", r.Intn(10000)) actor := model.Actor{ - ID: fmt.Sprintf("f0%d", r.Intn(10000)), + ID: actorID, Address: "f1" + randomLetterString(39), } + w := model.Wallet{ + Address: "f1" + randomLetterString(39), + KeyPath: "/tmp/fake-key", + KeyStore: "local", + ActorID: &actorID, + } // Setup preparation preparation := model.Preparation{ Name: gofakeit.AppName(), MaxSize: 30 << 30, PieceSize: 1 << 35, - Actors: []model.Actor{actor}, + Wallets: []model.Wallet{w}, SourceStorages: []model.Storage{source}, } - err := db.Create(&preparation).Error + err := db.Create(&actor).Error + if err != nil { + return errors.WithStack(err) + } + err = db.Create(&preparation).Error if err != nil { return errors.WithStack(err) } diff --git a/util/keystore/keystore_test.go b/util/keystore/keystore_test.go index 28f93108..edb5cbd7 100644 --- a/util/keystore/keystore_test.go +++ b/util/keystore/keystore_test.go @@ -24,13 +24,6 @@ func getTestKey(modifier int) string { return baseKey } -// Alternative test keys (pre-generated, valid lotus format) -var testKeys = []string{ - testutil.TestPrivateKeyHex, - // We only have one test key in testutil, so for multi-key tests we'll use the same one - // This is fine for testing keystore functionality -} - func TestLocalKeyStore_PutAndGet(t *testing.T) { tmpdir := t.TempDir() ks, err := NewLocalKeyStore(tmpdir) From 710ebb11b78617e7ec7223dd2e2f960974af35d0 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Wed, 25 Feb 2026 12:31:26 +0100 Subject: [PATCH 15/15] PR feedback --- api/api_test.go | 4 +- cmd/deal/send-manual.go | 10 +- cmd/deal_test.go | 4 +- cmd/wallet/remove.go | 7 +- cmd/wallet_test.go | 4 +- handler/deal/interface.go | 7 +- handler/deal/send-manual.go | 21 ++-- handler/deal/send-manual_test.go | 118 ++++++-------------- handler/wallet/interface.go | 5 +- handler/wallet/remove.go | 49 ++++----- handler/wallet/remove_test.go | 12 ++- handler/wallet/sign_test.go | 179 +++++++++++++++++++++++++++++++ model/migrate.go | 1 + service/dealpusher/dealpusher.go | 16 +-- 14 files changed, 287 insertions(+), 150 deletions(-) create mode 100644 handler/wallet/sign_test.go diff --git a/api/api_test.go b/api/api_test.go index 459f4e6f..a7fd2c87 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -98,7 +98,7 @@ func setupMockDeal() deal.Handler { m := new(deal.MockDeal) m.On("ListHandler", mock.Anything, mock.Anything, mock.Anything). Return([]model.Deal{{}}, nil) - m.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + m.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(&model.Deal{}, nil) return m } @@ -193,7 +193,7 @@ func setupMockWallet() wallet.Handler { Return([]model.Wallet{{}}, nil) m.On("ListAttachedHandler", mock.Anything, mock.Anything, "id"). Return([]model.Wallet{{}}, nil) - m.On("RemoveHandler", mock.Anything, mock.Anything, "wallet"). + m.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything, "wallet"). Return(nil) return m } diff --git a/cmd/deal/send-manual.go b/cmd/deal/send-manual.go index 16ba2c4c..46bb1a17 100644 --- a/cmd/deal/send-manual.go +++ b/cmd/deal/send-manual.go @@ -184,13 +184,9 @@ Notes: return errors.Wrap(err, "failed to init keystore") } - dealMaker := replication.NewDealMaker( - util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")), - h, - 10*timeout, - timeout, - ) - dealModel, err := deal.Default.SendManualHandler(ctx, db, ks, dealMaker, proposal) + lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")) + dealMaker := replication.NewDealMaker(lotusClient, h, 10*timeout, timeout) + dealModel, err := deal.Default.SendManualHandler(ctx, db, ks, lotusClient, dealMaker, proposal) if err != nil { return errors.WithStack(err) } diff --git a/cmd/deal_test.go b/cmd/deal_test.go index c1a32a56..35c93654 100644 --- a/cmd/deal_test.go +++ b/cmd/deal_test.go @@ -30,7 +30,7 @@ func TestSendDealHandler(t *testing.T) { defer runner.Save(t) mockHandler := new(deal.MockDeal) defer swapDealHandler(mockHandler)() - mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ + mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ State: "proposed", Provider: "f01", ProposalID: "proposal_id", @@ -46,7 +46,7 @@ func TestSendDealHandler(t *testing.T) { }, nil).Once() _, _, err = runner.Run(ctx, "singularity deal send-manual --client client --provider provider --piece-cid piece_cid --piece-size 1024 --save") require.NoError(t, err) - mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ + mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ State: "proposed", Provider: "f01", ProposalID: "proposal_id", diff --git a/cmd/wallet/remove.go b/cmd/wallet/remove.go index 859e2dca..0d112547 100644 --- a/cmd/wallet/remove.go +++ b/cmd/wallet/remove.go @@ -5,6 +5,7 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/wallet" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/urfave/cli/v2" ) @@ -25,6 +26,10 @@ var RemoveCmd = &cli.Command{ return errors.WithStack(err) } defer closer.Close() - return wallet.Default.RemoveHandler(c.Context, db, c.Args().Get(0)) + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return errors.WithStack(err) + } + return wallet.Default.RemoveHandler(c.Context, db, ks, c.Args().Get(0)) }, } diff --git a/cmd/wallet_test.go b/cmd/wallet_test.go index c1f242e1..96762c03 100644 --- a/cmd/wallet_test.go +++ b/cmd/wallet_test.go @@ -69,7 +69,7 @@ func TestWalletRemove(t *testing.T) { defer runner.Save(t) mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) _, _, err := runner.Run(ctx, "singularity wallet remove --really-do-it xxx") require.NoError(t, err) }) @@ -81,7 +81,7 @@ func TestWalletRemove_NoReallyDoIt(t *testing.T) { defer runner.Save(t) mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) _, _, err := runner.Run(ctx, "singularity wallet remove xxx") require.ErrorIs(t, err, cliutil.ErrReallyDoIt) }) diff --git a/handler/deal/interface.go b/handler/deal/interface.go index 6bf71a3c..42aed347 100644 --- a/handler/deal/interface.go +++ b/handler/deal/interface.go @@ -8,12 +8,13 @@ import ( "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/util/keystore" "github.com/stretchr/testify/mock" + "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) type Handler interface { ListHandler(ctx context.Context, db *gorm.DB, request ListDealRequest) ([]model.Deal, error) - SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) + SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, lotusClient jsonrpc.RPCClient, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) } type DefaultHandler struct{} @@ -31,7 +32,7 @@ func (m *MockDeal) ListHandler(ctx context.Context, db *gorm.DB, request ListDea return args.Get(0).([]model.Deal), args.Error(1) } -func (m *MockDeal) SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) { - args := m.Called(ctx, db, ks, dealMaker, request) +func (m *MockDeal) SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, lotusClient jsonrpc.RPCClient, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) { + args := m.Called(ctx, db, ks, lotusClient, dealMaker, request) return args.Get(0).(*model.Deal), args.Error(1) } diff --git a/handler/deal/send-manual.go b/handler/deal/send-manual.go index ff87207e..246e839c 100644 --- a/handler/deal/send-manual.go +++ b/handler/deal/send-manual.go @@ -16,6 +16,7 @@ import ( "github.com/dustin/go-humanize" "github.com/filecoin-project/go-state-types/crypto" "github.com/ipfs/go-cid" + "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) @@ -70,15 +71,16 @@ func (DefaultHandler) SendManualHandler( ctx context.Context, db *gorm.DB, ks keystore.KeyStore, + lotusClient jsonrpc.RPCClient, dealMaker replication.DealMaker, request Proposal, ) (*model.Deal, error) { db = db.WithContext(ctx) - // get the actor object - actor := model.Actor{} - err := db.Where("id = ? OR address = ?", request.ClientAddress, request.ClientAddress).First(&actor).Error + // find wallet by address, then lazily resolve actor for the deal proposal + var walletObj model.Wallet + err := db.Where("address = ?", request.ClientAddress).First(&walletObj).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(handlererror.ErrNotFound, "client address %s not found", request.ClientAddress) + return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", request.ClientAddress) } if err != nil { return nil, errors.WithStack(err) @@ -140,16 +142,17 @@ func (DefaultHandler) SendManualHandler( Duration: duration, } - // resolve wallet for signing - walletRecord, err := wallet.LoadWalletByActorID(ctx, db, actor.ID) + // resolve actor lazily — only makes RPC call if ActorID not yet linked + actor, err := wallet.GetOrCreateActor(ctx, db, lotusClient, &walletObj) if err != nil { - return nil, errors.Wrapf(err, "failed to load wallet for actor %s", actor.ID) + return nil, errors.Wrapf(err, "failed to resolve actor for wallet %s", walletObj.Address) } + signer := replication.ProposalSigner(func(msg []byte) (*crypto.Signature, error) { - return wallet.SignWithWallet(ks, *walletRecord, msg) + return wallet.SignWithWallet(ks, walletObj, msg) }) - dealModel, err := dealMaker.MakeDeal(ctx, actor, car, dealConfig, signer) + dealModel, err := dealMaker.MakeDeal(ctx, *actor, car, dealConfig, signer) if err != nil { return nil, errors.WithStack(err) } diff --git a/handler/deal/send-manual_test.go b/handler/deal/send-manual_test.go index db69bbc1..08318b91 100644 --- a/handler/deal/send-manual_test.go +++ b/handler/deal/send-manual_test.go @@ -39,152 +39,111 @@ var proposal = Proposal{ FileSize: 1000, } -func TestSendManualHandler_WalletNotFound(t *testing.T) { - actor := model.Actor{ - ID: "f09999", - Address: "f10000", +// creates a wallet (and optionally an actor) that matches proposal.ClientAddress +func createTestWalletAndActor(t *testing.T, db *gorm.DB, withActor bool) { + t.Helper() + actorID := "f01000" + if withActor { + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f01000"}).Error) } + w := model.Wallet{ + Address: "f01000", KeyPath: "/tmp/key-manual", KeyStore: "local", + } + if withActor { + w.ActorID = &actorID + } + require.NoError(t, db.Create(&w).Error) +} +func TestSendManualHandler_WalletNotFound(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, proposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, proposal) require.ErrorIs(t, err, handlererror.ErrNotFound) - require.ErrorContains(t, err, "client address") + require.ErrorContains(t, err, "wallet") }) } func TestSendManualHandler_InvalidPieceCID(t *testing.T) { - actor := model.Actor{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) badProposal := proposal badProposal.PieceCID = "bad" - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece CID") }) } func TestSendManualHandler_InvalidPieceCID_NOTCOMMP(t *testing.T) { - actor := model.Actor{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) badProposal := proposal badProposal.PieceCID = proposal.RootCID - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be commp") }) } func TestSendManualHandler_InvalidPieceSize(t *testing.T) { - actor := model.Actor{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) badProposal := proposal badProposal.PieceSize = "aaa" - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece size") }) } func TestSendManualHandler_InvalidPieceSize_NotPowerOfTwo(t *testing.T) { - actor := model.Actor{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) badProposal := proposal badProposal.PieceSize = "31GiB" - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be a power of 2") }) } func TestSendManualHandler_InvalidRootCID(t *testing.T) { - actor := model.Actor{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) badProposal := proposal badProposal.RootCID = "xxxx" - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid root CID") }) } func TestSendManualHandler_InvalidDuration(t *testing.T) { - actor := model.Actor{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) badProposal := proposal badProposal.Duration = "xxxx" - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid duration") }) } func TestSendManualHandler_InvalidStartDelay(t *testing.T) { - actor := model.Actor{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) badProposal := proposal badProposal.StartDelay = "xxxx" - _, err = Default.SendManualHandler(ctx, db, nil, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid start delay") }) @@ -194,19 +153,11 @@ func TestSendManualHandler(t *testing.T) { actorID := "f01000" actor := model.Actor{ ID: actorID, - Address: "f10000", + Address: "f01000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&actor).Error - require.NoError(t, err) - - // wallet record needed for signer resolution - err = db.Create(&model.Wallet{ - KeyPath: "/tmp/fake-key", KeyStore: "local", - Address: "f10000", ActorID: &actorID, - }).Error - require.NoError(t, err) + createTestWalletAndActor(t, db, true) mockDealMaker := new(MockDealMaker) mockDealMaker.On("MakeDeal", mock.Anything, actor, mock.Anything, replication.DealConfig{ @@ -222,7 +173,8 @@ func TestSendManualHandler(t *testing.T) { PricePerGB: proposal.PricePerGB, PricePerGBEpoch: proposal.PricePerGBEpoch, }).Return(&model.Deal{}, nil) - resp, err := Default.SendManualHandler(ctx, db, nil, mockDealMaker, proposal) + // lotusClient is nil — GetOrCreateActor won't call lotus because ActorID is already set + resp, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, proposal) mockDealMaker.AssertExpectations(t) require.NoError(t, err) require.NotNil(t, resp) diff --git a/handler/wallet/interface.go b/handler/wallet/interface.go index 8c888400..742efb84 100644 --- a/handler/wallet/interface.go +++ b/handler/wallet/interface.go @@ -41,6 +41,7 @@ type Handler interface { RemoveHandler( ctx context.Context, db *gorm.DB, + ks keystore.KeyStore, address string, ) error } @@ -80,7 +81,7 @@ func (m *MockWallet) ListAttachedHandler(ctx context.Context, db *gorm.DB, prepa return args.Get(0).([]model.Wallet), args.Error(1) } -func (m *MockWallet) RemoveHandler(ctx context.Context, db *gorm.DB, address string) error { - args := m.Called(ctx, db, address) +func (m *MockWallet) RemoveHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, address string) error { + args := m.Called(ctx, db, ks, address) return args.Error(0) } diff --git a/handler/wallet/remove.go b/handler/wallet/remove.go index 2953b958..2369cf2e 100644 --- a/handler/wallet/remove.go +++ b/handler/wallet/remove.go @@ -7,47 +7,42 @@ import ( "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/handlererror" "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/keystore" "gorm.io/gorm" ) -// TODO(#590): Clarify semantics of wallet remove after wallet/actor separation -// Before separation: removed Wallet (which contained both key and actor ID) -// After separation: Wallet = keystore entry, Actor = on-chain identity -// Should this: -// - Remove Wallet record only (keystore reference)? -// - Remove Actor record only (stop tracking deals)? -// - Remove both? -// - Delete the actual keystore file? -// -// Currently: Removes Actor record (temporary fix to match test expectations) -// -// RemoveHandler deletes an actor from the database based on its address or ID. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - address: The address or ID of the actor to be deleted. -// -// Returns: -// - An error, if any occurred during the database deletion operation. +// removes wallet record and keystore file +// does not remove the associated actor — actors may be shared or tracked independently func (DefaultHandler) RemoveHandler( ctx context.Context, db *gorm.DB, + ks keystore.KeyStore, address string, ) error { db = db.WithContext(ctx) - var affected int64 - err := database.DoRetry(ctx, func() error { - tx := db.Where("address = ? OR id = ?", address, address).Delete(&model.Actor{}) - affected = tx.RowsAffected - return tx.Error + var w model.Wallet + err := db.Where("address = ?", address).First(&w).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", address) + } + if err != nil { + return errors.WithStack(err) + } + + err = database.DoRetry(ctx, func() error { + return db.Delete(&w).Error }) if err != nil { return errors.WithStack(err) } - if affected == 0 { - return errors.Wrapf(handlererror.ErrNotFound, "actor %s not found", address) + + // best-effort keystore cleanup + if ks != nil && ks.Has(w.KeyPath) { + if delErr := ks.Delete(w.KeyPath); delErr != nil { + logger.Warnw("failed to delete key file", "path", w.KeyPath, "err", delErr) + } } + return nil } diff --git a/handler/wallet/remove_test.go b/handler/wallet/remove_test.go index bf6b1580..2fef0fd5 100644 --- a/handler/wallet/remove_test.go +++ b/handler/wallet/remove_test.go @@ -13,14 +13,18 @@ import ( func TestRemoveHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + t.Run("not found", func(t *testing.T) { + err := Default.RemoveHandler(ctx, db, nil, "nonexistent") + require.ErrorIs(t, err, handlererror.ErrNotFound) + }) t.Run("success", func(t *testing.T) { - err := db.Create(&model.Actor{ - ID: "test", + err := db.Create(&model.Wallet{ + Address: "f0remove", KeyPath: "/tmp/key-remove", KeyStore: "local", }).Error require.NoError(t, err) - err = Default.RemoveHandler(ctx, db, "test") + err = Default.RemoveHandler(ctx, db, nil, "f0remove") require.NoError(t, err) - err = Default.RemoveHandler(ctx, db, "test") + err = Default.RemoveHandler(ctx, db, nil, "f0remove") require.ErrorIs(t, err, handlererror.ErrNotFound) }) }) diff --git a/handler/wallet/sign_test.go b/handler/wallet/sign_test.go new file mode 100644 index 00000000..c7dab4cb --- /dev/null +++ b/handler/wallet/sign_test.go @@ -0,0 +1,179 @@ +package wallet + +import ( + "context" + "testing" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" +) + +type MockRPCClient struct { + mock.Mock +} + +func (m *MockRPCClient) Call(ctx context.Context, method string, params ...any) (*jsonrpc.RPCResponse, error) { + panic("implement me") +} + +func (m *MockRPCClient) CallRaw(ctx context.Context, request *jsonrpc.RPCRequest) (*jsonrpc.RPCResponse, error) { + panic("implement me") +} + +func (m *MockRPCClient) CallFor(ctx context.Context, out any, method string, params ...any) error { + return m.Called(ctx, out, method, params).Error(0) +} + +func (m *MockRPCClient) CallBatch(ctx context.Context, requests jsonrpc.RPCRequests) (jsonrpc.RPCResponses, error) { + panic("implement me") +} + +func (m *MockRPCClient) CallBatchRaw(ctx context.Context, requests jsonrpc.RPCRequests) (jsonrpc.RPCResponses, error) { + panic("implement me") +} + +func TestGetOrCreateActor_AlreadyLinked(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + actorID := "f01234" + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f3abc"}).Error) + w := model.Wallet{ + Address: "f3abc", KeyPath: "/tmp/key-linked", KeyStore: "local", + ActorID: &actorID, + } + require.NoError(t, db.Create(&w).Error) + + // lotusClient is nil — must not be called + actor, err := GetOrCreateActor(ctx, db, nil, &w) + require.NoError(t, err) + require.Equal(t, actorID, actor.ID) + require.Equal(t, "f3abc", actor.Address) + }) +} + +func TestGetOrCreateActor_LotusLookupFails(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + w := model.Wallet{ + Address: "f3unfunded", KeyPath: "/tmp/key-unfunded", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3unfunded", nil}). + Return(errors.New("actor not found")) + + _, err := GetOrCreateActor(ctx, db, lotus, &w) + require.Error(t, err) + require.ErrorContains(t, err, "not found on-chain") + require.ErrorContains(t, err, "may need funding") + lotus.AssertExpectations(t) + }) +} + +func TestGetOrCreateActor_CreateNewActor(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + w := model.Wallet{ + Address: "f3new", KeyPath: "/tmp/key-new", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3new", nil}). + Return(nil).Run(func(args mock.Arguments) { + *args.Get(1).(*string) = "f09999" + }) + + actor, err := GetOrCreateActor(ctx, db, lotus, &w) + require.NoError(t, err) + require.Equal(t, "f09999", actor.ID) + require.Equal(t, "f3new", actor.Address) + + // wallet should now be linked + var updated model.Wallet + require.NoError(t, db.First(&updated, w.ID).Error) + require.NotNil(t, updated.ActorID) + require.Equal(t, "f09999", *updated.ActorID) + + // actor should exist in DB + var dbActor model.Actor + require.NoError(t, db.First(&dbActor, "id = ?", "f09999").Error) + require.Equal(t, "f3new", dbActor.Address) + + lotus.AssertExpectations(t) + }) +} + +func TestGetOrCreateActor_LinkExistingActor(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + // actor exists in DB but wallet is not yet linked to it + require.NoError(t, db.Create(&model.Actor{ID: "f05555", Address: "f3existing"}).Error) + + w := model.Wallet{ + Address: "f3existing", KeyPath: "/tmp/key-existing", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3existing", nil}). + Return(nil).Run(func(args mock.Arguments) { + *args.Get(1).(*string) = "f05555" + }) + + actor, err := GetOrCreateActor(ctx, db, lotus, &w) + require.NoError(t, err) + require.Equal(t, "f05555", actor.ID) + + // wallet should now be linked + var updated model.Wallet + require.NoError(t, db.First(&updated, w.ID).Error) + require.NotNil(t, updated.ActorID) + require.Equal(t, "f05555", *updated.ActorID) + + lotus.AssertExpectations(t) + }) +} + +func TestGetOrCreateActor_ActorLinkedToDifferentWallet(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + actorID := "f07777" + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f3other"}).Error) + + // first wallet already linked to this actor + other := model.Wallet{ + Address: "f3other", KeyPath: "/tmp/key-other", KeyStore: "local", + ActorID: &actorID, + } + require.NoError(t, db.Create(&other).Error) + + // second wallet tries to claim the same actor + w := model.Wallet{ + Address: "f3conflict", KeyPath: "/tmp/key-conflict", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3conflict", nil}). + Return(nil).Run(func(args mock.Arguments) { + *args.Get(1).(*string) = actorID + }) + + _, err := GetOrCreateActor(ctx, db, lotus, &w) + require.Error(t, err) + require.ErrorContains(t, err, "already linked to wallet") + + // wallet must remain unlinked + var updated model.Wallet + require.NoError(t, db.First(&updated, w.ID).Error) + require.Nil(t, updated.ActorID) + + lotus.AssertExpectations(t) + }) +} diff --git a/model/migrate.go b/model/migrate.go index f381d4db..f1ca258c 100644 --- a/model/migrate.go +++ b/model/migrate.go @@ -221,6 +221,7 @@ var sequenceTables = []string{ "car_blocks", "deals", "schedules", + "wallets", } // fixPostgresSequences detects and fixes out-of-sync sequences. diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index 7cfaa78b..0009ff0d 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -24,6 +24,7 @@ import ( "github.com/libp2p/go-libp2p/core/host" "github.com/rjNemo/underscore" "github.com/robfig/cron/v3" + "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) @@ -41,6 +42,7 @@ var waitPendingInterval = time.Minute type DealPusher struct { dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. keyStore keystore.KeyStore // Keystore for loading private keys + lotusClient jsonrpc.RPCClient // Lotus JSON-RPC client for chain queries walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. dealMaker replication.DealMaker // Object responsible for making a deal in replication. pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. @@ -371,14 +373,11 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) return model.ScheduleError, errors.Wrap(err, "failed to choose wallet") } - // market deals need the on-chain actor for the deal proposal - if walletObj.ActorID == nil { - return model.ScheduleError, errors.Newf("wallet %s has no linked actor", walletObj.Address) - } - var actorObj model.Actor - err = db.Where("id = ?", *walletObj.ActorID).First(&actorObj).Error + // market deals need the on-chain actor for the deal proposal; + // lazily resolve if not yet linked (first use after offline import) + actorObj, err := wallet.GetOrCreateActor(ctx, db, d.lotusClient, &walletObj) if err != nil { - return model.ScheduleError, errors.Wrapf(err, "failed to load actor %s for wallet %s", *walletObj.ActorID, walletObj.Address) + return model.ScheduleError, errors.Wrapf(err, "failed to resolve actor for wallet %s", walletObj.Address) } proposalSigner := replication.ProposalSigner(func(msg []byte) (*crypto.Signature, error) { @@ -388,7 +387,7 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) err = retry.Do(func() error { dealModel, err = d.dealMaker.MakeDeal( ctx, - actorObj, + *actorObj, car, replication.DealConfig{ Provider: schedule.Provider, @@ -514,6 +513,7 @@ func NewDealPusher(db *gorm.DB, lotusURL string, dp := &DealPusher{ dbNoContext: db, keyStore: ks, + lotusClient: lotusClient, activeScheduleCancelFunc: make(map[model.ScheduleID]context.CancelFunc), activeSchedule: make(map[model.ScheduleID]*model.Schedule), cronEntries: make(map[model.ScheduleID]cron.EntryID),