package test import ( "fmt" "io/ioutil" "os" "path" "reflect" "github.com/vapor/account" "github.com/vapor/asset" "github.com/vapor/blockchain/pseudohsm" "github.com/vapor/blockchain/signers" "github.com/vapor/crypto/ed25519/chainkd" "github.com/vapor/database" dbm "github.com/vapor/database/leveldb" "github.com/vapor/event" "github.com/vapor/protocol" "github.com/vapor/protocol/bc/types" w "github.com/vapor/wallet" ) type walletTestConfig struct { Keys []*keyInfo `json:"keys"` Accounts []*accountInfo `json:"accounts"` Blocks []*wtBlock `json:"blocks"` RollbackTo uint64 `json:"rollback_to"` } type keyInfo struct { Name string `json:"name"` Password string `json:"password"` } type accountInfo struct { Name string `json:"name"` Keys []string `json:"keys"` Quorum int `json:"quorum"` } type wtBlock struct { CoinbaseAccount string `json:"coinbase_account"` Transactions []*wtTransaction `json:"transactions"` PostStates []*accountBalance `json:"post_states"` Append uint64 `json:"append"` } func (b *wtBlock) create(ctx *walletTestContext) (*types.Block, error) { transactions := make([]*types.Tx, 0, len(b.Transactions)) for _, t := range b.Transactions { tx, err := t.create(ctx) if err != nil { continue } transactions = append(transactions, tx) } return ctx.newBlock(transactions, b.CoinbaseAccount) } func (b *wtBlock) verifyPostStates(ctx *walletTestContext) error { for _, state := range b.PostStates { balance, err := ctx.getBalance(state.AccountAlias, state.AssetAlias) if err != nil { return err } if balance != state.Amount { return fmt.Errorf("AccountAlias: %s, AssetAlias: %s, expected: %d, have: %d", state.AccountAlias, state.AssetAlias, state.Amount, balance) } } return nil } type wtTransaction struct { Passwords []string `json:"passwords"` Inputs []*action `json:"inputs"` Outputs []*action `json:"outputs"` } // create signed transaction func (t *wtTransaction) create(ctx *walletTestContext) (*types.Tx, error) { generator := NewTxGenerator(ctx.Wallet.AccountMgr, ctx.Wallet.AssetReg, ctx.Wallet.Hsm) for _, input := range t.Inputs { switch input.Type { case "spend_account": if err := generator.AddSpendInput(input.AccountAlias, input.AssetAlias, input.Amount); err != nil { return nil, err } } } for _, output := range t.Outputs { switch output.Type { case "output": if err := generator.AddTxOutput(output.AccountAlias, output.AssetAlias, output.Amount); err != nil { return nil, err } case "retire": if err := generator.AddRetirement(output.AssetAlias, output.Amount); err != nil { return nil, err } } } return generator.Sign(t.Passwords) } type action struct { Type string `json:"type"` AccountAlias string `json:"name"` AssetAlias string `json:"asset"` Amount uint64 `json:"amount"` } type accountBalance struct { AssetAlias string `json:"asset"` AccountAlias string `json:"name"` Amount uint64 `json:"amount"` } type walletTestContext struct { Wallet *w.Wallet Chain *protocol.Chain } func (ctx *walletTestContext) createControlProgram(accountName string, change bool) (*account.CtrlProgram, error) { acc, err := ctx.Wallet.AccountMgr.FindByAlias(accountName) if err != nil { return nil, err } return ctx.Wallet.AccountMgr.CreateAddress(acc.ID, change) } func (ctx *walletTestContext) getPubkey(keyAlias string) *chainkd.XPub { pubKeys := ctx.Wallet.Hsm.ListKeys() for i, key := range pubKeys { if key.Alias == keyAlias { return &pubKeys[i].XPub } } return nil } func (ctx *walletTestContext) newBlock(txs []*types.Tx, coinbaseAccount string) (*types.Block, error) { controlProgram, err := ctx.createControlProgram(coinbaseAccount, true) if err != nil { return nil, err } return NewBlock(ctx.Chain, txs, controlProgram.ControlProgram) } func (ctx *walletTestContext) createKey(name string, password string) error { _, _, err := ctx.Wallet.Hsm.XCreate(name, password, "en") return err } func (ctx *walletTestContext) createAccount(name string, keys []string, quorum int) error { xpubs := []chainkd.XPub{} for _, alias := range keys { xpub := ctx.getPubkey(alias) if xpub == nil { return fmt.Errorf("can't find pubkey for %s", alias) } xpubs = append(xpubs, *xpub) } _, err := ctx.Wallet.AccountMgr.Create(xpubs, quorum, name, signers.BIP0044) return err } func (ctx *walletTestContext) update(block *types.Block) error { if _, err := ctx.Chain.ProcessBlock(block); err != nil { return err } if err := ctx.Wallet.AttachBlock(block); err != nil { return err } return nil } func (ctx *walletTestContext) getBalance(accountAlias string, assetAlias string) (uint64, error) { balances, _ := ctx.Wallet.GetAccountBalances("", "") for _, balance := range balances { if balance.Alias == accountAlias && balance.AssetAlias == assetAlias { return balance.Amount, nil } } return 0, nil } func (ctx *walletTestContext) getAccBalances() map[string]map[string]uint64 { accBalances := make(map[string]map[string]uint64) balances, _ := ctx.Wallet.GetAccountBalances("", "") for _, balance := range balances { if accBalance, ok := accBalances[balance.Alias]; ok { if _, ok := accBalance[balance.AssetAlias]; ok { accBalance[balance.AssetAlias] += balance.Amount continue } accBalance[balance.AssetAlias] = balance.Amount continue } accBalances[balance.Alias] = map[string]uint64{balance.AssetAlias: balance.Amount} } return accBalances } func (ctx *walletTestContext) getDetachedBlocks(height uint64) ([]*types.Block, error) { currentHeight := ctx.Chain.BestBlockHeight() detachedBlocks := make([]*types.Block, 0, currentHeight-height) for i := currentHeight; i > height; i-- { block, err := ctx.Chain.GetBlockByHeight(i) if err != nil { return detachedBlocks, err } detachedBlocks = append(detachedBlocks, block) } return detachedBlocks, nil } func (ctx *walletTestContext) validateRollback(oldAccBalances map[string]map[string]uint64) error { accBalances := ctx.getAccBalances() if reflect.DeepEqual(oldAccBalances, accBalances) { return nil } return fmt.Errorf("different account balances after rollback") } func (cfg *walletTestConfig) Run() error { dirPath, err := ioutil.TempDir(".", "pseudo_hsm") if err != nil { return err } defer os.RemoveAll(dirPath) hsm, err := pseudohsm.New(dirPath) if err != nil { return err } db := dbm.NewDB("wallet_test_db", "leveldb", path.Join(dirPath, "wallet_test_db")) chain, _, _, err := MockChain(db) if err != nil { return err } walletDB := dbm.NewDB("wallet", "leveldb", path.Join(dirPath, "wallet_db")) walletStore := database.NewWalletStore(walletDB) accountStore := database.NewAccountStore(walletDB) accountManager := account.NewManager(accountStore, chain) assets := asset.NewRegistry(walletDB, chain) dispatcher := event.NewDispatcher() wallet, err := w.NewWallet(walletStore, accountManager, assets, hsm, chain, dispatcher, false) if err != nil { return err } ctx := &walletTestContext{ Wallet: wallet, Chain: chain, } for _, key := range cfg.Keys { if err := ctx.createKey(key.Name, key.Password); err != nil { return err } } for _, acc := range cfg.Accounts { if err := ctx.createAccount(acc.Name, acc.Keys, acc.Quorum); err != nil { return err } } var accBalances map[string]map[string]uint64 var rollbackBlock *types.Block for _, blk := range cfg.Blocks { block, err := blk.create(ctx) if err != nil { return err } if err := ctx.update(block); err != nil { return err } if err := blk.verifyPostStates(ctx); err != nil { return err } if block.Height <= cfg.RollbackTo && cfg.RollbackTo <= block.Height+blk.Append { accBalances = ctx.getAccBalances() rollbackBlock = block } if err := AppendBlocks(ctx.Chain, blk.Append); err != nil { return err } } if rollbackBlock == nil { return nil } // rollback and validate detachedBlocks, err := ctx.getDetachedBlocks(rollbackBlock.Height) if err != nil { return err } forkPath, err := ioutil.TempDir(".", "forked_chain") if err != nil { return err } forkedChain, err := declChain(forkPath, ctx.Chain, rollbackBlock.Height, ctx.Chain.BestBlockHeight()+1) defer os.RemoveAll(forkPath) if err != nil { return err } if err := merge(forkedChain, ctx.Chain); err != nil { return err } for _, block := range detachedBlocks { if err := ctx.Wallet.DetachBlock(block); err != nil { return err } } return ctx.validateRollback(accBalances) }