diff --git a/go/consensus/tendermint/abci/mux_mock.go b/go/consensus/tendermint/abci/mux_mock.go index 5d5a0886f7b..e3566098a69 100644 --- a/go/consensus/tendermint/abci/mux_mock.go +++ b/go/consensus/tendermint/abci/mux_mock.go @@ -1,10 +1,9 @@ -// +build gofuzz - package abci import ( "context" + epochtime "github.com/oasislabs/oasis-core/go/epochtime/api" upgrade "github.com/oasislabs/oasis-core/go/upgrade/api" ) @@ -18,6 +17,17 @@ func (mux *MockABCIMux) MockRegisterApp(app Application) error { return mux.doRegister(app) } +// MockSetEpochtime sets the timesource used by this muxer when testing. +func (mux *MockABCIMux) MockSetEpochtime(epochTime epochtime.Backend) { + mux.state.timeSource = epochTime +} + +// MockSetTransactionAuthHandler sets the transaction auth hander used by +// this muxer when testing. +func (mux *MockABCIMux) MockSetTransactionAuthHandler(handler TransactionAuthHandler) { + mux.state.txAuthHandler = handler +} + // MockClose cleans up the muxer's state; it must be called once the muxer is no longer needed. func (mux *MockABCIMux) MockClose() { mux.doCleanup() diff --git a/go/oasis-node/cmd/debug/consim/consim.go b/go/oasis-node/cmd/debug/consim/consim.go new file mode 100644 index 00000000000..b2bbdd53b04 --- /dev/null +++ b/go/oasis-node/cmd/debug/consim/consim.go @@ -0,0 +1,233 @@ +// Package consim implements the mock consensus simulator. +package consim + +import ( + "context" + "crypto" + "encoding/hex" + "fmt" + "math/rand" + "path/filepath" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/oasislabs/oasis-core/go/common/crypto/drbg" + "github.com/oasislabs/oasis-core/go/common/crypto/mathrand" + "github.com/oasislabs/oasis-core/go/common/logging" + "github.com/oasislabs/oasis-core/go/consensus/tendermint/abci" + registryApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/registry" + stakingApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/staking" + genesisFile "github.com/oasislabs/oasis-core/go/genesis/file" + cmdCommon "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common" + cmdFlags "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/flags" +) + +const ( + cfgWorkload = "consim.workload" + cfgWorkloadSeed = "consim.workload.seed" +) + +var ( + logger = logging.GetLogger("cmd/consim") + + flagsConsim = flag.NewFlagSet("", flag.ContinueOnError) + + conSimCmd = &cobra.Command{ + Use: "consim", + Short: "mock consensus simulator", + RunE: doRun, + } +) + +func doRun(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + if err := cmdCommon.Init(); err != nil { + cmdCommon.EarlyLogAndExit(err) + } + + dataDir := cmdCommon.DataDir() + if dataDir == "" { + return fmt.Errorf("datadir is mandatory") + } + + ctx, cancelFn := context.WithCancel(context.Background()) + defer cancelFn() + + // Load the genesis document. + genesisProvider, err := genesisFile.DefaultFileProvider() + if err != nil { + logger.Error("failed to initialize genesis provider", + "err", err, + ) + return err + } + genesisDoc, err := genesisProvider.GetGenesisDocument() + if err != nil { + logger.Error("failed to get genesis document", + "err", err, + ) + return err + } + genesisDoc.SetChainContext() + tmChainID := genesisDoc.ChainContext()[:tmtypes.MaxChainIDLen] + + // Initialize the DRBG and workload. + rngSrc, err := drbg.New(crypto.SHA512, []byte(viper.GetString(cfgWorkloadSeed)), nil, []byte("consim workload generator")) + if err != nil { + logger.Error("failed to initialize DRBG", + "err", err, + ) + return err + } + + workload, err := newWorkload(rand.New(mathrand.New(rngSrc))) + if err != nil { + logger.Error("failed to create workload", + "err", err, + ) + return err + } + defer workload.Cleanup() + + if err = workload.Init(genesisDoc); err != nil { + logger.Error("failed to initialize workload", + "err", err, + ) + return err + } + + // Initialize the mock chain backend. + txAuthApp := stakingApp.New() + cfg := &mockChainCfg{ + dataDir: dataDir, + apps: []abci.Application{ + registryApp.New(), + txAuthApp, // This is the staking app. + }, + genesisDoc: genesisDoc, + tmChainID: tmChainID, + txAuthHandler: txAuthApp.(abci.TransactionAuthHandler), + } + mockChain, err := initMockChain(ctx, cfg) + if err != nil { + logger.Error("failed to initialize mock chain backend", + "err", err, + ) + return err + } + defer mockChain.close() + chainState, err := mockChain.stateToGenesis(ctx) + if err != nil { + logger.Error("failed to obtain chain state", + "err", err, + ) + return err + } + + // Start the workload. + cancelCh, errCh := make(chan struct{}), make(chan error) + defer close(cancelCh) + txVecCh, err := workload.Start(chainState, cancelCh, errCh) + if err != nil { + logger.Error("failed to start workload", + "err", err, + ) + return err + } + + // Emulate the tendermint block generation loop. +txLoop: + for { + var ( + txVec []BlockTx + ok bool + ) + select { + case err = <-errCh: + logger.Error("workload error", + "err", err, + ) + return err + case txVec, ok = <-txVecCh: + if !ok { + break txLoop + } + } + + mockChain.beginBlock() + + // CheckTx all the pending transactions for this block. + var toDeliver []BlockTx + for _, v := range txVec { + txCode := mockChain.checkTx(v.Tx) + if txCode != v.Code { + logger.Error("CheckTx response code mismatch", + "tx", hex.EncodeToString(v.Tx), + "code", txCode, + ) + return fmt.Errorf("consim: CheckTx response code mismatch") + } else if v.Code == types.CodeTypeOK { + toDeliver = append(toDeliver, v) + } + } + + // DeliverTx all the pending transactions for this block. + for _, v := range toDeliver { + txCode := mockChain.deliverTx(v.Tx) + if txCode != types.CodeTypeOK { + logger.Error("DeliverTx failed", + "tx", hex.EncodeToString(v.Tx), + "code", txCode, + ) + return fmt.Errorf("consim: DeliverTx response code mismatch") + } + } + + mockChain.endBlock() + } + + // Dump the final state to a JSON document. + finalGenesis, err := mockChain.stateToGenesis(ctx) + if err != nil { + logger.Error("failed to obtain state dump", + "err", err, + ) + return err + } + + if err = workload.Finalize(finalGenesis); err != nil { + logger.Error("failed to finalize workload", + "err", err, + ) + return err + } + + if err = finalGenesis.WriteFileJSON(filepath.Join(dataDir, "dump.json")); err != nil { + logger.Error("failed to write state dump", + "err", err, + ) + return err + } + + return nil +} + +// Register registers the consim sub-command. +func Register(parentCmd *cobra.Command) { + conSimCmd.Flags().AddFlagSet(cmdFlags.GenesisFileFlags) + conSimCmd.Flags().AddFlagSet(flagsConsim) + conSimCmd.Flags().AddFlagSet(fileTxsFlag) + conSimCmd.Flags().AddFlagSet(xferFlags) + parentCmd.AddCommand(conSimCmd) +} + +func init() { + flagsConsim.String(cfgWorkload, fileWorkloadName, "workload to execute") + flagsConsim.String(cfgWorkloadSeed, "seeeeeeeeeeeeeeeeeeeeeeeeeeeeeed", "DRBG seed for workloads") + _ = viper.BindPFlags(flagsConsim) +} diff --git a/go/oasis-node/cmd/debug/consim/file_workload.go b/go/oasis-node/cmd/debug/consim/file_workload.go new file mode 100644 index 00000000000..4bbd459c0f1 --- /dev/null +++ b/go/oasis-node/cmd/debug/consim/file_workload.go @@ -0,0 +1,86 @@ +package consim + +import ( + "bufio" + "encoding/json" + "fmt" + "math/rand" + "os" + + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + genesis "github.com/oasislabs/oasis-core/go/genesis/api" +) + +const ( + cfgFileTxs = "consim.workload.file.txs" + fileWorkloadName = "file" +) + +var fileTxsFlag = flag.NewFlagSet("", flag.ContinueOnError) + +type fileWorkload struct { + ch chan []BlockTx + dec *json.Decoder + + f *os.File +} + +func (w *fileWorkload) Init(doc *genesis.Document) error { + return nil +} + +func (w *fileWorkload) Start(doc *genesis.Document, cancelCh <-chan struct{}, errCh chan<- error) (<-chan []BlockTx, error) { + if _, err := w.dec.Token(); err != nil { + return nil, fmt.Errorf("consim/workload/file: failed to find opening delimiter: %w", err) + } + w.ch = make(chan []BlockTx) + + go func() { + defer close(w.ch) + for w.dec.More() { + var txVec []BlockTx + if err := w.dec.Decode(&txVec); err != nil { + errCh <- fmt.Errorf("consim/workload/file: failed to decode block tx: %w", err) + return + } + select { + case <-cancelCh: + return + case w.ch <- txVec: + } + } + }() + + return w.ch, nil +} + +func (w *fileWorkload) Finalize(*genesis.Document) error { + if _, err := w.dec.Token(); err != nil { + return fmt.Errorf("consim/workload/file: failed to find closing delimiter: %w", err) + } + + return nil +} + +func (w *fileWorkload) Cleanup() { + _ = w.f.Close() +} + +func newFileWorkload(rng *rand.Rand) (Workload, error) { + f, err := os.Open(viper.GetString(cfgFileTxs)) + if err != nil { + return nil, fmt.Errorf("consim/workload/file: failed to open transaction file: %w", err) + } + + return &fileWorkload{ + dec: json.NewDecoder(bufio.NewReader(f)), + f: f, + }, nil +} + +func init() { + fileTxsFlag.String(cfgFileTxs, "transactions.json", "path to transactions document") + _ = viper.BindPFlags(fileTxsFlag) +} diff --git a/go/oasis-node/cmd/debug/consim/mockchain.go b/go/oasis-node/cmd/debug/consim/mockchain.go new file mode 100644 index 00000000000..4dcdc22d9d2 --- /dev/null +++ b/go/oasis-node/cmd/debug/consim/mockchain.go @@ -0,0 +1,207 @@ +package consim + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "math" + "time" + + "github.com/tendermint/tendermint/abci/types" + + "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/memory" + "github.com/oasislabs/oasis-core/go/consensus/tendermint/abci" + registryApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/registry" + stakingApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/staking" + genesis "github.com/oasislabs/oasis-core/go/genesis/api" + "github.com/oasislabs/oasis-core/go/upgrade" +) + +type mockChainCfg struct { + dataDir string + apps []abci.Application + genesisDoc *genesis.Document + tmChainID string + txAuthHandler abci.TransactionAuthHandler +} + +type mockChain struct { + cfg *mockChainCfg + + mux *abci.MockABCIMux + timeSource *simTimeSource + + tmChainID string + + now time.Time + hash []byte + height int64 +} + +func (m *mockChain) beginBlock() { + m.height++ + m.now = m.now.Add(time.Second) + + m.mux.BeginBlock(types.RequestBeginBlock{ + Hash: m.hash, + Header: types.Header{ + ChainID: m.tmChainID, + Height: m.height, + Time: m.now, + }, + }) +} + +func (m *mockChain) checkTx(tx []byte) uint32 { + checkResp := m.mux.CheckTx(types.RequestCheckTx{ + Tx: tx, + Type: types.CheckTxType_New, + }) + return checkResp.Code +} + +func (m *mockChain) deliverTx(tx []byte) uint32 { + deliverResp := m.mux.DeliverTx(types.RequestDeliverTx{ + Tx: tx, + }) + if deliverResp.Code != types.CodeTypeOK { + logger.Debug("deliverTx failure", + "code", deliverResp.Code, + "log", deliverResp.Log, + ) + } + return deliverResp.Code +} + +func (m *mockChain) endBlock() { + m.mux.EndBlock(types.RequestEndBlock{ + Height: m.height, + }) + + respCommit := m.mux.Commit() + m.hash = respCommit.Data + + logger.Debug("block generated", + "height", m.height, + "hash", hex.EncodeToString(m.hash), + ) +} + +func (m *mockChain) stateToGenesis(ctx context.Context) (*genesis.Document, error) { + var err error + + doc := &genesis.Document{ + Height: m.height, + Time: m.now, + ChainID: m.cfg.genesisDoc.ChainID, + } + + // Dump the application state. + qHeight := m.height + 1 // Fuck if I know. + for _, v := range m.cfg.apps { + qfi := v.QueryFactory() + switch qf := qfi.(type) { + case *registryApp.QueryFactory: + var query registryApp.Query + if query, err = qf.QueryAt(ctx, qHeight); err != nil { + return nil, fmt.Errorf("consim/mockchain: failed to create registry query: %w", err) + } + regGen, qErr := query.Genesis(ctx) + if qErr != nil { + return nil, fmt.Errorf("consim/mockchain: failed to query registry state: %w", qErr) + } + doc.Registry = *regGen + case *stakingApp.QueryFactory: + var query stakingApp.Query + if query, err = qf.QueryAt(ctx, qHeight); err != nil { + return nil, fmt.Errorf("consim/mockchain: failed to create staking query: %w", err) + } + stGen, qErr := query.Genesis(ctx) + if qErr != nil { + return nil, fmt.Errorf("consim/mockchain: failed to query staking state: %w", qErr) + } + doc.Staking = *stGen + default: + logger.Warn("unsupported query factory", + "type", fmt.Sprintf("%T", qf), + ) + } + } + + // The timesource is "special". + tGen, _ := m.timeSource.StateToGenesis(ctx, qHeight) + doc.EpochTime = *tGen + + return doc, nil +} + +func (m *mockChain) close() { + m.mux.MockClose() +} + +func initMockChain(ctx context.Context, cfg *mockChainCfg) (*mockChain, error) { + // Initialize an ephemeral local signer. + localSigner, err := memory.NewSigner(rand.Reader) + if err != nil { + logger.Error("failed to initialize local signer", + "err", err, + ) + return nil, err + } + + // Initialize the mock ABCI backend. + muxCfg := &abci.ApplicationConfig{ + DataDir: cfg.dataDir, + HaltEpochHeight: math.MaxUint64, + MinGasPrice: 0, // XXX: Should this be configurable? + OwnTxSigner: localSigner.Public(), + } + mux, err := abci.NewMockMux(ctx, upgrade.NewDummyUpgradeManager(), muxCfg) + if err != nil { + logger.Error("failed to initialize mock mux", + "err", err, + ) + return nil, err + } + + m := &mockChain{ + cfg: cfg, + mux: mux, + timeSource: newSimTimeSource(&cfg.genesisDoc.EpochTime), + tmChainID: cfg.tmChainID, + now: cfg.genesisDoc.Time, + } + m.mux.MockSetEpochtime(m.timeSource) + m.mux.MockSetTransactionAuthHandler(cfg.txAuthHandler) + for _, v := range cfg.apps { + _ = mux.MockRegisterApp(v) + } + + // InitChain. + muxInfo := m.mux.Info(types.RequestInfo{}) + rawGenesisDoc, _ := json.Marshal(cfg.genesisDoc) + + switch muxInfo.LastBlockHeight { + case 0: + _ = m.mux.InitChain(types.RequestInitChain{ + Time: m.now, + ChainId: m.tmChainID, + AppStateBytes: rawGenesisDoc, + ConsensusParams: nil, + }) + respCommit := m.mux.Commit() + m.hash = respCommit.Data + default: + m.height = muxInfo.LastBlockHeight + m.hash = muxInfo.LastBlockAppHash + m.now = m.now.Add(time.Duration(m.height) * time.Second) + logger.Warn("existing ABCI state exists, skipping InitChain", + "height", m.height, + "hash", hex.EncodeToString(m.hash), + ) + } + + return m, nil +} diff --git a/go/oasis-node/cmd/debug/consim/timesource.go b/go/oasis-node/cmd/debug/consim/timesource.go new file mode 100644 index 00000000000..664365d646f --- /dev/null +++ b/go/oasis-node/cmd/debug/consim/timesource.go @@ -0,0 +1,57 @@ +package consim + +import ( + "context" + "fmt" + + "github.com/oasislabs/oasis-core/go/common/pubsub" + "github.com/oasislabs/oasis-core/go/epochtime/api" +) + +type simTimeSource struct { + base api.EpochTime + current api.EpochTime + interval int64 +} + +func (b *simTimeSource) GetBaseEpoch(ctx context.Context) (api.EpochTime, error) { + return b.base, nil +} + +func (b *simTimeSource) GetEpoch(ctx context.Context, height int64) (api.EpochTime, error) { + if height == 0 { + return b.current, nil + } + return b.base + api.EpochTime(height/b.interval), nil +} + +func (b *simTimeSource) GetEpochBlock(ctx context.Context, epoch api.EpochTime) (int64, error) { + if epoch < b.base { + return 0, fmt.Errorf("consim/epochtime: epoch predates base") + } + height := int64(epoch-b.base) * b.interval + return height, nil +} + +func (b *simTimeSource) WatchEpochs() (<-chan api.EpochTime, *pubsub.Subscription) { + panic("consim/epochtime: WatchEpochs not supported") +} + +func (b *simTimeSource) StateToGenesis(ctx context.Context, height int64) (*api.Genesis, error) { + // WARNING: This ignores the height because it's only used for the final + // dump. + return &api.Genesis{ + Base: b.current, + Parameters: api.ConsensusParameters{ + Interval: b.interval, + }, + }, nil +} + +func newSimTimeSource(genesis *api.Genesis) *simTimeSource { + return &simTimeSource{ + base: genesis.Base, + current: genesis.Base, + interval: genesis.Parameters.Interval, + } +} diff --git a/go/oasis-node/cmd/debug/consim/workload.go b/go/oasis-node/cmd/debug/consim/workload.go new file mode 100644 index 00000000000..c544de75a2d --- /dev/null +++ b/go/oasis-node/cmd/debug/consim/workload.go @@ -0,0 +1,49 @@ +package consim + +import ( + "fmt" + "math/rand" + "strings" + + "github.com/spf13/viper" + + genesis "github.com/oasislabs/oasis-core/go/genesis/api" +) + +// Tx is a single transaction, and the expected Check/DeliverTx status +// code. +type BlockTx struct { + Tx []byte + Code uint32 +} + +// Workload is a simulator workload. +type Workload interface { + // Init initializes the workload (and alters the genesis document as required). + Init(*genesis.Document) error + + // Start starts the workload. + // + // Note: The genesis document is the initial chain state, after the fixups + // from Init are applied, and existing state is loaded from disk. + Start(*genesis.Document, <-chan struct{}, chan<- error) (<-chan []BlockTx, error) + + // Finalize is called after the workload is complete with the final chain state. + Finalize(*genesis.Document) error + + // Cleanup cleans up the workload. + Cleanup() +} + +func newWorkload(rng *rand.Rand) (Workload, error) { + wName := viper.GetString(cfgWorkload) + + switch strings.ToLower(wName) { + case xferWorkloadName: + return newXferWorkload(rng) + case fileWorkloadName: + return newFileWorkload(rng) + default: + } + return nil, fmt.Errorf("consim: unsupported workload: '%v'", wName) +} diff --git a/go/oasis-node/cmd/debug/consim/xfer_workload.go b/go/oasis-node/cmd/debug/consim/xfer_workload.go new file mode 100644 index 00000000000..747646e7e1a --- /dev/null +++ b/go/oasis-node/cmd/debug/consim/xfer_workload.go @@ -0,0 +1,235 @@ +package consim + +import ( + "fmt" + "math/rand" + + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/oasislabs/oasis-core/go/common/cbor" + "github.com/oasislabs/oasis-core/go/common/crypto/signature" + "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/memory" + "github.com/oasislabs/oasis-core/go/common/entity" + "github.com/oasislabs/oasis-core/go/common/quantity" + "github.com/oasislabs/oasis-core/go/consensus/api/transaction" + genesis "github.com/oasislabs/oasis-core/go/genesis/api" + staking "github.com/oasislabs/oasis-core/go/staking/api" +) + +const ( + // TODO: Should these be made configurable? + transferNumAccounts = 10 + transferFundAmount = 1000 + transferAmount = 1 + + xferWorkloadName = "xfer" + + cfgXferIterations = "consim.workload.xfer.iterations" +) + +var xferFlags = flag.NewFlagSet("", flag.ContinueOnError) + +type xferWorkload struct { + ch chan []BlockTx + + rng *rand.Rand + + fundingAccount *xferAccount + accounts []*xferAccount +} + +type xferAccount struct { + signer signature.Signer + nonce uint64 + balance quantity.Quantity +} + +func (w *xferWorkload) Init(doc *genesis.Document) error { + // Check/fix the genesis document. + // + // Right now the workload is blissfully gas unaware, and will break if + // staking transfer transactions actually cost gas. Fossil fuels are + // bad for the environment, transactions should be nuclear powered + // instead. + if doc.Staking.Parameters.GasCosts[staking.GasOpTransfer] > 0 { + logger.Warn("consim/workload/xfer: forcing transfer op gas cost to zero") + doc.Staking.Parameters.GasCosts[staking.GasOpTransfer] = 0 + } + + // Ensure the genesis doc has the debug test entity to be used to + // fund the accounts. + testEntity, _, _ := entity.TestEntity() + testAccount := doc.Staking.Ledger[testEntity.ID] + if testAccount == nil { + return fmt.Errorf("consim/workload/xfer: test entity not present in genesis") + } + if !xferHasEnoughBalance(&testAccount.General.Balance, transferFundAmount*transferNumAccounts) { + return fmt.Errorf("consim/workload/xfer: test entity has insufficient balance") + } + + return nil +} + +func (w *xferWorkload) Start(initialState *genesis.Document, cancelCh <-chan struct{}, errCh chan<- error) (<-chan []BlockTx, error) { + // Initialize the funding account. + testEntity, testSigner, _ := entity.TestEntity() + testAccount := initialState.Staking.Ledger[testEntity.ID] + w.fundingAccount = &xferAccount{ + signer: testSigner, + nonce: testAccount.General.Nonce, + balance: testAccount.General.Balance, + } + + // Initialize the test accounts. + for i := 0; i < transferNumAccounts; i++ { + accSigner, err := memory.NewSigner(w.rng) + if err != nil { + return nil, fmt.Errorf("consim/workload/xfer: failed to create signer: %w", err) + } + + acc := &xferAccount{ + signer: accSigner, + } + if lacc := initialState.Staking.Ledger[accSigner.Public()]; lacc != nil { + acc.nonce = lacc.General.Nonce + acc.balance = lacc.General.Balance + } + w.accounts = append(w.accounts, acc) + } + + w.ch = make(chan []BlockTx) + + go w.worker(cancelCh, errCh) + + return w.ch, nil +} + +func (w *xferWorkload) Finalize(finalState *genesis.Document) error { + for _, acc := range w.accounts { + id := acc.signer.Public() + lacc := finalState.Staking.Ledger[id] + if lacc == nil { + return fmt.Errorf("consim/workload/xfer: account missing: %v", id) + } + if lacc.General.Nonce != acc.nonce { + return fmt.Errorf("consim/workload/xfer: nonce mismatch: %v (expected: %v, actual: %v)", id, acc.nonce, lacc.General.Nonce) + } + if lacc.General.Balance.Cmp(&acc.balance) != 0 { + return fmt.Errorf("consim/workload/xfer: balance mismatch: %v (expected: %v, actual: %v)", id, acc.balance, lacc.General.Balance) + } + } + return nil +} + +func (w *xferWorkload) Cleanup() {} + +func (w *xferWorkload) worker(cancelCh <-chan struct{}, errCh chan<- error) { + defer close(w.ch) + + // Fund all the accounts. + // Note: This needs to be done 1 tx/block(?). + for _, v := range w.accounts { + tx, err := xferGenTx(w.fundingAccount, v, transferFundAmount) + if err != nil { + errCh <- err + return + } + w.ch <- []BlockTx{BlockTx{Tx: tx}} + } + + numAccounts, numIterations := len(w.accounts), viper.GetInt(cfgXferIterations) + + // Shuffle tokens around till bored. + for nBlocks := 0; nBlocks < numIterations; nBlocks++ { + // Check for cancelation due to errors. + select { + case <-cancelCh: + return + default: + } + + numTxsInBlock := w.rng.Intn(numAccounts) + + var xferTxs []BlockTx + fromPerm := w.rng.Perm(numAccounts) + toPerm := w.rng.Perm(numAccounts) + for i := 0; i < numTxsInBlock; i++ { + from := w.accounts[fromPerm[i]] + if !xferHasEnoughBalance(&from.balance, transferAmount) { + continue + } + to := w.accounts[toPerm[i]] + if from.signer.Public().Equal(to.signer.Public()) { + // The helper doesn't support this at the moment. + continue + } + tx, err := xferGenTx(from, to, transferAmount) + if err != nil { + errCh <- err + return + } + xferTxs = append(xferTxs, BlockTx{ + Tx: tx, + }) + } + if len(xferTxs) > 0 { + w.ch <- xferTxs + } + } +} + +func xferGenTx(from, to *xferAccount, amount uint64) ([]byte, error) { + // TODO: At some point this should pay gas, for now don't under + // the assumption that transactions are free. + xfer := &staking.Transfer{ + To: to.signer.Public(), + } + if err := xfer.Tokens.FromUint64(amount); err != nil { + return nil, err + } + + var fee transaction.Fee + tx := staking.NewTransferTx(from.nonce, &fee, xfer) + signedTx, err := transaction.Sign(from.signer, tx) + if err != nil { + return nil, err + } + + logger.Debug("TX", + "from", from.signer.Public(), + "to", to.signer.Public(), + "nonce", from.nonce, + ) + + // Update the state on the assumption that the tx will be submitted + // successfully. + // + // Note: The Move call will break if from == to, so don't do that. + from.nonce++ + if err = quantity.Move(&to.balance, &from.balance, &xfer.Tokens); err != nil { + return nil, err + } + + return cbor.Marshal(signedTx), nil +} + +func xferHasEnoughBalance(bal *quantity.Quantity, amnt uint64) bool { + var target quantity.Quantity + if err := target.FromUint64(amnt); err != nil { + return false + } + + return bal.Cmp(&target) >= 0 +} + +func newXferWorkload(rng *rand.Rand) (Workload, error) { + return &xferWorkload{ + rng: rng, + }, nil +} + +func init() { + xferFlags.Int(cfgXferIterations, 10000, "number of iterations") + _ = viper.BindPFlags(xferFlags) +} diff --git a/go/oasis-node/cmd/debug/debug.go b/go/oasis-node/cmd/debug/debug.go index d69dc86de48..b1f7362f2c4 100644 --- a/go/oasis-node/cmd/debug/debug.go +++ b/go/oasis-node/cmd/debug/debug.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/byzantine" + "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/consim" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/control" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/fixgenesis" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/storage" @@ -25,6 +26,7 @@ func Register(parentCmd *cobra.Command) { txsource.Register(debugCmd) fixgenesis.Register(debugCmd) control.Register(debugCmd) + consim.Register(debugCmd) parentCmd.AddCommand(debugCmd) } diff --git a/go/oasis-node/consim-genesis.json b/go/oasis-node/consim-genesis.json new file mode 100644 index 00000000000..bdb2dcca4ff --- /dev/null +++ b/go/oasis-node/consim-genesis.json @@ -0,0 +1 @@ +{"height":0,"genesis_time":"2020-04-01T11:27:11.906400344Z","chain_id":"test","epochtime":{"params":{"interval":86400,"debug_mock_backend":false},"base":0},"registry":{"params":{"gas_costs":{"deregister_entity":1000,"register_entity":1000,"register_node":1000,"register_runtime":1000,"runtime_epoch_maintenance":1000,"unfreeze_node":1000,"update_keymanager":1000},"max_node_expiration":5},"entities":[{"untrusted_raw_value":"o2JpZFggTqUyj5Q+9vZtqu10yw6Zw7HEX3Ywe0JQA9vHyzY47TVlbm9kZXP2eBlhbGxvd19lbnRpdHlfc2lnbmVkX25vZGVz9Q==","signature":{"public_key":"TqUyj5Q+9vZtqu10yw6Zw7HEX3Ywe0JQA9vHyzY47TU=","signature":"5CnlsannKM749UmwEadxMJ6WTAQA4tffytgwUm8WLYdpcMEBXv9aK4qwATUrO5O3hJyob0uCRRX3MHk8o/qWDw=="}}]},"roothash":{"params":{"gas_costs":{"compute_commit":1000,"merge_commit":1000}}},"staking":{"params":{"thresholds":{"0":"0","1":"0","2":"0","3":"0","4":"0","5":"0","6":"0"},"debonding_interval":2,"commission_schedule_rules":{"rate_change_interval":10,"rate_bound_lead":30,"max_rate_steps":12,"max_bound_steps":12},"gas_costs":{"add_escrow":10,"burn":10,"reclaim_escrow":10,"transfer":10},"min_delegation":"0","fee_split_weight_propose":"2","fee_split_weight_vote":"1","fee_split_weight_next_propose":"1","reward_factor_epoch_signed":"0","reward_factor_block_proposed":"0"},"total_supply":"290000000000","common_pool":"0","last_block_fees":"0","ledger":{"B14NG+Joea3j09rVt0SEPQ4Z6Ned1EEi6rzZoa2YrYw=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"DbeoxcRwDO4Wh8bwq5rAR7wzhiB+LeYn+y7lFSGAZ7I=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"IluobMWMoZvxeW5ceAEiQj+TJUXZZJu6ZCWgtZxsHds=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"LQu4ZtFg8OJ0MC4M4QMeUR7Is6Xt4A/CW+PK/7TPiH0=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"TqUyj5Q+9vZtqu10yw6Zw7HEX3Ywe0JQA9vHyzY47TU=":{"general":{"balance":"100000000000","nonce":0},"escrow":{"active":{"balance":"100000000000","total_shares":"1"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"hcWVUucu/MwOyEDmj5T4TACwpgre5e2cJLczBggxxm0=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"iPYyR3I45FRGK568PTJIZUmELgcFYPhIb80e2GxrIf0=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"mh+b9hN+2oLAflZACgBgVHA0EjeZK3ew6kml75s2wN4=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"oWk0qdNhp7XNQDX4YFJY2KeFM+9u4fo16DeSdW1gChg=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}},"toAWSrxsxSFbGXQIhu4p2fOL0z/fnirrGT/MuzRgcW8=":{"general":{"balance":"10000000000","nonce":0},"escrow":{"active":{"balance":"0","total_shares":"0"},"debonding":{"balance":"0","total_shares":"0"},"commission_schedule":{"rates":null,"bounds":null},"stake_accumulator":{}}}},"delegations":{"TqUyj5Q+9vZtqu10yw6Zw7HEX3Ywe0JQA9vHyzY47TU=":{"TqUyj5Q+9vZtqu10yw6Zw7HEX3Ywe0JQA9vHyzY47TU=":{"shares":"1"}}}},"keymanager":{},"scheduler":{"params":{"min_validators":1,"max_validators":100,"max_validators_per_entity":1,"debug_bypass_stake":false,"debug_static_validators":false,"reward_factor_epoch_election_any":"0"}},"beacon":{"params":{"debug_deterministic":false}},"consensus":{"backend":"tendermint","params":{"timeout_commit":1000000000,"skip_timeout_commit":false,"empty_block_interval":0,"max_tx_size":32768,"max_block_size":22020096,"max_block_gas":0,"max_evidence_age":100000}},"halt_epoch":18446744073709551615,"extra_data":null} \ No newline at end of file