diff --git a/cmd/keeper.go b/cmd/keeper.go index 65ed9ac4..ac023ff1 100644 --- a/cmd/keeper.go +++ b/cmd/keeper.go @@ -69,8 +69,37 @@ func KeeperBootCmd(c *cli.Context) error { return err } defer kd.Close() + + var fromStart bool + legacyDB := c.String("legacy") + if legacyDB != "" { + req, err := kd.ReadLatestRequest(ctx) + if err != nil { + return err + } + actions, err := db.ListActions(ctx, mtg.ActionStateDone, 1) + if err != nil { + return err + } + if req == nil && len(actions) == 0 { + ld, err := keeper.OpenSQLite3StoreLegacy(legacyDB) + if err != nil { + return err + } + err = kd.ImportBackup(ctx, ld) + if err != nil { + return err + } + err = ld.Close() + if err != nil { + return err + } + fromStart = true + } + } + keeper := keeper.NewNode(kd, group, mc.Keeper, mc.Signer.MTG, client) - keeper.Boot(ctx) + keeper.Boot(ctx, fromStart) if mmc := mc.Keeper.MonitorConversaionId; mmc != "" { go MonitorKeeper(ctx, db, kd, mc.Keeper, group, mmc, version) @@ -98,3 +127,29 @@ func KeeperFundRequest(c *cli.Context) error { traceId := uuid.Must(uuid.NewV4()).String() return makeKeeperPaymentRequest(c.String("config"), assetId, amount, traceId, "") } + +func KeeperExportLegacyData(c *cli.Context) error { + ctx := context.Background() + + input := c.String("database") + if input == "" { + return fmt.Errorf("empty path of legacy database") + } + path := c.String("export") + if path == "" { + return fmt.Errorf("empty path of export database") + } + + ld, err := keeper.OpenSQLite3StoreLegacy(input) + if err != nil { + return err + } + defer ld.Close() + sd, err := keeper.OpenSQLite3Store(path) + if err != nil { + return err + } + defer sd.Close() + + return sd.ImportBackup(ctx, ld) +} diff --git a/cmd/signer.go b/cmd/signer.go index fc722bde..1c86afaa 100644 --- a/cmd/signer.go +++ b/cmd/signer.go @@ -55,12 +55,40 @@ func SignerBootCmd(c *cli.Context) error { return err } - kd, err := signer.OpenSQLite3Store(mc.Signer.StoreDir + "/mpc.sqlite3") + kd, err := signer.OpenSQLite3Store(mc.Signer.StoreDir+"/mpc.sqlite3", false) if err != nil { return err } defer kd.Close() + var fromStart bool + legacyDB := c.String("legacy") + if legacyDB != "" { + state, err := kd.SessionsState(ctx) + if err != nil { + return err + } + actions, err := db.ListActions(ctx, mtg.ActionStateDone, 1) + if err != nil { + return err + } + if state.Done == 0 && len(actions) == 0 { + ld, err := signer.OpenSQLite3Store(legacyDB, true) + if err != nil { + return err + } + err = kd.ImportBackup(ctx, ld) + if err != nil { + return err + } + err = ld.Close() + if err != nil { + return err + } + fromStart = true + } + } + s := &mixin.Keystore{ ClientID: mc.Signer.MTG.App.AppId, SessionID: mc.Signer.MTG.App.SessionId, @@ -82,7 +110,7 @@ func SignerBootCmd(c *cli.Context) error { mc.Signer.MTG.App.SpendPrivateKey = key.String() node := signer.NewNode(kd, group, messenger, mc.Signer, mc.Keeper.MTG, client) - node.Boot(ctx) + node.Boot(ctx, fromStart) if mmc := mc.Signer.MonitorConversaionId; mmc != "" { go MonitorSigner(ctx, db, kd, mc.Signer, group, mmc, version) @@ -190,3 +218,29 @@ func makeSignerPaymentRequest(conf *signer.Configuration, op *common.Operation, qrterminal.GenerateHalfBlock(url, qrterminal.H, os.Stdout) return nil } + +func SignerExportLegacyData(c *cli.Context) error { + ctx := context.Background() + + input := c.String("database") + if input == "" { + return fmt.Errorf("empty path of legacy database") + } + path := c.String("export") + if path == "" { + return fmt.Errorf("empty path of export database") + } + + ld, err := signer.OpenSQLite3Store(input, true) + if err != nil { + return err + } + defer ld.Close() + sd, err := signer.OpenSQLite3Store(path, false) + if err != nil { + return err + } + defer sd.Close() + + return sd.ImportBackup(ctx, ld) +} diff --git a/common/request.go b/common/request.go index bd3fa495..796bc584 100644 --- a/common/request.go +++ b/common/request.go @@ -82,6 +82,7 @@ type Request struct { ExtraHEX string State uint8 CreatedAt time.Time + UpdatedAt time.Time Sequence uint64 Output *mtg.Action diff --git a/keeper/interface.go b/keeper/interface.go index 2be389ca..719b7a53 100644 --- a/keeper/interface.go +++ b/keeper/interface.go @@ -28,8 +28,12 @@ type Configuration struct { MTG *mtg.Configuration `toml:"mtg"` } +func OpenSQLite3StoreLegacy(path string) (*store.SQLite3Store, error) { + return store.OpenSQLite3Store(path, true) +} + func OpenSQLite3Store(path string) (*store.SQLite3Store, error) { - return store.OpenSQLite3Store(path) + return store.OpenSQLite3Store(path, false) } func OpenSQLite3ReadOnlyStore(path string) (*store.SQLite3Store, error) { diff --git a/keeper/node.go b/keeper/node.go index 077ce136..55148134 100644 --- a/keeper/node.go +++ b/keeper/node.go @@ -41,14 +41,16 @@ func NewNode(store *store.SQLite3Store, group *mtg.Group, conf *Configuration, s return node } -func (node *Node) Boot(ctx context.Context) { +func (node *Node) Boot(ctx context.Context, fromStart bool) { terminated, err := node.store.ReadTerminate(ctx) if err != nil || terminated { panic(err) } - err = node.Migrate(ctx) - if err != nil { - panic(err) + if !fromStart { + err = node.Migrate(ctx) + if err != nil { + panic(err) + } } } diff --git a/keeper/store/ethereum.go b/keeper/store/ethereum.go index 1f45bec4..881bdbb4 100644 --- a/keeper/store/ethereum.go +++ b/keeper/store/ethereum.go @@ -21,6 +21,15 @@ type SafeBalance struct { UpdatedAt time.Time } +func BalancFromRow(row *sql.Rows) (*SafeBalance, error) { + var sb SafeBalance + err := row.Scan(&sb.Address, &sb.AssetId, &sb.AssetAddress, &sb.balance, &sb.LatestTxHash, &sb.UpdatedAt) + if err != nil { + return nil, err + } + return &sb, nil +} + func (sb *SafeBalance) UpdateBalance(change *big.Int) { balance := new(big.Int).Add(sb.BigBalance(), change) if balance.Sign() < 0 { diff --git a/keeper/store/export.go b/keeper/store/export.go new file mode 100644 index 00000000..7263b5f1 --- /dev/null +++ b/keeper/store/export.go @@ -0,0 +1,672 @@ +package store + +import ( + "context" + "database/sql" + "encoding/hex" + "fmt" + "strings" + "time" + + "github.com/MixinNetwork/mixin/crypto" + "github.com/MixinNetwork/mixin/logger" + "github.com/MixinNetwork/safe/common" + "github.com/shopspring/decimal" +) + +type Property struct { + Key string + Value string + CreatedAt time.Time +} + +type BitcoinOutput struct { + TransactionHash string + Index uint32 + Address string + Satoshi int64 + Script []byte + Sequence uint32 + Chain int64 + State int64 + SpentBy sql.NullString + RequestId string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ExportData struct { + Requests []*common.Request + NetworkInfos []*NetworkInfo + OperationParams []*OperationParams + Assets []*Asset + Keys []*Key + SafeProposals []*SafeProposal + Safes []*Safe + BitcoinOutputs []*BitcoinOutput + EthereumBalances []*SafeBalance + Deposits []*Deposit + Transactions []*Transaction + SignatureRequests []*SignatureRequest + Properties []*Property +} + +func (s *SQLite3Store) ImportBackup(ctx context.Context, bd *SQLite3Store) error { + logger.Println("Reading data from backup database...") + + requests, err := bd.listRequests(ctx) + if err != nil { + return err + } + logger.Printf("Read %d requests\n", len(requests)) + + infos, err := bd.listLatestNetworkInfos(ctx) + if err != nil { + return err + } + logger.Printf("Read %d network_infos\n", len(infos)) + + ops, err := bd.listLatestOperationParams(ctx) + if err != nil { + return err + } + logger.Printf("Read %d operation_params\n", len(ops)) + + assets, err := bd.listAssets(ctx) + if err != nil { + return err + } + logger.Printf("Read %d assets\n", len(assets)) + + keys, err := bd.listKeys(ctx) + if err != nil { + return err + } + logger.Printf("Read %d keys\n", len(keys)) + + proposals, err := bd.listProposals(ctx) + if err != nil { + return err + } + logger.Printf("Read %d safe_proposals\n", len(proposals)) + + safes, err := bd.listSafes(ctx) + if err != nil { + return err + } + logger.Printf("Read %d safes\n", len(safes)) + + outputs, err := bd.listBitcoinOutputs(ctx) + if err != nil { + return err + } + logger.Printf("Read %d bitcoin_outputs\n", len(outputs)) + + balances, err := bd.listEthereumBalances(ctx) + if err != nil { + return err + } + logger.Printf("Read %d ethereum_balances\n", len(balances)) + + deposits, err := bd.listDeposits(ctx) + if err != nil { + return err + } + logger.Printf("Read %d deposits\n", len(deposits)) + + txs, err := bd.listTransactions(ctx) + if err != nil { + return err + } + logger.Printf("Read %d transactions\n", len(txs)) + + signatures, err := bd.listSignatureRequests(ctx) + if err != nil { + return err + } + logger.Printf("Read %d signature requests\n", len(signatures)) + + properties, err := bd.listProperties(ctx) + if err != nil { + return err + } + logger.Printf("Read %d properties\n", len(properties)) + + return s.Export(ctx, ExportData{ + Requests: requests, + NetworkInfos: infos, + OperationParams: ops, + Assets: assets, + Keys: keys, + SafeProposals: proposals, + Safes: safes, + BitcoinOutputs: outputs, + EthereumBalances: balances, + Deposits: deposits, + Transactions: txs, + SignatureRequests: signatures, + Properties: properties, + }) +} + +func (s *SQLite3Store) Export(ctx context.Context, data ExportData) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + logger.Println("Exporting requests...") + err = s.exportRequests(ctx, tx, data.Requests) + if err != nil { + return err + } + logger.Println("Exporting network_infos...") + err = s.exportNetworkInfos(ctx, tx, data.NetworkInfos) + if err != nil { + return err + } + logger.Println("Exporting operation_params...") + err = s.exportOperationParams(ctx, tx, data.OperationParams) + if err != nil { + return err + } + logger.Println("Exporting assets...") + err = s.exportAssets(ctx, tx, data.Assets) + if err != nil { + return err + } + logger.Println("Exporting keys...") + err = s.exportKeys(ctx, tx, data.Keys) + if err != nil { + return err + } + logger.Println("Exporting safe_proposals...") + err = s.exportSafeProposals(ctx, tx, data.SafeProposals) + if err != nil { + return err + } + logger.Println("Exporting safes...") + err = s.exportSafes(ctx, tx, data.Safes) + if err != nil { + return err + } + logger.Println("Exporting bitcoin_outputs...") + err = s.exportBitcoinOutputs(ctx, tx, data.BitcoinOutputs) + if err != nil { + return err + } + logger.Println("Exporting ethereum_balances...") + err = s.exportEthereumBalances(ctx, tx, data.EthereumBalances) + if err != nil { + return err + } + logger.Println("Exporting deposits...") + err = s.exportDeposits(ctx, tx, data.Deposits) + if err != nil { + return err + } + logger.Println("Exporting transactions...") + err = s.exportTransactions(ctx, tx, data.Transactions) + if err != nil { + return err + } + logger.Println("Exporting signature Requests...") + err = s.exportSignatureRequests(ctx, tx, data.SignatureRequests) + if err != nil { + return err + } + logger.Println("Exporting properties...") + err = s.exportProperties(ctx, tx, data.Properties) + if err != nil { + return err + } + + return tx.Commit() +} + +func (s *SQLite3Store) listRequests(ctx context.Context) ([]*common.Request, error) { + var cols = []string{"request_id", "mixin_hash", "mixin_index", "asset_id", "amount", "role", "action", "curve", "holder", "extra", "state", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM requests ORDER BY created_at ASC, request_id ASC", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var requests []*common.Request + for rows.Next() { + var mh string + var r common.Request + err = rows.Scan(&r.Id, &mh, &r.MixinIndex, &r.AssetId, &r.Amount, &r.Role, &r.Action, &r.Curve, &r.Holder, &r.ExtraHEX, &r.State, &r.CreatedAt, &r.UpdatedAt) + if err != nil { + return nil, err + } + r.MixinHash, err = crypto.HashFromString(mh) + if err != nil { + return nil, err + } + requests = append(requests, &r) + } + return requests, nil +} + +func (s *SQLite3Store) readLatestNetworkInfo(ctx context.Context, chain int64) (*NetworkInfo, error) { + var cols = []string{"request_id", "chain", "fee", "height", "hash", "created_at"} + query := fmt.Sprintf("SELECT %s FROM network_infos WHERE chain=? ORDER BY created_at DESC, request_id DESC LIMIT 1", strings.Join(cols, ",")) + row := s.db.QueryRowContext(ctx, query, chain) + + var n NetworkInfo + err := row.Scan(&n.RequestId, &n.Chain, &n.Fee, &n.Height, &n.Hash, &n.CreatedAt) + if err == sql.ErrNoRows { + return nil, nil + } + return &n, err +} + +func (s *SQLite3Store) listLatestNetworkInfos(ctx context.Context) ([]*NetworkInfo, error) { + var infos []*NetworkInfo + for _, c := range []int64{common.SafeChainBitcoin, common.SafeChainEthereum, common.SafeChainLitecoin, common.SafeChainPolygon} { + info, err := s.readLatestNetworkInfo(ctx, c) + if err != nil || info == nil { + return nil, fmt.Errorf("legacy.readLatestNetworkInfo(%d) => %v %v", c, info, err) + } + infos = append(infos, info) + } + return infos, nil +} + +func (s *SQLite3Store) readLatestOperationParams(ctx context.Context, chain int64) (*OperationParams, error) { + var cols = []string{"request_id", "chain", "price_asset", "price_amount", "transaction_minimum", "created_at"} + query := fmt.Sprintf("SELECT %s FROM operation_params WHERE chain=? ORDER BY created_at DESC, request_id DESC LIMIT 1", strings.Join(cols, ",")) + row := s.db.QueryRowContext(ctx, query, chain) + + var p OperationParams + var price, minimum string + err := row.Scan(&p.RequestId, &p.Chain, &p.OperationPriceAsset, &price, &minimum, &p.CreatedAt) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + p.OperationPriceAmount = decimal.RequireFromString(price) + p.TransactionMinimum = decimal.RequireFromString(minimum) + return &p, nil +} + +func (s *SQLite3Store) listLatestOperationParams(ctx context.Context) ([]*OperationParams, error) { + var ops []*OperationParams + for _, c := range []int64{common.SafeChainBitcoin, common.SafeChainEthereum, common.SafeChainLitecoin, common.SafeChainPolygon} { + op, err := s.readLatestOperationParams(ctx, c) + if err != nil || op == nil { + return nil, fmt.Errorf("legacy.readLatestOperationParams(%d) => %v %v", c, op, err) + } + ops = append(ops, op) + } + return ops, nil +} + +func (s *SQLite3Store) listAssets(ctx context.Context) ([]*Asset, error) { + var cols = []string{"asset_id", "mixin_id", "asset_key", "symbol", "name", "decimals", "chain", "created_at"} + query := fmt.Sprintf("SELECT %s FROM assets", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var assets []*Asset + for rows.Next() { + var a Asset + err := rows.Scan(&a.AssetId, &a.MixinId, &a.AssetKey, &a.Symbol, &a.Name, &a.Decimals, &a.Chain, &a.CreatedAt) + if err != nil { + return nil, err + } + assets = append(assets, &a) + } + return assets, nil +} + +func (s *SQLite3Store) listKeys(ctx context.Context) ([]*Key, error) { + var cols = []string{"public_key", "curve", "request_id", "role", "extra", "flags", "holder", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM keys", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var keys []*Key + for rows.Next() { + var k Key + err := rows.Scan(&k.Public, &k.Curve, &k.RequestId, &k.Role, &k.Extra, &k.Flags, &k.Holder, &k.CreatedAt, &k.UpdatedAt) + if err != nil { + return nil, err + } + keys = append(keys, &k) + } + return keys, nil +} + +func (s *SQLite3Store) listProposals(ctx context.Context) ([]*SafeProposal, error) { + var cols = []string{"request_id", "chain", "holder", "signer", "observer", "timelock", "path", "address", "extra", "receivers", "threshold", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM safe_proposals", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var ps []*SafeProposal + for rows.Next() { + var s SafeProposal + var receivers string + err := rows.Scan(&s.RequestId, &s.Chain, &s.Holder, &s.Signer, &s.Observer, &s.Timelock, &s.Path, &s.Address, &s.Extra, &receivers, &s.Threshold, &s.CreatedAt, &s.UpdatedAt) + if err != nil { + return nil, err + } + s.Receivers = strings.Split(receivers, ";") + ps = append(ps, &s) + } + return ps, nil +} + +func (s *SQLite3Store) listSafes(ctx context.Context) ([]*Safe, error) { + var cols = []string{"holder", "chain", "signer", "observer", "timelock", "path", "address", "extra", "receivers", "threshold", "request_id", "nonce", "state", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM safes", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var safes []*Safe + for rows.Next() { + var s Safe + var receivers string + err := rows.Scan(&s.Holder, &s.Chain, &s.Signer, &s.Observer, &s.Timelock, &s.Path, &s.Address, &s.Extra, &receivers, &s.Threshold, &s.RequestId, &s.Nonce, &s.State, &s.CreatedAt, &s.UpdatedAt) + if err != nil { + return nil, err + } + s.Receivers = strings.Split(receivers, ";") + safes = append(safes, &s) + } + return safes, nil +} + +func (s *SQLite3Store) listBitcoinOutputs(ctx context.Context) ([]*BitcoinOutput, error) { + cols := strings.Join([]string{"transaction_hash", "output_index", "address", "satoshi", "script", "sequence", "chain", "state", "spent_by", "request_id", "created_at", "updated_at"}, ",") + query := fmt.Sprintf("SELECT %s FROM bitcoin_outputs ORDER BY created_at ASC, request_id ASC", cols) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var inputs []*BitcoinOutput + for rows.Next() { + var script string + var input BitcoinOutput + err = rows.Scan(&input.TransactionHash, &input.Index, &input.Address, &input.Satoshi, &script, &input.Sequence, &input.Chain, &input.State, &input.SpentBy, &input.RequestId, &input.CreatedAt, &input.UpdatedAt) + if err != nil { + return nil, err + } + b, _ := hex.DecodeString(script) + input.Script = b + inputs = append(inputs, &input) + } + return inputs, nil +} + +func (s *SQLite3Store) listEthereumBalances(ctx context.Context) ([]*SafeBalance, error) { + cols := []string{"address", "asset_id", "asset_address", "balance", "latest_tx_hash", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM ethereum_balances", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var sbs []*SafeBalance + for rows.Next() { + b, err := BalancFromRow(rows) + if err != nil { + return nil, err + } + sbs = append(sbs, b) + } + return sbs, nil +} + +func (s *SQLite3Store) listDeposits(ctx context.Context) ([]*Deposit, error) { + var cols = []string{"transaction_hash", "output_index", "asset_id", "amount", "receiver", "sender", "state", "chain", "holder", "category", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM deposits", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var ds []*Deposit + for rows.Next() { + var d Deposit + err = rows.Scan(&d.TransactionHash, &d.OutputIndex, &d.AssetId, &d.Amount, &d.Receiver, &d.Sender, &d.State, &d.Chain, &d.Holder, &d.Category, &d.CreatedAt, &d.UpdatedAt) + if err != nil { + return nil, err + } + ds = append(ds, &d) + } + return ds, nil +} + +func (s *SQLite3Store) listTransactions(ctx context.Context) ([]*Transaction, error) { + var cols = []string{"transaction_hash", "raw_transaction", "holder", "chain", "asset_id", "state", "data", "request_id", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM transactions", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var txs []*Transaction + for rows.Next() { + var tx Transaction + err = rows.Scan(&tx.TransactionHash, &tx.RawTransaction, &tx.Holder, &tx.Chain, &tx.AssetId, &tx.State, &tx.Data, &tx.RequestId, &tx.CreatedAt, &tx.UpdatedAt) + if err != nil { + return nil, err + } + txs = append(txs, &tx) + } + return txs, nil +} + +func (s *SQLite3Store) listSignatureRequests(ctx context.Context) ([]*SignatureRequest, error) { + var cols = []string{"request_id", "transaction_hash", "input_index", "signer", "curve", "message", "signature", "state", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM signature_requests ORDER BY created_at DESC, request_id DESC", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var rs []*SignatureRequest + for rows.Next() { + var r SignatureRequest + err := rows.Scan(&r.RequestId, &r.TransactionHash, &r.InputIndex, &r.Signer, &r.Curve, &r.Message, &r.Signature, &r.State, &r.CreatedAt, &r.UpdatedAt) + if err != nil { + return nil, err + } + rs = append(rs, &r) + } + return rs, nil +} + +func (s *SQLite3Store) listProperties(ctx context.Context) ([]*Property, error) { + var cols = []string{"key", "value", "created_at"} + query := fmt.Sprintf("SELECT %s FROM properties", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var ps []*Property + for rows.Next() { + var p Property + err := rows.Scan(&p.Key, &p.Value, &p.CreatedAt) + if err != nil { + return nil, err + } + ps = append(ps, &p) + } + return ps, nil +} + +func (s *SQLite3Store) exportRequests(ctx context.Context, tx *sql.Tx, requests []*common.Request) error { + for _, req := range requests { + vals := []any{req.Id, req.MixinHash.String(), req.MixinIndex, req.AssetId, req.Amount, req.Role, req.Action, req.Curve, req.Holder, req.ExtraHEX, req.State, req.CreatedAt, req.UpdatedAt, req.Sequence} + _, err := tx.ExecContext(ctx, buildInsertionSQL("requests", requestCols), vals...) + if err != nil { + return fmt.Errorf("INSERT requests %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportNetworkInfos(ctx context.Context, tx *sql.Tx, infos []*NetworkInfo) error { + for _, info := range infos { + vals := []any{info.RequestId, info.Chain, info.Fee, info.Height, info.Hash, info.CreatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("network_infos", infoCols), vals...) + if err != nil { + return fmt.Errorf("INSERT network_infos %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportOperationParams(ctx context.Context, tx *sql.Tx, ops []*OperationParams) error { + for _, params := range ops { + amount := params.OperationPriceAmount.String() + minimum := params.TransactionMinimum.String() + vals := []any{params.RequestId, params.Chain, params.OperationPriceAsset, amount, minimum, params.CreatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("operation_params", paramsCols), vals...) + if err != nil { + return fmt.Errorf("INSERT operation_params %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportAssets(ctx context.Context, tx *sql.Tx, assets []*Asset) error { + for _, asset := range assets { + vals := []any{asset.AssetId, asset.MixinId, asset.AssetKey, asset.Symbol, asset.Name, asset.Decimals, asset.Chain, asset.CreatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("assets", assetCols), vals...) + if err != nil { + return fmt.Errorf("INSERT assets %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportKeys(ctx context.Context, tx *sql.Tx, keys []*Key) error { + for _, key := range keys { + vals := []any{key.Public, key.Curve, key.RequestId, key.Role, key.Extra, key.Flags, key.Holder, key.CreatedAt, key.UpdatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("keys", keyCols), vals...) + if err != nil { + return fmt.Errorf("INSERT keys %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportSafeProposals(ctx context.Context, tx *sql.Tx, proposals []*SafeProposal) error { + for _, sp := range proposals { + _, err := tx.ExecContext(ctx, buildInsertionSQL("safe_proposals", safeProposalCols), sp.values()...) + if err != nil { + return fmt.Errorf("INSERT safe_proposals %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportSafes(ctx context.Context, tx *sql.Tx, safes []*Safe) error { + for _, safe := range safes { + _, err := tx.ExecContext(ctx, buildInsertionSQL("safes", safeCols), safe.values()...) + if err != nil { + return fmt.Errorf("INSERT safes %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportBitcoinOutputs(ctx context.Context, tx *sql.Tx, outputs []*BitcoinOutput) error { + for _, utxo := range outputs { + script := hex.EncodeToString(utxo.Script) + cols := []string{"transaction_hash", "output_index", "address", "satoshi", "script", "sequence", "chain", "state", "spent_by", "request_id", "created_at", "updated_at"} + vals := []any{utxo.TransactionHash, utxo.Index, utxo.Address, utxo.Satoshi, script, utxo.Sequence, utxo.Chain, utxo.State, utxo.SpentBy, utxo.RequestId, utxo.CreatedAt, utxo.CreatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("bitcoin_outputs", cols), vals...) + if err != nil { + return fmt.Errorf("INSERT bitcoin_outputs %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportEthereumBalances(ctx context.Context, tx *sql.Tx, balances []*SafeBalance) error { + for _, balance := range balances { + cols := []string{"address", "asset_id", "asset_address", "safe_asset_id", "balance", "latest_tx_hash", "updated_at"} + vals := []any{balance.Address, balance.AssetId, balance.AssetAddress, balance.SafeAssetId, balance.balance, balance.LatestTxHash, balance.UpdatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("ethereum_balances", cols), vals...) + if err != nil { + return fmt.Errorf("INSERT ethereum_balances %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportDeposits(ctx context.Context, tx *sql.Tx, deposits []*Deposit) error { + for _, deposit := range deposits { + vals := []any{deposit.TransactionHash, deposit.OutputIndex, deposit.AssetId, deposit.Amount, deposit.Receiver, deposit.Sender, deposit.State, deposit.Chain, deposit.Holder, deposit.Category, deposit.CreatedAt, deposit.CreatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("deposits", depositsCols), vals...) + if err != nil { + return fmt.Errorf("INSERT deposits %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportTransactions(ctx context.Context, tx *sql.Tx, transactions []*Transaction) error { + for _, trx := range transactions { + vals := []any{trx.TransactionHash, trx.RawTransaction, trx.Holder, trx.Chain, trx.AssetId, trx.State, trx.Data, trx.RequestId, trx.CreatedAt, trx.UpdatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("transactions", transactionCols), vals...) + if err != nil { + return fmt.Errorf("INSERT transactions %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportSignatureRequests(ctx context.Context, tx *sql.Tx, signatures []*SignatureRequest) error { + for _, r := range signatures { + vals := []any{r.RequestId, r.TransactionHash, r.InputIndex, r.Signer, r.Curve, r.Message, r.Signature, r.State, r.CreatedAt, r.UpdatedAt} + _, err := tx.ExecContext(ctx, buildInsertionSQL("signature_requests", signatureCols), vals...) + if err != nil { + return fmt.Errorf("INSERT signature_requests %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportProperties(ctx context.Context, tx *sql.Tx, properties []*Property) error { + for _, p := range properties { + cols := []string{"key", "value", "created_at"} + _, err := tx.ExecContext(ctx, buildInsertionSQL("properties", cols), p.Key, p.Value, p.CreatedAt) + if err != nil { + return fmt.Errorf("INSERT properties %v", err) + } + } + return nil +} diff --git a/keeper/store/legacy.sql b/keeper/store/legacy.sql new file mode 100644 index 00000000..fe68a460 --- /dev/null +++ b/keeper/store/legacy.sql @@ -0,0 +1,253 @@ +CREATE TABLE IF NOT EXISTS requests ( + request_id VARCHAR NOT NULL, + mixin_hash VARCHAR NOT NULL, + mixin_index INTEGER NOT NULL, + asset_id VARCHAR NOT NULL, + amount VARCHAR NOT NULL, + role INTEGER NOT NULL, + action INTEGER NOT NULL, + curve INTEGER NOT NULL, + holder VARCHAR NOT NULL, + extra VARCHAR NOT NULL, + state INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('request_id') +); + +CREATE UNIQUE INDEX IF NOT EXISTS requests_by_mixin_hash_index ON requests(mixin_hash, mixin_index); +CREATE INDEX IF NOT EXISTS requests_by_state_created ON requests(state, created_at); + + + + +CREATE TABLE IF NOT EXISTS network_infos ( + request_id VARCHAR NOT NULL, + chain INTEGER NOT NULL, + fee INTEGER NOT NULL, + hash VARCHAR NOT NULL, + height INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY ('request_id') +); + +CREATE INDEX IF NOT EXISTS network_infos_by_chain_created ON network_infos(chain, created_at); + + + + +CREATE TABLE IF NOT EXISTS operation_params ( + request_id VARCHAR NOT NULL, + chain INTEGER NOT NULL, + price_asset VARCHAR NOT NULL, + price_amount VARCHAR NOT NULL, + transaction_minimum VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY ('request_id') +); + +CREATE INDEX IF NOT EXISTS operation_params_by_chain_created ON operation_params(chain, created_at); + + + + +CREATE TABLE IF NOT EXISTS assets ( + asset_id VARCHAR NOT NULL, + mixin_id VARCHAR NOT NULL, + asset_key VARCHAR NOT NULL, + symbol VARCHAR NOT NULL, + name VARCHAR NOT NULL, + decimals INTEGER NOT NULL, + chain INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY ('asset_id') +); + +CREATE UNIQUE INDEX IF NOT EXISTS assets_by_mixin_id ON assets(mixin_id); + + + + + +CREATE TABLE IF NOT EXISTS keys ( + public_key VARCHAR NOT NULL, + curve INTEGER NOT NULL, + request_id VARCHAR NOT NULL, + role INTEGER NOT NULL, + extra VARCHAR NOT NULL, + flags INTEGER NOT NULL, + holder VARCHAR, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('public_key') +); + +CREATE UNIQUE INDEX IF NOT EXISTS keys_by_request_id ON keys(request_id); +CREATE UNIQUE INDEX IF NOT EXISTS keys_by_holder_role ON keys(holder, role); + + + + + + +CREATE TABLE IF NOT EXISTS safe_proposals ( + request_id VARCHAR NOT NULL, + chain INTEGER NOT NULL, + holder VARCHAR NOT NULL, + signer VARCHAR NOT NULL, + observer VARCHAR NOT NULL, + timelock INTEGER NOT NULL, + path VARCHAR NOT NULL, + address VARCHAR NOT NULL, + extra VARCHAR NOT NULL, + receivers VARCHAR NOT NULL, + threshold INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('request_id') +); + +CREATE UNIQUE INDEX IF NOT EXISTS safe_proposals_by_signer ON safe_proposals(signer); +CREATE UNIQUE INDEX IF NOT EXISTS safe_proposals_by_observer ON safe_proposals(observer); +CREATE UNIQUE INDEX IF NOT EXISTS safe_proposals_by_address ON safe_proposals(address); + + + + + + +CREATE TABLE IF NOT EXISTS safes ( + holder VARCHAR NOT NULL, + chain INTEGER NOT NULL, + signer VARCHAR NOT NULL, + observer VARCHAR NOT NULL, + timelock INTEGER NOT NULL, + path VARCHAR NOT NULL, + address VARCHAR NOT NULL, + extra VARCHAR NOT NULL, + receivers VARCHAR NOT NULL, + threshold INTEGER NOT NULL, + request_id VARCHAR NOT NULL, + nonce INTEGER NOT NULL, + state INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('holder') +); + +CREATE UNIQUE INDEX IF NOT EXISTS safes_by_signer ON safes(signer); +CREATE UNIQUE INDEX IF NOT EXISTS safes_by_observer ON safes(observer); +CREATE UNIQUE INDEX IF NOT EXISTS safes_by_address ON safes(address); +CREATE UNIQUE INDEX IF NOT EXISTS safes_by_request_id ON safes(request_id); + + + + + +CREATE TABLE IF NOT EXISTS bitcoin_outputs ( + transaction_hash VARCHAR NOT NULL, + output_index INTEGER NOT NULL, + address VARCHAR NOT NULL, + satoshi INTEGER NOT NULL, + script VARCHAR NOT NULL, + sequence INTEGER NOT NULL, + chain INTEGER NOT NULL, + state INTEGER NOT NULL, + spent_by VARCHAR, + request_id VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('transaction_hash', 'output_index') +); + +CREATE UNIQUE INDEX IF NOT EXISTS bitcoin_outputs_by_request_id ON bitcoin_outputs(request_id); +CREATE INDEX IF NOT EXISTS bitcoin_outputs_by_address_state_created ON bitcoin_outputs(address, state, created_at); + + + + + + +CREATE TABLE IF NOT EXISTS ethereum_balances ( + address VARCHAR NOT NULL, + asset_id VARCHAR NOT NULL, + asset_address VARCHAR NOT NULL, + balance VARCHAR NOT NULL, + latest_tx_hash VARCHAR NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('address', 'asset_id') +); + + + + + + + +CREATE TABLE IF NOT EXISTS deposits ( + transaction_hash VARCHAR NOT NULL, + output_index VARCHAR NOT NULL, + asset_id VARCHAR NOT NULL, + amount VARCHAR NOT NULL, + receiver VARCHAR NOT NULL, + sender VARCHAR NOT NULL, + state INTEGER NOT NULL, + chain INTEGER NOT NULL, + holder VARCHAR NOT NULL, + category INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('transaction_hash', 'output_index') +); + + + + + + + +CREATE TABLE IF NOT EXISTS transactions ( + transaction_hash VARCHAR NOT NULL, + raw_transaction VARCHAR NOT NULL, + holder VARCHAR NOT NULL, + chain INTEGER NOT NULL, + asset_id VARCHAR NOT NULL, + state INTEGER NOT NULL, + data VARCHAR NOT NULL, + request_id VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('transaction_hash') +); + +CREATE UNIQUE INDEX IF NOT EXISTS transactions_by_request_id ON transactions(request_id); + + + + + +CREATE TABLE IF NOT EXISTS signature_requests ( + request_id VARCHAR NOT NULL, + transaction_hash VARCHAR NOT NULL, + input_index INTEGER NOT NULL, + signer VARCHAR NOT NULL, + curve INTEGER NOT NULL, + message VARCHAR NOT NULL, + signature VARCHAR, + state INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('request_id') +); + +CREATE INDEX IF NOT EXISTS signature_requests_by_transaction_state_created ON signature_requests(transaction_hash, state, created_at); + + + + +CREATE TABLE IF NOT EXISTS properties ( + key VARCHAR NOT NULL, + value VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY ('key') +); \ No newline at end of file diff --git a/keeper/store/sqlite3.go b/keeper/store/sqlite3.go index 9ac1670d..78a343ea 100644 --- a/keeper/store/sqlite3.go +++ b/keeper/store/sqlite3.go @@ -16,13 +16,20 @@ import ( //go:embed schema.sql var SCHEMA string +//go:embed legacy.sql +var LEGACY_SCHEMA string + type SQLite3Store struct { db *sql.DB mutex *sync.Mutex } -func OpenSQLite3Store(path string) (*SQLite3Store, error) { - db, err := common.OpenSQLite3Store(path, SCHEMA) +func OpenSQLite3Store(path string, legacy bool) (*SQLite3Store, error) { + schema := SCHEMA + if legacy { + schema = LEGACY_SCHEMA + } + db, err := common.OpenSQLite3Store(path, schema) if err != nil { return nil, err } diff --git a/main.go b/main.go index 2e5dce8a..60d42565 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,10 @@ func main() { Value: "~/.mixin/safe/config.toml", Usage: "The configuration file path", }, + &cli.StringFlag{ + Name: "legacy", + Usage: "The path of legacy database", + }, }, }, { @@ -117,6 +121,10 @@ func main() { Value: "~/.mixin/safe/config.toml", Usage: "The configuration file path", }, + &cli.StringFlag{ + Name: "legacy", + Usage: "The path of legacy database", + }, }, }, { @@ -418,6 +426,36 @@ func main() { }, }, }, + { + Name: "exportKeeperLegacyData", + Usage: "Export data of Legacy Mixin Network from keeper node", + Action: cmd.KeeperExportLegacyData, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "database", + Usage: "The legacy database backup from keeper", + }, + &cli.StringFlag{ + Name: "export", + Usage: "The export database path", + }, + }, + }, + { + Name: "exportSignerLegacyData", + Usage: "Export data of Legacy Mixin Network from signer node", + Action: cmd.SignerExportLegacyData, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "database", + Usage: "The legacy database backup from keeper", + }, + &cli.StringFlag{ + Name: "export", + Usage: "The export database path", + }, + }, + }, }, } diff --git a/signer/export.go b/signer/export.go new file mode 100644 index 00000000..a10c745e --- /dev/null +++ b/signer/export.go @@ -0,0 +1,215 @@ +package signer + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/MixinNetwork/mixin/logger" +) + +type ExportData struct { + Properties []*Property + Sessions []*Session + SessionSigners []*SessionSigner + SessionWorks []*SessionWork +} + +func (s *SQLite3Store) ImportBackup(ctx context.Context, bd *SQLite3Store) error { + logger.Println("Reading data from database...") + + properties, err := bd.listProperties(ctx) + if err != nil { + return err + } + sessions, err := bd.listSessions(ctx) + if err != nil { + return err + } + signers, err := bd.listSessionSigners(ctx) + if err != nil { + return err + } + works, err := bd.listSessionWorks(ctx) + if err != nil { + return err + } + + return s.Export(ctx, ExportData{ + Properties: properties, + Sessions: sessions, + SessionSigners: signers, + SessionWorks: works, + }) +} + +func (s *SQLite3Store) Export(ctx context.Context, data ExportData) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + logger.Println("Exporting properties...") + err = s.exportProperties(ctx, tx, data.Properties) + if err != nil { + return err + } + + logger.Println("Exporting sessions...") + err = s.exportSessions(ctx, tx, data.Sessions) + if err != nil { + return err + } + + logger.Println("Exporting session_signers...") + err = s.exportSessionSigners(ctx, tx, data.SessionSigners) + if err != nil { + return err + } + + logger.Println("Exporting session_works...") + err = s.exportSessionWorks(ctx, tx, data.SessionWorks) + if err != nil { + return err + } + + return tx.Commit() +} + +func (s *SQLite3Store) listProperties(ctx context.Context) ([]*Property, error) { + var cols = []string{"key", "value", "created_at"} + query := fmt.Sprintf("SELECT %s FROM properties", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var ps []*Property + for rows.Next() { + var p Property + err := rows.Scan(&p.Key, &p.Value, &p.CreatedAt) + if err != nil { + return nil, err + } + ps = append(ps, &p) + } + return ps, nil +} + +func (s *SQLite3Store) listSessions(ctx context.Context) ([]*Session, error) { + cols := "session_id, mixin_hash, mixin_index, operation, curve, public, extra, state, created_at, updated_at, committed_at, prepared_at" + query := fmt.Sprintf("SELECT %s FROM sessions", cols) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var sessions []*Session + for rows.Next() { + var r Session + err := rows.Scan(&r.Id, &r.MixinHash, &r.MixinIndex, &r.Operation, &r.Curve, &r.Public, &r.Extra, &r.State, &r.CreatedAt, &r.UpdatedAt, &r.CommittedAt, &r.PreparedAt) + if err != nil { + return nil, err + } + sessions = append(sessions, &r) + } + return sessions, nil +} + +func (s *SQLite3Store) listSessionSigners(ctx context.Context) ([]*SessionSigner, error) { + var cols = []string{"session_id", "signer_id", "extra", "created_at", "updated_at"} + query := fmt.Sprintf("SELECT %s FROM session_signers", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var signers []*SessionSigner + for rows.Next() { + var s SessionSigner + err := rows.Scan(&s.SessionId, &s.SignerId, &s.Extra, &s.CreatedAt, &s.UpdatedAt) + if err != nil { + return nil, err + } + signers = append(signers, &s) + } + return signers, nil +} + +func (s *SQLite3Store) listSessionWorks(ctx context.Context) ([]*SessionWork, error) { + var cols = []string{"session_id", "signer_id", "round", "extra", "created_at"} + query := fmt.Sprintf("SELECT %s FROM session_works", strings.Join(cols, ",")) + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var works []*SessionWork + for rows.Next() { + var s SessionWork + err := rows.Scan(&s.SessionId, &s.SignerId, &s.Round, &s.Extra, &s.CreatedAt) + if err != nil { + return nil, err + } + works = append(works, &s) + } + return works, nil +} + +func (s *SQLite3Store) exportProperties(ctx context.Context, tx *sql.Tx, properties []*Property) error { + for _, p := range properties { + cols := []string{"key", "value", "created_at"} + err := s.execOne(ctx, tx, buildInsertionSQL("properties", cols), p.Key, p.Value, p.CreatedAt) + if err != nil { + return fmt.Errorf("INSERT properties %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportSessions(ctx context.Context, tx *sql.Tx, sessions []*Session) error { + for _, session := range sessions { + cols := []string{"session_id", "mixin_hash", "mixin_index", "operation", "curve", "public", + "extra", "state", "created_at", "updated_at", "committed_at", "prepared_at"} + vals := []any{session.Id, session.MixinHash, session.MixinIndex, session.Operation, session.Curve, session.Public, + session.Extra, session.State, session.CreatedAt, session.UpdatedAt, session.CommittedAt, session.PreparedAt} + err := s.execOne(ctx, tx, buildInsertionSQL("sessions", cols), vals...) + if err != nil { + return fmt.Errorf("SQLite3Store INSERT sessions %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportSessionSigners(ctx context.Context, tx *sql.Tx, signers []*SessionSigner) error { + for _, signer := range signers { + cols := []string{"session_id", "signer_id", "extra", "created_at", "updated_at"} + err := s.execOne(ctx, tx, buildInsertionSQL("session_signers", cols), + signer.SessionId, signer.SignerId, signer.Extra, signer.CreatedAt, signer.UpdatedAt) + if err != nil { + return fmt.Errorf("SQLite3Store INSERT session_signers %v", err) + } + } + return nil +} + +func (s *SQLite3Store) exportSessionWorks(ctx context.Context, tx *sql.Tx, works []*SessionWork) error { + for _, work := range works { + cols := []string{"session_id", "signer_id", "round", "extra", "created_at"} + err := s.execOne(ctx, tx, buildInsertionSQL("session_works", cols), + work.SessionId, work.SignerId, work.Round, work.Extra, work.CreatedAt) + if err != nil { + return fmt.Errorf("SQLite3Store INSERT session_works %v", err) + } + } + return nil +} diff --git a/signer/group.go b/signer/group.go index cf070d93..72dea38a 100644 --- a/signer/group.go +++ b/signer/group.go @@ -35,16 +35,18 @@ const ( ) type Session struct { - Id string - MixinHash string - MixinIndex int - Operation byte - Curve byte - Public string - Extra string - State byte - CreatedAt time.Time - PreparedAt sql.NullTime + Id string + MixinHash string + MixinIndex int + Operation byte + Curve byte + Public string + Extra string + State byte + CreatedAt time.Time + UpdatedAt time.Time + CommittedAt sql.NullTime + PreparedAt sql.NullTime } type KeygenResult struct { diff --git a/signer/legacy.sql b/signer/legacy.sql new file mode 100644 index 00000000..f2331470 --- /dev/null +++ b/signer/legacy.sql @@ -0,0 +1,58 @@ +CREATE TABLE IF NOT EXISTS properties ( + key VARCHAR NOT NULL, + value VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY ('key') +); + +CREATE TABLE IF NOT EXISTS keys ( + public VARCHAR NOT NULL, + fingerprint VARCHAR NOT NULL, + curve INTEGER NOT NULL, + share VARHCAR NOT NULL, + session_id VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY ('public') +); + +CREATE UNIQUE INDEX IF NOT EXISTS keys_by_session_id ON keys(session_id); +CREATE UNIQUE INDEX IF NOT EXISTS keys_by_fingerprint ON keys(fingerprint); + +CREATE TABLE IF NOT EXISTS sessions ( + session_id VARCHAR NOT NULL, + mixin_hash VARCHAR NOT NULL, + mixin_index INTEGER NOT NULL, + operation INTEGER NOT NULL, + curve INTEGER NOT NULL, + public VARCHAR NOT NULL, + extra VARCHAR NOT NULL, + state INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + committed_at TIMESTAMP, + prepared_at TIMESTAMP, + PRIMARY KEY ('session_id') +); + +CREATE UNIQUE INDEX IF NOT EXISTS sessions_by_mixin_hash_index ON sessions(mixin_hash, mixin_index); +CREATE INDEX IF NOT EXISTS sessions_by_state_created ON sessions(state, created_at); + + +CREATE TABLE IF NOT EXISTS session_signers ( + session_id VARCHAR NOT NULL, + signer_id VARCHAR NOT NULL, + extra VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('session_id', 'signer_id') +); + + +CREATE TABLE IF NOT EXISTS session_works ( + session_id VARCHAR NOT NULL, + signer_id VARCHAR NOT NULL, + round INTEGER NOT NULL, + extra VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY ('session_id', 'signer_id', 'round') +); \ No newline at end of file diff --git a/signer/node.go b/signer/node.go index 9928bb9e..c5b0b982 100644 --- a/signer/node.go +++ b/signer/node.go @@ -77,14 +77,16 @@ func NewNode(store *SQLite3Store, group *mtg.Group, network Network, conf *Confi return node } -func (node *Node) Boot(ctx context.Context) { - err := node.store.Migrate(ctx) - if err != nil { - panic(err) - } - err = node.store.Migrate2(ctx) - if err != nil { - panic(err) +func (node *Node) Boot(ctx context.Context, fromStart bool) { + if !fromStart { + err := node.store.Migrate(ctx) + if err != nil { + panic(err) + } + err = node.store.Migrate2(ctx) + if err != nil { + panic(err) + } } go node.loopBackup(ctx) go node.loopInitialSessions(ctx) diff --git a/signer/store.go b/signer/store.go index 20464f25..9489da41 100644 --- a/signer/store.go +++ b/signer/store.go @@ -21,13 +21,42 @@ import ( //go:embed schema.sql var SCHEMA string +//go:embed legacy.sql +var LEGACY_SCHEMA string + type SQLite3Store struct { db *sql.DB mutex *sync.Mutex } -func OpenSQLite3Store(path string) (*SQLite3Store, error) { - db, err := common.OpenSQLite3Store(path, SCHEMA) +type Property struct { + Key string + Value string + CreatedAt time.Time +} + +type SessionSigner struct { + SessionId string + SignerId string + Extra string + CreatedAt time.Time + UpdatedAt time.Time +} + +type SessionWork struct { + SessionId string + SignerId string + Round int64 + Extra string + CreatedAt time.Time +} + +func OpenSQLite3Store(path string, legacy bool) (*SQLite3Store, error) { + schema := SCHEMA + if legacy { + schema = LEGACY_SCHEMA + } + db, err := common.OpenSQLite3Store(path, schema) if err != nil { return nil, err } diff --git a/signer/test.go b/signer/test.go index 89aea1e9..6474b149 100644 --- a/signer/test.go +++ b/signer/test.go @@ -205,7 +205,7 @@ func testBuildNode(ctx context.Context, require *require.Assertions, root string if !(strings.HasPrefix(conf.Signer.StoreDir, "/tmp/") || strings.HasPrefix(conf.Signer.StoreDir, "/var/folders")) { panic(root) } - kd, err := OpenSQLite3Store(conf.Signer.StoreDir + "/mpc.sqlite3") + kd, err := OpenSQLite3Store(conf.Signer.StoreDir+"/mpc.sqlite3", false) require.Nil(err) md, err := mtg.OpenSQLite3Store(conf.Signer.StoreDir + "/mtg.sqlite3")