From 19c6d6851ae0f78463e8af24531cf78677b4638c Mon Sep 17 00:00:00 2001 From: Pau Date: Tue, 10 Oct 2023 16:29:43 +0200 Subject: [PATCH] vochain: order transactions when creating new blocks (#1121) refactor Vocdoni's Interaction with CometBFT 1. Implement transaction ordering during block creation. 2. Ensure blocks do not store invalid transactions. 3. Introduce mempool TTL mechanism and max attempts protection. 4. Implement Optimistic Execution for validators. 5. Resolve an app hash bug during block construction. **Transaction Ordering** - Transactions are sorted by nonce for the same sender during the "Prepare proposal" phase of CometBFT callbacks. **Block Transaction Validation** - A temporary state is constructed to execute proposal transactions, leading to hard-checks against this state. This eliminates the need for the previous soft-check approach. - `ProcessProposal` will return REJECT if any transaction in the block proposal is invalid. **Mempool Enhancements** - Transactions will be removed from the mempool after a set duration (TTL). - After three unsuccessful attempts to include a transaction in a block, it will be removed from the mempool. **Optimistic Execution** - If a block is executed during `ProcessProposal` by a validator and is accepted, its state is retained and not rolled back. - During `FinalizeBlock`, the system checks if the block was previously executed. If it was, there's no need for re-execution, potentially speeding up block production. **Block Construction Bug** - An issue was identified where the app hash reported by `FinalizeBlock` was incorrectly computed. - Due to the new CometBFT callback structure, block execution and hash return are separated from saving to persistent storage. - The previous `State.Save()` was incompatible with this two-step process. It has been modified to now include `PrepareCommit()` and `Commit()` functions. --- Signed-off-by: p4u --- apiclient/account.go | 10 +- cmd/end2endtest/account.go | 65 ++- cmd/end2endtest/helpers.go | 4 + cmd/node/main.go | 57 ++- dockerfiles/testsuite/env.gateway0 | 2 +- dockerfiles/testsuite/env.seed | 4 +- dockerfiles/testsuite/genesis.json | 14 +- log/log.go | 11 +- statedb/treeupdate.go | 31 +- test/testcommon/api.go | 3 +- tree/arbo/tree.go | 29 +- tree/arbo/tree_test.go | 86 ++++ vochain/account_test.go | 53 +-- vochain/app.go | 372 ++++------------ vochain/app_benchmark_test.go | 4 +- vochain/apptest.go | 6 +- vochain/apputils.go | 6 + vochain/cometbft.go | 472 +++++++++++++++++++++ vochain/genesis/genesis.go | 8 +- vochain/hysteresis_test.go | 78 ++-- vochain/indexer/indexer_test.go | 1 + vochain/ist/ist_test.go | 2 + vochain/process_test.go | 9 +- vochain/proof_erc20_test.go | 2 +- vochain/proof_minime_test.go | 2 +- vochain/proof_test.go | 4 +- vochain/proposal_test.go | 106 +++++ vochain/start.go | 14 +- vochain/state/sik.go | 40 +- vochain/state/sik_test.go | 3 + vochain/state/state.go | 35 +- vochain/state/state_snapshot_test.go | 2 + vochain/state/state_test.go | 32 +- vochain/transaction/account_tx.go | 14 - vochain/transaction/election_tx.go | 8 - vochain/transaction/nonce.go | 88 ++++ vochain/transaction/tokens_tx.go | 10 - vochain/transaction/transaction.go | 7 +- vochain/transaction/vochaintx/vochaintx.go | 9 +- vochain/transaction_zk_test.go | 3 + vochain/vochaininfo/vochaininfo.go | 2 +- vocone/vocone.go | 44 +- 42 files changed, 1202 insertions(+), 550 deletions(-) create mode 100644 vochain/cometbft.go create mode 100644 vochain/proposal_test.go create mode 100644 vochain/transaction/nonce.go diff --git a/apiclient/account.go b/apiclient/account.go index 0a42aeee5..900d77508 100644 --- a/apiclient/account.go +++ b/apiclient/account.go @@ -69,18 +69,26 @@ func (c *HTTPclient) Account(address string) (*api.Account, error) { } // Transfer sends tokens from the account associated with the client to the given address. +// The nonce is automatically calculated from the account information. // Returns the transaction hash. func (c *HTTPclient) Transfer(to common.Address, amount uint64) (types.HexBytes, error) { acc, err := c.Account("") if err != nil { return nil, err } + return c.TransferWithNonce(to, amount, acc.Nonce) +} + +// TransferWithNonce sends tokens from the account associated with the client to the given address. +// Returns the transaction hash. +func (c *HTTPclient) TransferWithNonce(to common.Address, amount uint64, nonce uint32) (types.HexBytes, error) { + var err error stx := models.SignedTx{} stx.Tx, err = proto.Marshal(&models.Tx{ Payload: &models.Tx_SendTokens{ SendTokens: &models.SendTokensTx{ Txtype: models.TxType_SET_ACCOUNT_INFO_URI, - Nonce: acc.Nonce, + Nonce: nonce, From: c.account.Address().Bytes(), To: to.Bytes(), Value: amount, diff --git a/cmd/end2endtest/account.go b/cmd/end2endtest/account.go index f3e379933..929a46977 100644 --- a/cmd/end2endtest/account.go +++ b/cmd/end2endtest/account.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "strings" + "sync" "time" "github.com/ethereum/go-ethereum/common" @@ -150,6 +151,8 @@ func testSendTokens(api *apiclient.HTTPclient, aliceKeys, bobKeys *ethereum.Sign // both pay 2 for each tx // resulting in balance 52 for alice // and 44 for bob + // In addition, we send a couple of token txs to burn address to increase the nonce, + // without waiting for them to be mined (this tests that the mempool transactions are properly ordered). txCost, err := api.TransactionCost(models.TxType_SEND_TOKENS) if err != nil { @@ -181,23 +184,47 @@ func testSendTokens(api *apiclient.HTTPclient, aliceKeys, bobKeys *ethereum.Sign // try to send tokens at the same time: // alice sends 1/4 of her balance to bob // sends 1/3 of his balance to alice - amountAtoB := aliceAcc.Balance / 4 - amountBtoA := bobAcc.Balance / 3 - - txhasha, err := alice.Transfer(bobKeys.Address(), amountAtoB) - if err != nil { - return fmt.Errorf("cannot send tokens: %v", err) - } - log.Infof("alice sent %d tokens to bob", amountAtoB) - log.Debugf("tx hash is %x", txhasha) + // Subtract 1 + txCost from each since we are sending an extra tx to increase the nonce to the burn address + amountAtoB := (aliceAcc.Balance) / 4 + amountBtoA := (bobAcc.Balance) / 3 + + // send a couple of token txs to increase the nonce, without waiting for them to be mined + // this tests that the mempool transactions are properly ordered. + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + log.Warnf("send transactions with nonce+1, should not be mined before the others") + // send 1 token to burn address with nonce + 1 (should be mined after the other txs) + if _, err = alice.TransferWithNonce(state.BurnAddress, 1, aliceAcc.Nonce+1); err != nil { + log.Fatalf("cannot burn tokens: %v", err) + } + if _, err = bob.TransferWithNonce(state.BurnAddress, 1, bobAcc.Nonce+1); err != nil { + log.Fatalf("cannot burn tokens: %v", err) + } + wg.Done() + }() + log.Warnf("waiting 6 seconds to let the burn txs be sent") + time.Sleep(6 * time.Second) + var txhasha, txhashb []byte + wg.Add(1) + go func() { + txhasha, err = alice.TransferWithNonce(bobKeys.Address(), amountAtoB, aliceAcc.Nonce) + if err != nil { + log.Fatalf("cannot send tokens: %v", err) + } + log.Infof("alice sent %d tokens to bob", amountAtoB) + log.Debugf("tx hash is %x", txhasha) - txhashb, err := bob.Transfer(aliceKeys.Address(), amountBtoA) - if err != nil { - return fmt.Errorf("cannot send tokens: %v", err) - } - log.Infof("bob sent %d tokens to alice", amountBtoA) - log.Debugf("tx hash is %x", txhashb) + txhashb, err = bob.TransferWithNonce(aliceKeys.Address(), amountBtoA, bobAcc.Nonce) + if err != nil { + log.Fatalf("cannot send tokens: %v", err) + } + log.Infof("bob sent %d tokens to alice", amountBtoA) + log.Debugf("tx hash is %x", txhashb) + wg.Done() + }() + wg.Wait() ctx, cancel := context.WithTimeout(context.Background(), time.Second*40) defer cancel() txrefa, err := api.WaitUntilTxIsMined(ctx, txhasha) @@ -216,12 +243,12 @@ func testSendTokens(api *apiclient.HTTPclient, aliceKeys, bobKeys *ethereum.Sign _ = api.WaitUntilNextBlock() // now check the resulting state - if err := checkAccountNonceAndBalance(alice, aliceAcc.Nonce+1, - aliceAcc.Balance-amountAtoB-txCost+amountBtoA); err != nil { + if err := checkAccountNonceAndBalance(alice, aliceAcc.Nonce+2, + aliceAcc.Balance-amountAtoB-(2*txCost+1)+amountBtoA); err != nil { return err } - if err := checkAccountNonceAndBalance(bob, bobAcc.Nonce+1, - bobAcc.Balance-amountBtoA-txCost+amountAtoB); err != nil { + if err := checkAccountNonceAndBalance(bob, bobAcc.Nonce+2, + bobAcc.Balance-amountBtoA-(2*txCost+1)+amountAtoB); err != nil { return err } diff --git a/cmd/end2endtest/helpers.go b/cmd/end2endtest/helpers.go index 0dd7d954f..d97ca225b 100644 --- a/cmd/end2endtest/helpers.go +++ b/cmd/end2endtest/helpers.go @@ -186,6 +186,10 @@ func (t *e2eElection) generateProofs(csp *ethereum.SignKeys, voterAccts []*ether wg sync.WaitGroup vcount int32 ) + // Wait for the next block to assure the SIK root is updated + if err := t.api.WaitUntilNextBlock(); err != nil { + return err + } errorChan := make(chan error) t.voters = new(sync.Map) addNaccounts := func(accounts []*ethereum.SignKeys) { diff --git a/cmd/node/main.go b/cmd/node/main.go index 6ee6267b5..f6aa45be2 100644 --- a/cmd/node/main.go +++ b/cmd/node/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/hex" "fmt" "io" "net" @@ -174,13 +175,29 @@ func newConfig() (*config.Config, config.Error) { viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // set FlagVars first - viper.BindPFlag("dataDir", flag.Lookup("dataDir")) + if err = viper.BindPFlag("dataDir", flag.Lookup("dataDir")); err != nil { + log.Fatalf("failed to bind dataDir flag to viper: %v", err) + } globalCfg.DataDir = viper.GetString("dataDir") - viper.BindPFlag("chain", flag.Lookup("chain")) + + if err = viper.BindPFlag("chain", flag.Lookup("chain")); err != nil { + log.Fatalf("failed to bind chain flag to viper: %v", err) + } globalCfg.Vochain.Chain = viper.GetString("chain") - viper.BindPFlag("dev", flag.Lookup("dev")) + + if err = viper.BindPFlag("dev", flag.Lookup("dev")); err != nil { + log.Fatalf("failed to bind dev flag to viper: %v", err) + } globalCfg.Dev = viper.GetBool("dev") - viper.BindPFlag("pprofPort", flag.Lookup("pprof")) + + if err = viper.BindPFlag("pprofPort", flag.Lookup("pprof")); err != nil { + log.Fatalf("failed to bind pprof flag to viper: %v", err) + } + + if err = viper.BindPFlag("dbType", flag.Lookup("dbType")); err != nil { + log.Fatalf("failed to bind dbType flag to viper: %v", err) + } + globalCfg.Vochain.DBType = viper.GetString("dbType") // use different datadirs for different chains globalCfg.DataDir = filepath.Join(globalCfg.DataDir, globalCfg.Vochain.Chain) @@ -267,7 +284,7 @@ func newConfig() (*config.Config, config.Error) { } } - if len(globalCfg.SigningKey) < 32 { + if globalCfg.SigningKey == "" { fmt.Println("no signing key, generating one...") signer := ethereum.NewSignKeys() err = signer.Generate() @@ -283,6 +300,10 @@ func newConfig() (*config.Config, config.Error) { globalCfg.SaveConfig = true } + if globalCfg.Vochain.MinerKey == "" { + globalCfg.Vochain.MinerKey = globalCfg.SigningKey + } + if globalCfg.SaveConfig { viper.Set("saveConfig", false) if err := viper.WriteConfig(); err != nil { @@ -381,7 +402,8 @@ func main() { log.Error(http.Serve(ln, nil)) }() } - log.Infow("starting vocdoni node", "version", internal.Version, "mode", globalCfg.Mode) + log.Infow("starting vocdoni node", "version", internal.Version, "mode", globalCfg.Mode, + "chain", globalCfg.Vochain.Chain, "dbType", globalCfg.Vochain.DBType) if globalCfg.Dev { log.Warn("developer mode is enabled!") } @@ -497,9 +519,7 @@ func main() { if err != nil { log.Fatal(err) } - if validator == nil { - log.Warnw("node is not a validator", "address", signer.Address().Hex()) - } else { + if validator != nil { // start keykeeper service (if key index specified) if validator.KeyIndex > 0 { srv.KeyKeeper, err = keykeeper.NewKeyKeeper( @@ -511,12 +531,10 @@ func main() { log.Fatal(err) } go srv.KeyKeeper.RevealUnpublished() - } else { - log.Warnw("validator keyIndex disabled") + log.Infow("configured keykeeper validator", + "address", signer.Address().Hex(), + "keyIndex", validator.KeyIndex) } - log.Infow("configured vochain validator", - "address", signer.Address().Hex(), - "keyIndex", validator.KeyIndex) } } @@ -571,6 +589,17 @@ func main() { signal.Notify(c, os.Interrupt, syscall.SIGTERM) <-c log.Warnf("received SIGTERM, exiting at %s", time.Now().Format(time.RFC850)) + height, err := srv.App.State.LastHeight() + if err != nil { + log.Warn(err) + } + hash, err := srv.App.State.MainTreeView().Root() + if err != nil { + log.Warn(err) + } + tmBlock := srv.App.GetBlockByHeight(int64(height)) + log.Infow("last block", "height", height, "appHash", hex.EncodeToString(hash), + "time", tmBlock.Time, "tmAppHash", tmBlock.AppHash.String(), "tmHeight", tmBlock.Height) os.Exit(0) } diff --git a/dockerfiles/testsuite/env.gateway0 b/dockerfiles/testsuite/env.gateway0 index 7d4d833e9..82862cbfd 100755 --- a/dockerfiles/testsuite/env.gateway0 +++ b/dockerfiles/testsuite/env.gateway0 @@ -10,4 +10,4 @@ VOCDONI_VOCHAIN_NOWAITSYNC=True VOCDONI_METRICS_ENABLED=True VOCDONI_METRICS_REFRESHINTERVAL=5 VOCDONI_CHAIN=dev -VOCDONI_SIGNINGKEY=e0f1412b86d6ca9f2b318f1d243ef50be23d315a2e6c1c3035bc72d44c8b2f90 +VOCDONI_SIGNINGKEY=e0f1412b86d6ca9f2b318f1d243ef50be23d315a2e6c1c3035bc72d44c8b2f90 # 0x88a499cEf9D1330111b41360173967c9C1bf703f diff --git a/dockerfiles/testsuite/env.seed b/dockerfiles/testsuite/env.seed index 464489495..2573ade20 100755 --- a/dockerfiles/testsuite/env.seed +++ b/dockerfiles/testsuite/env.seed @@ -1,9 +1,9 @@ VOCDONI_DATADIR=/app/run VOCDONI_MODE=seed -VOCDONI_LOGLEVEL=info +VOCDONI_LOGLEVEL=debug VOCDONI_DEV=True VOCDONI_VOCHAIN_PUBLICADDR=seed:26656 -VOCDONI_VOCHAIN_LOGLEVEL=debug +VOCDONI_VOCHAIN_LOGLEVEL=info VOCDONI_VOCHAIN_GENESIS=/app/misc/genesis.json VOCDONI_VOCHAIN_NODEKEY=0x2060e20d1f0894d6b23901bce3f20f26107baf0335451ad75ef27b14e4fc56ae050a65ae3883c379b70d811d6e12db2fe1e3a5cf0cae4d03dbbbfebc68601bdd VOCDONI_METRICS_ENABLED=True diff --git a/dockerfiles/testsuite/genesis.json b/dockerfiles/testsuite/genesis.json index 98d395039..cee349070 100755 --- a/dockerfiles/testsuite/genesis.json +++ b/dockerfiles/testsuite/genesis.json @@ -57,20 +57,8 @@ ], "accounts":[ { - "address":"0xccEc2c2D658261Fbdc40b04FEc06d49057242D39", - "balance":10000000 - }, - { - "address":"0x776d858D17C8018F07899dB535866EBf805a32E0", - "balance":10000000 - }, - { - "address":"0x074fcAacb8B01850539eaE7E9fEE8dc94549db96", - "balance":10000000 - }, - { "address":"0x88a499cEf9D1330111b41360173967c9C1bf703f", - "balance":10000000 + "balance":1000000000000 } ], "treasurer": "0xfe10DAB06D636647f4E40dFd56599da9eF66Db1c", diff --git a/log/log.go b/log/log.go index 552673917..a4c5fc020 100644 --- a/log/log.go +++ b/log/log.go @@ -7,6 +7,7 @@ import ( "os" "path" "runtime/debug" + "strings" "time" "github.com/rs/zerolog" @@ -89,11 +90,9 @@ func (*invalidCharChecker) Write(p []byte) (int, error) { return len(p), nil } -// Init initializes the logger. Output can be either "stdout/stderr/". -// Log level can be "debug/info/warn/error". -// errorOutput is an optional filename which only receives Warning and Error messages. func Init(level, output string, errorOutput io.Writer) { var out io.Writer + outputs := []io.Writer{} switch output { case "stdout": out = os.Stdout @@ -107,12 +106,16 @@ func Init(level, output string, errorOutput io.Writer) { panic(fmt.Sprintf("cannot create log output: %v", err)) } out = f + if strings.HasSuffix(output, ".json") { + outputs = append(outputs, f) + out = os.Stdout + } } out = zerolog.ConsoleWriter{ Out: out, TimeFormat: time.RFC3339Nano, } - outputs := []io.Writer{out} + outputs = append(outputs, out) if errorOutput != nil { outputs = append(outputs, &errorLevelWriter{zerolog.ConsoleWriter{ diff --git a/statedb/treeupdate.go b/statedb/treeupdate.go index 21878e291..ea5d6e0a3 100644 --- a/statedb/treeupdate.go +++ b/statedb/treeupdate.go @@ -150,14 +150,6 @@ func (u *TreeUpdate) SubTree(cfg TreeConfig) (treeUpdate *TreeUpdate, err error) if treeUpdate, ok := u.openSubs.Load(cfg.prefix); ok { return treeUpdate.(*TreeUpdate), nil } - parentLeaf, err := u.tree.Get(u.tree.tx, cfg.parentLeafKey) - if err != nil { - return nil, err - } - root, err := cfg.parentLeafGetRoot(parentLeaf) - if err != nil { - return nil, err - } tx := subWriteTx(u.tx, path.Join(subKeySubTree, cfg.prefix)) txTree := subWriteTx(tx, subKeyTree) tree, err := tree.New(txTree, @@ -165,18 +157,6 @@ func (u *TreeUpdate) SubTree(cfg TreeConfig) (treeUpdate *TreeUpdate, err error) if err != nil { return nil, err } - lastRoot, err := tree.Root(txTree) - if err != nil { - return nil, err - } - if !bytes.Equal(root, lastRoot) { - panic(fmt.Sprintf("root for %s mismatch: %x != %x", cfg.kindID, root, lastRoot)) - // Note (Pau): since we modified arbo to remove all unecessary intermediate nodes, - // we cannot set a past root.We should probably remove this code. - //if err := tree.SetRoot(txTree, root); err != nil { - // return nil, err - //} - } treeUpdate = &TreeUpdate{ tx: tx, tree: treeWithTx{ @@ -331,6 +311,15 @@ func propagateRoot(treeUpdate *TreeUpdate) ([]byte, error) { // version numbers, but overwriting an existing version can be useful in some // cases (for example, overwriting version 0 to setup a genesis state). func (t *TreeTx) Commit(version uint32) error { + if err := t.CommitOnTx(version); err != nil { + return err + } + return t.tx.Commit() +} + +// CommitOnTx do as Commit but without committing the transaction to database. +// After CommitOnTx(), caller should call SaveWithoutCommit() to commit the transaction. +func (t *TreeTx) CommitOnTx(version uint32) error { root, err := propagateRoot(&t.TreeUpdate) if err != nil { return fmt.Errorf("could not propagate root: %w", err) @@ -350,7 +339,7 @@ func (t *TreeTx) Commit(version uint32) error { if err := setVersionRoot(t.tx, version, root); err != nil { return fmt.Errorf("could not set version root: %w", err) } - return t.tx.Commit() + return nil } // Discard all the changes that have been made from the TreeTx. After calling diff --git a/test/testcommon/api.go b/test/testcommon/api.go index 443cfbccc..5e0d872bd 100644 --- a/test/testcommon/api.go +++ b/test/testcommon/api.go @@ -1,7 +1,6 @@ package testcommon import ( - "context" "net/url" "testing" @@ -60,7 +59,7 @@ func (d *APIserver) Start(t testing.TB, apis ...string) { // create and add balance for the pre-created Account err = d.VochainAPP.State.CreateAccount(d.Account.Address(), "", nil, 100000) qt.Assert(t, err, qt.IsNil) - d.VochainAPP.Commit(context.TODO(), nil) + d.VochainAPP.CommitState() // create vochain info (we do not start since it is not required) d.VochainInfo = vochaininfo.NewVochainInfo(d.VochainAPP) diff --git a/tree/arbo/tree.go b/tree/arbo/tree.go index bc07c430e..0fd9354da 100644 --- a/tree/arbo/tree.go +++ b/tree/arbo/tree.go @@ -53,8 +53,8 @@ var ( // in disk. DefaultThresholdNLeafs = 65536 - dbKeyRoot = []byte("root") - dbKeyNLeafs = []byte("nleafs") + dbKeyRoot = []byte("arbo/root/") + dbKeyNLeafs = []byte("arbo/nleafs/") emptyValue = []byte{0} // ErrKeyNotFound is used when a key is not found in the db neither in @@ -178,10 +178,11 @@ func (t *Tree) RootWithTx(rTx db.Reader) ([]byte, error) { return t.snapshotRoot, nil } // get db root - return rTx.Get(dbKeyRoot) + hash, err := rTx.Get(dbKeyRoot) + return hash, err } -func (*Tree) setRoot(wTx db.WriteTx, root []byte) error { +func (t *Tree) setRoot(wTx db.WriteTx, root []byte) error { return wTx.Set(dbKeyRoot, root) } @@ -325,7 +326,6 @@ func (t *Tree) Update(k, v []byte) error { func (t *Tree) UpdateWithTx(wTx db.WriteTx, k, v []byte) error { t.Lock() defer t.Unlock() - if !t.editable() { return ErrSnapshotNotEditable } @@ -484,16 +484,16 @@ func (t *Tree) deleteWithTx(wTx db.WriteTx, k []byte) error { return err } - // Update the root of the tree. - if err := t.setRoot(wTx, newRoot); err != nil { - return err - } - // Delete the orphan intermediate nodes. if err := deleteNodes(wTx, intermediates); err != nil { return fmt.Errorf("error deleting orphan intermediate nodes: %v", err) } + // Update the root of the tree. + if err := t.setRoot(wTx, newRoot); err != nil { + return err + } + // Delete the neighbour's childs and add them back to the tree in the right place. for i, k := range neighbourKeys { if err := t.deleteWithTx(wTx, k); err != nil { @@ -521,6 +521,9 @@ func (t *Tree) Get(k []byte) ([]byte, []byte, error) { // ErrKeyNotFound, and in the leafK & leafV parameters will be placed the data // found in the tree in the leaf that was on the path going to the input key. func (t *Tree) GetWithTx(rTx db.Reader, k []byte) ([]byte, []byte, error) { + t.Lock() + defer t.Unlock() + keyPath, err := keyPathFromKey(t.maxLevels, k) if err != nil { return nil, nil, err @@ -602,6 +605,8 @@ func (t *Tree) SetRoot(root []byte) error { // SetRootWithTx sets the root to the given root using the given db.WriteTx func (t *Tree) SetRootWithTx(wTx db.WriteTx, root []byte) error { + t.Lock() + defer t.Unlock() if !t.editable() { return ErrSnapshotNotEditable } @@ -620,6 +625,8 @@ func (t *Tree) SetRootWithTx(wTx db.WriteTx, root []byte) error { // The provided root must be a valid existing intermediate node in the tree. // The list of roots for a level can be obtained using tree.RootsFromLevel(). func (t *Tree) Snapshot(fromRoot []byte) (*Tree, error) { + t.Lock() + defer t.Unlock() // allow to define which root to use if fromRoot == nil { var err error @@ -672,6 +679,8 @@ func (t *Tree) IterateWithTx(rTx db.Reader, fromRoot []byte, f func([]byte, []by return err } } + t.Lock() + defer t.Unlock() return t.iter(rTx, fromRoot, f) } diff --git a/tree/arbo/tree_test.go b/tree/arbo/tree_test.go index 1e32dca30..1ec2882ad 100644 --- a/tree/arbo/tree_test.go +++ b/tree/arbo/tree_test.go @@ -242,6 +242,92 @@ func TestUpdate(t *testing.T) { c.Check(gettedValue, qt.DeepEquals, BigIntToBytes(bLen, big.NewInt(11))) } +func TestRootOnTx(t *testing.T) { + c := qt.New(t) + database := metadb.NewTest(t) + tree, err := NewTree(Config{Database: database, MaxLevels: 256, HashFunction: HashFunctionBlake2b}) + c.Assert(err, qt.IsNil) + + bLen := 32 + k := BigIntToBytes(bLen, big.NewInt(int64(20))) + v := BigIntToBytes(bLen, big.NewInt(int64(12))) + if err := tree.Add(k, v); err != nil { + t.Fatal(err) + } + rootInitial, err := tree.Root() + c.Assert(err, qt.IsNil) + + // Start a new transaction + tx := database.WriteTx() + + // Update the value of the key + v = BigIntToBytes(bLen, big.NewInt(int64(11))) + err = tree.UpdateWithTx(tx, k, v) + c.Assert(err, qt.IsNil) + + // Check that the root has not been updated yet + rootBefore, err := tree.Root() + c.Assert(err, qt.IsNil) + c.Assert(rootBefore, qt.DeepEquals, rootInitial) + + newRoot, err := tree.RootWithTx(tx) + c.Assert(err, qt.IsNil) + c.Assert(newRoot, qt.Not(qt.DeepEquals), rootBefore) + + // Commit the tx + c.Assert(tx.Commit(), qt.IsNil) + tx = database.WriteTx() + + // Check the new root equals the one returned by Root() + rootAfter, err := tree.Root() + c.Assert(err, qt.IsNil) + c.Assert(rootAfter, qt.DeepEquals, newRoot) + + // Add a new key-value pair + k2 := BigIntToBytes(bLen, big.NewInt(int64(30))) + v2 := BigIntToBytes(bLen, big.NewInt(int64(40))) + if err := tree.AddWithTx(tx, k2, v2); err != nil { + t.Fatal(err) + } + + // Check that the tx root has been updated + newRoot, err = tree.RootWithTx(tx) + c.Assert(err, qt.IsNil) + c.Assert(rootAfter, qt.Not(qt.DeepEquals), newRoot) + + c.Assert(tx.Commit(), qt.IsNil) + tx = database.WriteTx() + + // Check that the root has been updated after commit + rootInitial, err = tree.Root() + c.Assert(err, qt.IsNil) + c.Assert(rootInitial, qt.DeepEquals, newRoot) + + // Delete a key-value pair + if err := tree.DeleteWithTx(tx, k); err != nil { + t.Fatal(err) + } + + // Check that the root has not been updated yet + rootBefore, err = tree.Root() + c.Assert(err, qt.IsNil) + c.Assert(rootBefore, qt.DeepEquals, rootInitial) + + // Check that the tx root is different from the tree root + newRoot, err = tree.RootWithTx(tx) + c.Assert(err, qt.IsNil) + c.Assert(newRoot, qt.Not(qt.DeepEquals), rootBefore) + + // Commit the transaction + err = tx.Commit() + c.Assert(err, qt.IsNil) + + // Check that the root has been updated + rootAfter, err = tree.Root() + c.Assert(err, qt.IsNil) + c.Assert(rootAfter, qt.DeepEquals, newRoot) +} + func TestAux(t *testing.T) { // TODO split in proper tests c := qt.New(t) database := metadb.NewTest(t) diff --git a/vochain/account_test.go b/vochain/account_test.go index 5146966a7..b84cb96b9 100644 --- a/vochain/account_test.go +++ b/vochain/account_test.go @@ -26,6 +26,15 @@ const ( "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5" ) +func testCommitState(t *testing.T, app *BaseApplication) { + if _, err := app.State.PrepareCommit(); err != nil { + t.Fatal(err) + } + if _, err := app.CommitState(); err != nil { + t.Fatal(err) + } +} + func setupTestBaseApplicationAndSigners(t *testing.T, numberSigners int) (*BaseApplication, []*ethereum.SignKeys, error) { app := TestBaseApplication(t) @@ -58,8 +67,8 @@ func setupTestBaseApplicationAndSigners(t *testing.T, return nil, nil, err } // save state - _, err := app.Commit(context.TODO(), nil) - return app, signers, err + testCommitState(t, app) + return app, signers, nil } func TestSetAccountTx(t *testing.T) { @@ -71,7 +80,7 @@ func TestSetAccountTx(t *testing.T) { Account: models.Account{Balance: 10000, InfoURI: infoURI}}), qt.IsNil, ) - app.Commit(context.TODO(), nil) + testCommitState(t, app) // CREATE ACCOUNT @@ -331,7 +340,7 @@ func TestSetAccountTx(t *testing.T) { // should ignore tx cost if tx cost is set to 0 qt.Assert(t, app.State.SetTxBaseCost(models.TxType_CREATE_ACCOUNT, 0), qt.IsNil) - app.Commit(context.TODO(), nil) + testCommitState(t, app) faucetPkg, err = GenerateFaucetPackage(signers[0], signers[7].Address(), 10) qt.Assert(t, err, qt.IsNil) qt.Assert(t, testSetAccountTx(t, @@ -373,7 +382,7 @@ func TestSetAccountTx(t *testing.T) { // should not work if tx cost is not 0 and no faucet package is provided qt.Assert(t, app.State.SetTxBaseCost(models.TxType_CREATE_ACCOUNT, 1), qt.IsNil) - app.Commit(context.TODO(), nil) + testCommitState(t, app) qt.Assert(t, testSetAccountTx(t, signers[9], common.Address{}, nil, app, infoURI, 0, true), qt.IsNotNil, @@ -527,7 +536,7 @@ func testSetAccountTx(t *testing.T, if err := sendTx(app, signer, stx); err != nil { return err } - app.Commit(context.TODO(), nil) + testCommitState(t, app) return nil } @@ -585,7 +594,7 @@ func testSetTransactionCostsTx(t *testing.T, if err := sendTx(app, signer, stx); err != nil { return err } - app.Commit(context.TODO(), nil) + testCommitState(t, app) return nil } @@ -606,7 +615,7 @@ func TestMintTokensTx(t *testing.T) { toAccAddr := common.HexToAddress(randomEthAccount) err = app.State.CreateAccount(toAccAddr, "ipfs://", [][]byte{}, 0) qt.Assert(t, err, qt.IsNil) - app.Commit(context.TODO(), nil) + testCommitState(t, app) // should mint if err := testMintTokensTx(t, &signer, app, toAccAddr, 100, 0); err != nil { @@ -626,10 +635,6 @@ func TestMintTokensTx(t *testing.T) { if err := testMintTokensTx(t, ¬Treasurer, app, toAccAddr, 100, 1); err == nil { t.Fatal(err) } - // should fail minting if invalid nonce - if err := testMintTokensTx(t, &signer, app, toAccAddr, 100, rand.Uint32()); err == nil { - t.Fatal(err) - } // get account toAcc, err := app.State.GetAccount(toAccAddr, false) @@ -675,7 +680,7 @@ func testMintTokensTx(t *testing.T, if err := sendTx(app, signer, stx); err != nil { return err } - app.Commit(context.TODO(), nil) + testCommitState(t, app) return nil } @@ -706,7 +711,7 @@ func TestSendTokensTx(t *testing.T) { Amount: 1000, }) qt.Assert(t, err, qt.IsNil) - app.Commit(context.TODO(), nil) + testCommitState(t, app) // should send err = testSendTokensTx(t, &signer, app, toAccAddr, 100, 0) @@ -761,8 +766,8 @@ func testSendTokensTx(t *testing.T, if err := sendTx(app, signer, stx); err != nil { return err } - _, err = app.Commit(context.TODO(), nil) - return err + testCommitState(t, app) + return nil } func TestSetAccountDelegateTx(t *testing.T) { @@ -804,8 +809,8 @@ func TestSetAccountDelegateTx(t *testing.T) { Amount: 1000, }) qt.Assert(t, err, qt.IsNil) - _, err = app.Commit(context.TODO(), nil) - qt.Assert(t, err, qt.IsNil) + + testCommitState(t, app) // should add delegate if owner err = testSetAccountDelegateTx(t, &signer, app, toAccAddr, true, 0) @@ -869,8 +874,8 @@ func testSetAccountDelegateTx(t *testing.T, if err := sendTx(app, signer, stx); err != nil { return err } - _, err = app.Commit(context.TODO(), nil) - return err + testCommitState(t, app) + return nil } func TestCollectFaucetTx(t *testing.T) { @@ -902,8 +907,8 @@ func TestCollectFaucetTx(t *testing.T) { Amount: 1000, }) qt.Assert(t, err, qt.IsNil) - _, err = app.Commit(context.TODO(), nil) - qt.Assert(t, err, qt.IsNil) + + testCommitState(t, app) randomIdentifier := uint64(util.RandomInt(0, 10000000)) // should work if all data and tx are valid @@ -1005,8 +1010,8 @@ func testCollectFaucetTx(t *testing.T, if err := sendTx(app, to, stx); err != nil { return err } - _, err = app.Commit(context.TODO(), nil) - return err + testCommitState(t, app) + return nil } // sendTx signs and sends a vochain transaction diff --git a/vochain/app.go b/vochain/app.go index e86f4a7dc..dd1b2dbc8 100644 --- a/vochain/app.go +++ b/vochain/app.go @@ -1,17 +1,14 @@ package vochain import ( - "context" "encoding/hex" - "encoding/json" - "errors" "fmt" "path/filepath" + "sync" "sync/atomic" "time" abcitypes "github.com/cometbft/cometbft/abci/types" - crypto256k1 "github.com/cometbft/cometbft/crypto/secp256k1" tmnode "github.com/cometbft/cometbft/node" tmcli "github.com/cometbft/cometbft/rpc/client/local" ctypes "github.com/cometbft/cometbft/rpc/core/types" @@ -20,7 +17,6 @@ import ( lru "github.com/hashicorp/golang-lru/v2" "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/test/testcommon/testutil" - "go.vocdoni.io/dvote/vochain/genesis" "go.vocdoni.io/dvote/vochain/ist" vstate "go.vocdoni.io/dvote/vochain/state" "go.vocdoni.io/dvote/vochain/transaction" @@ -33,7 +29,13 @@ import ( const ( // recheckTxHeightInterval is the number of blocks after which the mempool is // checked for transactions to be rechecked. - recheckTxHeightInterval = 12 + recheckTxHeightInterval = 6 * 5 // 5 minutes + // transactionBlocksTTL is the number of blocks after which a transaction is + // removed from the mempool. + transactionBlocksTTL = 6 * 10 // 10 minutes + // maxPendingTxAttempts is the number of times a transaction can be included in a block + // and fail before being removed from the mempool. + maxPendingTxAttempts = 3 ) var ( @@ -49,6 +51,7 @@ type BaseApplication struct { Istc *ist.Controller Node *tmnode.Node NodeClient *tmcli.Local + NodeAddress ethcommon.Address TransactionHandler *transaction.TransactionHandler isSynchronizingFn func() bool // tendermint WaitSync() function is racy, we need to use a mutex in order to avoid @@ -64,6 +67,9 @@ type BaseApplication struct { fnMempoolSize func() int fnMempoolPrune func(txKey [32]byte) error blockCache *lru.Cache[int64, *tmtypes.Block] + // txTTLReferences is a map of tx hashes to the block height where they failed. + txReferences sync.Map + // endBlockTimestamp is the last block end timestamp calculated from local time. endBlockTimestamp atomic.Int64 // startBlockTimestamp is the current block timestamp from tendermint's @@ -74,10 +80,28 @@ type BaseApplication struct { dataDir string genesisInfo *tmtypes.GenesisDoc + // lastDeliverTxResponse is used to store the last DeliverTxResponse, so validators + // can skip block re-execution on FinalizeBlock call. + lastDeliverTxResponse []*DeliverTxResponse + // lastRootHash is used to store the last root hash of the current on-going state, + // it is used by validators to skip block re-execution on FinalizeBlock call. + lastRootHash []byte + // lastBlockHash stores the last cometBFT block hash + lastBlockHash []byte + // prepareProposalLock is used to avoid concurrent calls between Prepare/Process Proposal and FinalizeBlock + prepareProposalLock sync.Mutex + // testMockBlockStore is used for testing purposes only testMockBlockStore *testutil.MockBlockStore } +// pendingTxReference is used to store the block height where the transaction was accepted by the mempool, and the number +// of times it has been included in a block but failed. +type pendingTxReference struct { + height uint32 + failedCount int +} + // DeliverTxResponse is the response returned by DeliverTx after executing the transaction. type DeliverTxResponse struct { Code uint32 @@ -86,6 +110,14 @@ type DeliverTxResponse struct { Data []byte } +// ExecuteBlockResponse is the response returned by ExecuteBlock after executing the block. +// If InvalidTransactions is true, it means that at least one transaction in the block was invalid. +type ExecuteBlockResponse struct { + Responses []*DeliverTxResponse + Root []byte + InvalidTransactions [][32]byte +} + // NewBaseApplication creates a new BaseApplication given a name and a DB backend. // Node still needs to be initialized with SetNode. // Callback functions still need to be initialized. @@ -124,175 +156,15 @@ func NewBaseApplication(dbType, dbpath string) (*BaseApplication, error) { }, nil } -// Info Return information about the application state. -// Used to sync Tendermint with the application during a handshake that happens on startup. -// The returned AppVersion will be included in the Header of every block. -// Tendermint expects LastBlockAppHash and LastBlockHeight to be updated during Commit, -// ensuring that Commit is never called twice for the same block height. -// -// We use this method to initialize some state variables. -func (app *BaseApplication) Info(_ context.Context, - req *abcitypes.RequestInfo) (*abcitypes.ResponseInfo, error) { - lastHeight, err := app.State.LastHeight() - if err != nil { - return nil, fmt.Errorf("cannot get State.LastHeight: %w", err) - } - app.State.SetHeight(lastHeight) - appHash := app.State.WorkingHash() - if err := app.State.SetElectionPriceCalc(); err != nil { - return nil, fmt.Errorf("cannot set election price calc: %w", err) - } - // print some basic version info about tendermint components - log.Infow("cometbft info", "cometVersion", req.Version, "p2pVersion", - req.P2PVersion, "blockVersion", req.BlockVersion, "lastHeight", - lastHeight, "appHash", hex.EncodeToString(appHash)) - - return &abcitypes.ResponseInfo{ - LastBlockHeight: int64(lastHeight), - LastBlockAppHash: appHash, - }, nil -} - -// InitChain called once upon genesis -// ResponseInitChain can return a list of validators. If the list is empty, -// Tendermint will use the validators loaded in the genesis file. -func (app *BaseApplication) InitChain(_ context.Context, - req *abcitypes.RequestInitChain) (*abcitypes.ResponseInitChain, error) { - // setting the app initial state with validators, height = 0 and empty apphash - // unmarshal app state from genesis - var genesisAppState genesis.AppState - err := json.Unmarshal(req.AppStateBytes, &genesisAppState) - if err != nil { - return nil, fmt.Errorf("cannot unmarshal app state bytes: %w", err) - } - // create accounts - for _, acc := range genesisAppState.Accounts { - addr := ethcommon.BytesToAddress(acc.Address) - if err := app.State.CreateAccount(addr, "", nil, acc.Balance); err != nil { - if err != vstate.ErrAccountAlreadyExists { - return nil, fmt.Errorf("cannot create acount %x: %w", addr, err) - } - if err := app.State.InitChainMintBalance(addr, acc.Balance); err != nil { - return nil, fmt.Errorf("cannot initialize chain minintg balance: %w", err) - } - } - log.Infow("created account", "addr", addr.Hex(), "tokens", acc.Balance) - } - // get validators - // TODO pau: unify this code with the one on apputils.go that essentially does the same - tendermintValidators := []abcitypes.ValidatorUpdate{} - for i := 0; i < len(genesisAppState.Validators); i++ { - log.Infow("add genesis validator", - "signingAddress", genesisAppState.Validators[i].Address.String(), - "consensusPubKey", genesisAppState.Validators[i].PubKey.String(), - "power", genesisAppState.Validators[i].Power, - "name", genesisAppState.Validators[i].Name, - "keyIndex", genesisAppState.Validators[i].KeyIndex, - ) - - v := &models.Validator{ - Address: genesisAppState.Validators[i].Address, - PubKey: genesisAppState.Validators[i].PubKey, - Power: genesisAppState.Validators[i].Power, - KeyIndex: uint32(genesisAppState.Validators[i].KeyIndex), - } - if err = app.State.AddValidator(v); err != nil { - return nil, fmt.Errorf("cannot add validator %s: %w", log.FormatProto(v), err) - } - tendermintValidators = append(tendermintValidators, - abcitypes.UpdateValidator( - genesisAppState.Validators[i].PubKey, - int64(genesisAppState.Validators[i].Power), - crypto256k1.KeyType, - )) - } - - // set treasurer address - if genesisAppState.Treasurer != nil { - log.Infof("adding genesis treasurer %x", genesisAppState.Treasurer) - if err := app.State.SetTreasurer(ethcommon.BytesToAddress(genesisAppState.Treasurer), 0); err != nil { - return nil, fmt.Errorf("could not set State.Treasurer from genesis file: %w", err) - } - } - - // add tx costs - for k, v := range genesisAppState.TxCost.AsMap() { - err = app.State.SetTxBaseCost(k, v) - if err != nil { - return nil, fmt.Errorf("could not set tx cost %q to value %q from genesis file to the State", k, v) - } - } - - // create burn account - if err := app.State.SetAccount(vstate.BurnAddress, &vstate.Account{}); err != nil { - return nil, fmt.Errorf("unable to set burn address") - } - - // set max election size - if err := app.State.SetMaxProcessSize(genesisAppState.MaxElectionSize); err != nil { - return nil, fmt.Errorf("unable to set max election size") - } - - // set network capacity - if err := app.State.SetNetworkCapacity(genesisAppState.NetworkCapacity); err != nil { - return nil, fmt.Errorf("unable to set network capacity") - } - - // initialize election price calc - if err := app.State.SetElectionPriceCalc(); err != nil { - return nil, fmt.Errorf("cannot set election price calc: %w", err) - } - - // commit state and get hash - hash, err := app.State.Save() - if err != nil { - return nil, fmt.Errorf("cannot save state: %w", err) - } - return &abcitypes.ResponseInitChain{ - Validators: tendermintValidators, - AppHash: hash, - }, nil -} - -// CheckTx unmarshals req.Tx and checks its validity -func (app *BaseApplication) CheckTx(_ context.Context, - req *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) { - if req.Type == abcitypes.CheckTxType_Recheck { - if app.Height()%recheckTxHeightInterval != 0 { - return &abcitypes.ResponseCheckTx{Code: 0}, nil - } - } - tx := new(vochaintx.Tx) - if err := tx.Unmarshal(req.Tx, app.ChainID()); err != nil { - return &abcitypes.ResponseCheckTx{Code: 1, Data: []byte("unmarshalTx " + err.Error())}, err - } - response, err := app.TransactionHandler.CheckTx(tx, false) - if err != nil { - if errors.Is(err, transaction.ErrorAlreadyExistInCache) { - return &abcitypes.ResponseCheckTx{Code: 0}, nil - } - log.Errorw(err, "checkTx") - return &abcitypes.ResponseCheckTx{Code: 1, Data: []byte("checkTx " + err.Error())}, err - } - return &abcitypes.ResponseCheckTx{ - Code: 0, - Data: response.Data, - Info: fmt.Sprintf("%x", response.TxHash), - Log: response.Log, - }, nil -} - -// FinalizeBlock It delivers a decided block to the Application. The Application must execute -// the transactions in the block deterministically and update its state accordingly. -// Cryptographic commitments to the block and transaction results, returned via the corresponding -// parameters in ResponseFinalizeBlock, are included in the header of the next block. -// CometBFT calls it when a new block is decided. -func (app *BaseApplication) FinalizeBlock(_ context.Context, - req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) { - height := uint32(req.GetHeight()) - app.beginBlock(req.GetTime(), height) - txResults := make([]*abcitypes.ExecTxResult, len(req.Txs)) - for i, tx := range req.Txs { +// ExecuteBlock delivers a block of transactions to the Application. +// It modifies the state according to the transactions and returns the resulting Merkle root hash. +// It returns a list of ResponseDeliverTx, one for each transaction in the block. +// This call rollbacks the current state. +func (app *BaseApplication) ExecuteBlock(txs [][]byte, height uint32, blockTime time.Time) (*ExecuteBlockResponse, error) { + result := []*DeliverTxResponse{} + app.beginBlock(blockTime, height) + invalidTxs := [][32]byte{} + for _, tx := range txs { resp := app.deliverTx(tx) if resp.Code != 0 { log.Warnw("deliverTx failed", @@ -300,28 +172,34 @@ func (app *BaseApplication) FinalizeBlock(_ context.Context, "data", string(resp.Data), "info", resp.Info, "log", resp.Log) + invalidTxs = append(invalidTxs, [32]byte{}) } - txResults[i] = &abcitypes.ExecTxResult{ - Code: resp.Code, - Data: resp.Data, - Log: resp.Log, - } + result = append(result, resp) } // execute internal state transition commit if err := app.Istc.Commit(height); err != nil { return nil, fmt.Errorf("cannot execute ISTC commit: %w", err) } - app.endBlock(req.GetTime(), height) - return &abcitypes.ResponseFinalizeBlock{ - AppHash: app.State.WorkingHash(), - TxResults: txResults, // TODO: check if we can remove this + app.endBlock(blockTime, height) + root, err := app.State.PrepareCommit() + if err != nil { + return nil, fmt.Errorf("cannot prepare commit: %w", err) + } + return &ExecuteBlockResponse{ + Responses: result, + Root: root, + InvalidTransactions: invalidTxs, }, nil } -// Commit saves the current vochain state and returns a commit hash -func (app *BaseApplication) Commit(_ context.Context, _ *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) { - // save state - _, err := app.State.Save() +// CommitState saves the state to persistent storage and returns the hash. +// Before save the state, app.State.PrepareCommit() should be called. +func (app *BaseApplication) CommitState() ([]byte, error) { + // Commit the state and get the hash + if app.State.TxCounter() > 0 { + log.Infow("commit block", "height", app.Height(), "txs", app.State.TxCounter()) + } + hash, err := app.State.Save() if err != nil { return nil, fmt.Errorf("cannot save state: %w", err) } @@ -330,17 +208,12 @@ func (app *BaseApplication) Commit(_ context.Context, _ *abcitypes.RequestCommit startTime := time.Now() log.Infof("performing a state snapshot on block %d", app.Height()) if _, err := app.State.Snapshot(); err != nil { - return nil, fmt.Errorf("cannot make state snapshot: %w", err) + return hash, fmt.Errorf("cannot make state snapshot: %w", err) } log.Infof("snapshot created successfully, took %s", time.Since(startTime)) log.Debugf("%+v", app.State.ListSnapshots()) } - if app.State.TxCounter() > 0 { - log.Infow("commit block", "height", app.Height(), "txs", app.State.TxCounter()) - } - return &abcitypes.ResponseCommit{ - RetainHeight: 0, // When snapshot sync enabled, we can start to remove old blocks - }, nil + return hash, err } // deliverTx unmarshals req.Tx and adds it to the State if it is valid @@ -363,6 +236,7 @@ func (app *BaseApplication) deliverTx(rawTx []byte) *DeliverTxResponse { log.Errorw(err, "rejected tx") return &DeliverTxResponse{Code: 1, Data: []byte(err.Error())} } + app.txReferences.Delete(tx.TxID) // call event listeners for _, e := range app.State.EventListeners() { e.OnNewTx(tx, app.Height(), app.State.TxCounter()) @@ -388,18 +262,14 @@ func (app *BaseApplication) beginBlock(t time.Time, height uint32) { app.startBlockTimestamp.Store(t.Unix()) app.State.SetHeight(height) go app.State.CachePurge(height) - if err := app.State.FetchValidSIKRoots(); err != nil { - log.Errorw(err, "error fetching valid SIK roots") - } app.State.OnBeginBlock(vstate.BeginBlock{ Height: int64(height), Time: t, - // TODO: remove data hash from this event call }) } // endBlock is called at the end of every block. -func (app *BaseApplication) endBlock(t time.Time, _ uint32) { +func (app *BaseApplication) endBlock(t time.Time, h uint32) { app.endBlockTimestamp.Store(t.Unix()) } @@ -457,98 +327,6 @@ func (app *BaseApplication) SendTx(tx []byte) (*ctypes.ResultBroadcastTx, error) return app.fnSendTx(tx) } -// Query does nothing -func (*BaseApplication) Query(_ context.Context, - _ *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) { - return &abcitypes.ResponseQuery{}, nil -} - -// PrepareProposal allows the block proposer to perform application-dependent work in a block before -// proposing it. This enables, for instance, batch optimizations to a block, which has been empirically -// demonstrated to be a key component for improved performance. Method PrepareProposal is called every -// time CometBFT is about to broadcast a Proposal message and validValue is nil. CometBFT gathers -// outstanding transactions from the mempool, generates a block header, and uses them to create a block -// to propose. Then, it calls RequestPrepareProposal with the newly created proposal, called raw proposal. -// The Application can make changes to the raw proposal, such as modifying the set of transactions or the -// order in which they appear, and returns the (potentially) modified proposal, called prepared proposal in -// the ResponsePrepareProposal call. The logic modifying the raw proposal MAY be non-deterministic. -func (app *BaseApplication) PrepareProposal(ctx context.Context, - req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) { - // TODO: Prepare Proposal should check the validity of the transactions for the next block. - // Currently they are executed by CheckTx, but it does not allow height to be passed in. - validTxs := [][]byte{} - for _, tx := range req.GetTxs() { - resp, err := app.CheckTx(ctx, &abcitypes.RequestCheckTx{ - Tx: tx, Type: abcitypes.CheckTxType_New, - }) - if err != nil || resp.Code != 0 { - log.Warnw("discard invalid tx on prepare proposal", - "err", err, - "code", resp.Code, - "data", string(resp.Data), - "info", resp.Info, - "log", resp.Log) - continue - } - validTxs = append(validTxs, tx) - } - - return &abcitypes.ResponsePrepareProposal{ - Txs: validTxs, - }, nil -} - -// ProcessProposal allows a validator to perform application-dependent work in a proposed block. This enables -// features such as immediate block execution, and allows the Application to reject invalid blocks. -// CometBFT calls it when it receives a proposal and validValue is nil. The Application cannot modify the -// proposal at this point but can reject it if invalid. If that is the case, the consensus algorithm will -// prevote nil on the proposal, which has strong liveness implications for CometBFT. As a general rule, the -// Application SHOULD accept a prepared proposal passed via ProcessProposal, even if a part of the proposal -// is invalid (e.g., an invalid transaction); the Application can ignore the invalid part of the prepared -// proposal at block execution time. The logic in ProcessProposal MUST be deterministic. -func (*BaseApplication) ProcessProposal(_ context.Context, - _ *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) { - return &abcitypes.ResponseProcessProposal{ - Status: abcitypes.ResponseProcessProposal_ACCEPT, - }, nil -} - -// ListSnapshots returns a list of available snapshots. -func (*BaseApplication) ListSnapshots(context.Context, - *abcitypes.RequestListSnapshots) (*abcitypes.ResponseListSnapshots, error) { - return &abcitypes.ResponseListSnapshots{}, nil -} - -// OfferSnapshot returns the response to a snapshot offer. -func (*BaseApplication) OfferSnapshot(context.Context, - *abcitypes.RequestOfferSnapshot) (*abcitypes.ResponseOfferSnapshot, error) { - return &abcitypes.ResponseOfferSnapshot{}, nil -} - -// LoadSnapshotChunk returns the response to a snapshot chunk loading request. -func (*BaseApplication) LoadSnapshotChunk(context.Context, - *abcitypes.RequestLoadSnapshotChunk) (*abcitypes.ResponseLoadSnapshotChunk, error) { - return &abcitypes.ResponseLoadSnapshotChunk{}, nil -} - -// ApplySnapshotChunk returns the response to a snapshot chunk applying request. -func (*BaseApplication) ApplySnapshotChunk(context.Context, - *abcitypes.RequestApplySnapshotChunk) (*abcitypes.ResponseApplySnapshotChunk, error) { - return &abcitypes.ResponseApplySnapshotChunk{}, nil -} - -// ExtendVote creates application specific vote extension -func (*BaseApplication) ExtendVote(context.Context, - *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) { - return &abcitypes.ResponseExtendVote{}, nil -} - -// VerifyVoteExtension verifies application's vote extension data -func (*BaseApplication) VerifyVoteExtension(context.Context, - *abcitypes.RequestVerifyVoteExtension) (*abcitypes.ResponseVerifyVoteExtension, error) { - return &abcitypes.ResponseVerifyVoteExtension{}, nil -} - // ChainID returns the Node ChainID func (app *BaseApplication) ChainID() string { return app.chainID @@ -560,6 +338,16 @@ func (app *BaseApplication) SetChainID(chainID string) { app.State.SetChainID(chainID) } +// MempoolDeleteTx removes a transaction from the mempool. If the mempool implementation does not allow it, +// its a no-op function. Errors are logged but not returned. +func (app *BaseApplication) MempoolDeleteTx(txID [32]byte) { + if app.fnMempoolPrune != nil { + if err := app.fnMempoolPrune(txID); err != nil { + log.Warnw("could not remove mempool tx", "txID", hex.EncodeToString(txID[:]), "err", err) + } + } +} + // Genesis returns the tendermint genesis information func (app *BaseApplication) Genesis() *tmtypes.GenesisDoc { return app.genesisInfo diff --git a/vochain/app_benchmark_test.go b/vochain/app_benchmark_test.go index fb787f96e..1cfe78fdb 100644 --- a/vochain/app_benchmark_test.go +++ b/vochain/app_benchmark_test.go @@ -135,13 +135,13 @@ func benchCheckTx(b *testing.B, app *BaseApplication, voters []*models.SignedTx) } i++ if i%100 == 0 { - _, err = app.Commit(context.TODO(), nil) + _, err = app.CommitState() if err != nil { b.Fatal(err) } } } - _, err = app.Commit(context.TODO(), nil) + _, err = app.CommitState() if err != nil { b.Fatal(err) } diff --git a/vochain/apptest.go b/vochain/apptest.go index e42d56bc9..3d09376b1 100644 --- a/vochain/apptest.go +++ b/vochain/apptest.go @@ -105,7 +105,11 @@ func (app *BaseApplication) AdvanceTestBlock() { // finalize block app.endBlock(time.Now(), height) // save the state - _, err := app.Commit(context.TODO(), nil) + _, err := app.State.PrepareCommit() + if err != nil { + panic(err) + } + _, err = app.CommitState() if err != nil { panic(err) } diff --git a/vochain/apputils.go b/vochain/apputils.go index 8d59bef8c..3a0e4b6f3 100644 --- a/vochain/apputils.go +++ b/vochain/apputils.go @@ -16,6 +16,7 @@ import ( "go.vocdoni.io/proto/build/go/models" "google.golang.org/protobuf/proto" + "github.com/cometbft/cometbft/crypto" crypto25519 "github.com/cometbft/cometbft/crypto/ed25519" crypto256k1 "github.com/cometbft/cometbft/crypto/secp256k1" tmp2p "github.com/cometbft/cometbft/p2p" @@ -63,6 +64,11 @@ func NewNodeKey(tmPrivKey, nodeKeyFilePath string) (*tmp2p.NodeKey, error) { return nodeKey, nodeKey.SaveAs(nodeKeyFilePath) } +// NodeKeyToAddress returns the ethereum address of the given cometBFT node key +func NodePvKeyToAddress(pubk crypto.PubKey) (ethcommon.Address, error) { + return ethereum.AddrFromPublicKey(pubk.Bytes()) +} + // GenerateFaucetPackage generates a faucet package. // The package is signed by the given `from` key (holder of the funds) and sent to the `to` address. // The `amount` is the amount of tokens to be sent. diff --git a/vochain/cometbft.go b/vochain/cometbft.go new file mode 100644 index 000000000..9341fb6c5 --- /dev/null +++ b/vochain/cometbft.go @@ -0,0 +1,472 @@ +package vochain + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "sort" + "time" + + abcitypes "github.com/cometbft/cometbft/abci/types" + crypto256k1 "github.com/cometbft/cometbft/crypto/secp256k1" + ethcommon "github.com/ethereum/go-ethereum/common" + "go.vocdoni.io/dvote/log" + "go.vocdoni.io/dvote/vochain/genesis" + vstate "go.vocdoni.io/dvote/vochain/state" + "go.vocdoni.io/dvote/vochain/transaction" + "go.vocdoni.io/dvote/vochain/transaction/vochaintx" + "go.vocdoni.io/proto/build/go/models" +) + +// Info Return information about the application state. +// Used to sync Tendermint with the application during a handshake that happens on startup. +// The returned AppVersion will be included in the Header of every block. +// Tendermint expects LastBlockAppHash and LastBlockHeight to be updated during Commit, +// ensuring that Commit is never called twice for the same block height. +// +// We use this method to initialize some state variables. +func (app *BaseApplication) Info(_ context.Context, + req *abcitypes.RequestInfo) (*abcitypes.ResponseInfo, error) { + app.isSynchronizing.Store(true) + lastHeight, err := app.State.LastHeight() + if err != nil { + return nil, fmt.Errorf("cannot get State.LastHeight: %w", err) + } + app.State.SetHeight(lastHeight) + appHash := app.State.CommittedHash() + if err := app.State.SetElectionPriceCalc(); err != nil { + return nil, fmt.Errorf("cannot set election price calc: %w", err) + } + // print some basic version info about tendermint components + log.Infow("cometbft info", "cometVersion", req.Version, "p2pVersion", + req.P2PVersion, "blockVersion", req.BlockVersion, "lastHeight", + lastHeight, "appHash", hex.EncodeToString(appHash)) + + return &abcitypes.ResponseInfo{ + LastBlockHeight: int64(lastHeight), + LastBlockAppHash: appHash, + }, nil +} + +// InitChain called once upon genesis +// ResponseInitChain can return a list of validators. If the list is empty, +// Tendermint will use the validators loaded in the genesis file. +func (app *BaseApplication) InitChain(_ context.Context, + req *abcitypes.RequestInitChain) (*abcitypes.ResponseInitChain, error) { + // setting the app initial state with validators, height = 0 and empty apphash + // unmarshal app state from genesis + var genesisAppState genesis.AppState + err := json.Unmarshal(req.AppStateBytes, &genesisAppState) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal app state bytes: %w", err) + } + // create accounts + for _, acc := range genesisAppState.Accounts { + addr := ethcommon.BytesToAddress(acc.Address) + if err := app.State.CreateAccount(addr, "", nil, acc.Balance); err != nil { + if err != vstate.ErrAccountAlreadyExists { + return nil, fmt.Errorf("cannot create acount %x: %w", addr, err) + } + if err := app.State.InitChainMintBalance(addr, acc.Balance); err != nil { + return nil, fmt.Errorf("cannot initialize chain minintg balance: %w", err) + } + } + log.Infow("created account", "addr", addr.Hex(), "tokens", acc.Balance) + } + // get validators + // TODO pau: unify this code with the one on apputils.go that essentially does the same + tendermintValidators := []abcitypes.ValidatorUpdate{} + for i := 0; i < len(genesisAppState.Validators); i++ { + log.Infow("add genesis validator", + "signingAddress", genesisAppState.Validators[i].Address.String(), + "consensusPubKey", genesisAppState.Validators[i].PubKey.String(), + "power", genesisAppState.Validators[i].Power, + "name", genesisAppState.Validators[i].Name, + "keyIndex", genesisAppState.Validators[i].KeyIndex, + ) + v := &models.Validator{ + Address: genesisAppState.Validators[i].Address, + PubKey: genesisAppState.Validators[i].PubKey, + Power: genesisAppState.Validators[i].Power, + KeyIndex: uint32(genesisAppState.Validators[i].KeyIndex), + } + if err = app.State.AddValidator(v); err != nil { + return nil, fmt.Errorf("cannot add validator %s: %w", log.FormatProto(v), err) + } + addr := ethcommon.BytesToAddress(v.Address) + if err := app.State.CreateAccount(addr, "", nil, 0); err != nil { + if err != vstate.ErrAccountAlreadyExists { + return nil, fmt.Errorf("cannot create validator acount %x: %w", addr, err) + } + } + tendermintValidators = append(tendermintValidators, + abcitypes.UpdateValidator( + genesisAppState.Validators[i].PubKey, + int64(genesisAppState.Validators[i].Power), + crypto256k1.KeyType, + )) + } + myValidator, err := app.State.Validator(ethcommon.Address(app.NodeAddress), false) + if err != nil { + return nil, fmt.Errorf("cannot get node validator: %w", err) + } + if myValidator != nil { + log.Infow("node is a validator!", "power", myValidator.Power, "name", myValidator.Name) + } + + // set treasurer address + if genesisAppState.Treasurer != nil { + log.Infof("adding genesis treasurer %x", genesisAppState.Treasurer) + if err := app.State.SetTreasurer(ethcommon.BytesToAddress(genesisAppState.Treasurer), 0); err != nil { + return nil, fmt.Errorf("could not set State.Treasurer from genesis file: %w", err) + } + } + + // add tx costs + for k, v := range genesisAppState.TxCost.AsMap() { + err = app.State.SetTxBaseCost(k, v) + if err != nil { + return nil, fmt.Errorf("could not set tx cost %q to value %q from genesis file to the State", k, v) + } + } + + // create burn account + if err := app.State.SetAccount(vstate.BurnAddress, &vstate.Account{}); err != nil { + return nil, fmt.Errorf("unable to set burn address") + } + + // set max election size + if err := app.State.SetMaxProcessSize(genesisAppState.MaxElectionSize); err != nil { + return nil, fmt.Errorf("unable to set max election size") + } + + // set network capacity + if err := app.State.SetNetworkCapacity(genesisAppState.NetworkCapacity); err != nil { + return nil, fmt.Errorf("unable to set network capacity") + } + + // initialize election price calc + if err := app.State.SetElectionPriceCalc(); err != nil { + return nil, fmt.Errorf("cannot set election price calc: %w", err) + } + + // commit state and get hash + hash, err := app.State.PrepareCommit() + if err != nil { + return nil, fmt.Errorf("cannot prepare commit: %w", err) + } + if _, err = app.State.Save(); err != nil { + return nil, fmt.Errorf("cannot save state: %w", err) + } + return &abcitypes.ResponseInitChain{ + Validators: tendermintValidators, + AppHash: hash, + }, nil +} + +// CheckTx unmarshals req.Tx and checks its validity +func (app *BaseApplication) CheckTx(_ context.Context, + req *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) { + txReference := vochaintx.TxKey(req.Tx) + ref, ok := app.txReferences.Load(txReference) + if !ok { + // store the initial height of the tx if its the first time we see it + app.txReferences.Store(txReference, &pendingTxReference{ + height: app.Height(), + }) + } else { + height := ref.(*pendingTxReference).height + // check if the tx is referenced by a previous block and the TTL has expired + if app.Height() > height+transactionBlocksTTL { + // remove tx reference and return checkTx error + log.Debugw("pruning expired tx from mempool", "height", app.Height(), "hash", fmt.Sprintf("%x", txReference)) + app.txReferences.Delete(txReference) + return &abcitypes.ResponseCheckTx{Code: 1, Data: []byte(fmt.Sprintf("tx expired %x", txReference))}, nil + } + } + // execute recheck mempool every recheckTxHeightInterval blocks + if req.Type == abcitypes.CheckTxType_Recheck { + if app.Height()%recheckTxHeightInterval != 0 { + return &abcitypes.ResponseCheckTx{Code: 0}, nil + } + } + // unmarshal tx and check it + tx := new(vochaintx.Tx) + if err := tx.Unmarshal(req.Tx, app.ChainID()); err != nil { + return &abcitypes.ResponseCheckTx{Code: 1, Data: []byte("unmarshalTx " + err.Error())}, err + } + response, err := app.TransactionHandler.CheckTx(tx, false) + if err != nil { + if errors.Is(err, transaction.ErrorAlreadyExistInCache) { + return &abcitypes.ResponseCheckTx{Code: 0}, nil + } + log.Errorw(err, "checkTx") + return &abcitypes.ResponseCheckTx{Code: 1, Data: []byte("checkTx " + err.Error())}, err + } + return &abcitypes.ResponseCheckTx{ + Code: 0, + Data: response.Data, + Info: fmt.Sprintf("%x", response.TxHash), + Log: response.Log, + }, nil +} + +// FinalizeBlock is executed by cometBFT when a new block is decided. +// Cryptographic commitments to the block and transaction results, returned via the corresponding +// parameters in ResponseFinalizeBlock, are included in the header of the next block. +func (app *BaseApplication) FinalizeBlock(_ context.Context, + req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) { + app.prepareProposalLock.Lock() + defer app.prepareProposalLock.Unlock() + start := time.Now() + height := uint32(req.GetHeight()) + var root []byte + var resp []*DeliverTxResponse + // skip execution if we already have the results and root (from ProcessProposal) + // or if the stored lastBlockHash is different from the one requested. + if app.lastRootHash == nil || !bytes.Equal(app.lastBlockHash, req.GetHash()) { + result, err := app.ExecuteBlock(req.Txs, height, req.GetTime()) + if err != nil { + return nil, fmt.Errorf("cannot execute block: %w", err) + } + root = result.Root + resp = result.Responses + } else { + root = make([]byte, len(app.lastRootHash)) + copy(root, app.lastRootHash[:]) + resp = app.lastDeliverTxResponse + } + + txResults := make([]*abcitypes.ExecTxResult, len(req.Txs)) + for i, tx := range resp { + txResults[i] = &abcitypes.ExecTxResult{ + Code: tx.Code, + Data: tx.Data, + Log: tx.Log, + Info: tx.Info, + } + } + log.Debugw("finalize block", "height", height, + "txs", len(req.Txs), "hash", hex.EncodeToString(root), + "blockSeconds", time.Since(req.GetTime()).Seconds(), + "elapsedSeconds", time.Since(start).Seconds(), + "proposer", hex.EncodeToString(req.GetProposerAddress())) + return &abcitypes.ResponseFinalizeBlock{ + AppHash: root, + TxResults: txResults, + }, nil +} + +// Commit is the CometBFT implementation of the ABCI Commit method. We currently do nothing here. +func (app *BaseApplication) Commit(_ context.Context, _ *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) { + app.prepareProposalLock.Lock() + defer app.prepareProposalLock.Unlock() + // save state and get hash + h, err := app.CommitState() + if err != nil { + return nil, err + } + log.Debugw("commit block", "height", app.Height(), "hash", hex.EncodeToString(h)) + return &abcitypes.ResponseCommit{ + RetainHeight: 0, // When snapshot sync enabled, we can start to remove old blocks + }, nil +} + +// PrepareProposal allows the block proposer to perform application-dependent work in a block before +// proposing it. This enables, for instance, batch optimizations to a block, which has been empirically +// demonstrated to be a key component for improved performance. Method PrepareProposal is called every +// time CometBFT is about to broadcast a Proposal message and validValue is nil. CometBFT gathers +// outstanding transactions from the mempool, generates a block header, and uses them to create a block +// to propose. Then, it calls RequestPrepareProposal with the newly created proposal, called raw proposal. +// The Application can make changes to the raw proposal, such as modifying the set of transactions or the +// order in which they appear, and returns the (potentially) modified proposal, called prepared proposal in +// the ResponsePrepareProposal call. The logic modifying the raw proposal MAY be non-deterministic. +func (app *BaseApplication) PrepareProposal(ctx context.Context, + req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) { + app.prepareProposalLock.Lock() + defer app.prepareProposalLock.Unlock() + startTime := time.Now() + + type txInfo struct { + Data []byte + Addr *ethcommon.Address + Nonce uint32 + DecodedTx *vochaintx.Tx + } + // Rollback the state to discard previous non saved changes. + // It might happen if ProcessProposal fails and then this node is selected for preparing the next proposal. + app.State.Rollback() + + // Get and execute transactions from the mempool + validTxInfos := []txInfo{} + for _, tx := range req.GetTxs() { + vtx := new(vochaintx.Tx) + if err := vtx.Unmarshal(tx, app.ChainID()); err != nil { + // invalid transaction + app.MempoolDeleteTx(vochaintx.TxKey(tx)) + log.Warnw("could not unmarshal transaction", "err", err) + continue + } + senderAddr, nonce, err := app.TransactionHandler.ExtractNonceAndSender(vtx) + if err != nil { + log.Warnw("could not extract nonce and/or sender from transaction", "err", err) + continue + } + + validTxInfos = append(validTxInfos, txInfo{ + Data: tx, + Addr: senderAddr, + Nonce: nonce, + DecodedTx: vtx, + }) + } + + // Sort the transactions based on the sender's address and nonce + sort.Slice(validTxInfos, func(i, j int) bool { + if validTxInfos[i].Addr == nil && validTxInfos[j].Addr != nil { + return true + } + if validTxInfos[i].Addr != nil && validTxInfos[j].Addr == nil { + return false + } + if validTxInfos[i].Addr != nil && validTxInfos[j].Addr != nil { + if bytes.Equal(validTxInfos[i].Addr.Bytes(), validTxInfos[j].Addr.Bytes()) { + return validTxInfos[i].Nonce < validTxInfos[j].Nonce + } + return bytes.Compare(validTxInfos[i].Addr.Bytes(), validTxInfos[j].Addr.Bytes()) == -1 + } + return false + }) + + // Check the validity of the transactions + validTxs := [][]byte{} + for _, txInfo := range validTxInfos { + // Check the validity of the transaction using forCommit true + resp, err := app.TransactionHandler.CheckTx(txInfo.DecodedTx, true) + if err != nil { + log.Warnw("discard invalid tx on prepare proposal", + "err", err, + "hash", fmt.Sprintf("%x", txInfo.DecodedTx.TxID), + "data", func() string { + if resp != nil { + return string(resp.Data) + } + return "" + }()) + // remove transaction from mempool if max attempts reached + val, ok := app.txReferences.Load(txInfo.DecodedTx.TxID) + if ok { + val.(*pendingTxReference).failedCount++ + if val.(*pendingTxReference).failedCount > maxPendingTxAttempts { + log.Debugf("transaction %x has reached max attempts, remove from mempool", txInfo.DecodedTx.TxID) + app.MempoolDeleteTx(txInfo.DecodedTx.TxID) + app.txReferences.Delete(txInfo.DecodedTx.TxID) + } + } + continue + } + validTxs = append(validTxs, txInfo.Data) + } + + // Rollback the state to discard the changes made + app.State.Rollback() + log.Debugw("prepare proposal", "height", app.Height(), "txs", len(validTxs), + "elapsedSeconds", time.Since(startTime).Seconds()) + return &abcitypes.ResponsePrepareProposal{ + Txs: validTxs, + }, nil +} + +// ProcessProposal allows a validator to perform application-dependent work in a proposed block. This enables +// features such as immediate block execution, and allows the Application to reject invalid blocks. +// CometBFT calls it when it receives a proposal and validValue is nil. The Application cannot modify the +// proposal at this point but can reject it if invalid. If that is the case, the consensus algorithm will +// prevote nil on the proposal, which has strong liveness implications for CometBFT. As a general rule, the +// Application SHOULD accept a prepared proposal passed via ProcessProposal, even if a part of the proposal +// is invalid (e.g., an invalid transaction); the Application can ignore the invalid part of the prepared +// proposal at block execution time. The logic in ProcessProposal MUST be deterministic. +func (app *BaseApplication) ProcessProposal(_ context.Context, + req *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) { + app.prepareProposalLock.Lock() + defer app.prepareProposalLock.Unlock() + // Check if the node is a validator, if not, just accept the proposal and return (nothing to say) + validator, err := app.State.Validator(app.NodeAddress, true) + if err != nil { + return nil, fmt.Errorf("cannot get node validator info: %w", err) + } + if validator == nil { + // we reset the last deliver tx response and root hash to avoid using them at finalize block + app.lastDeliverTxResponse = nil + app.lastRootHash = nil + app.lastBlockHash = nil + return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT}, nil + } + + startTime := time.Now() + resp, err := app.ExecuteBlock(req.Txs, uint32(req.GetHeight()), req.GetTime()) + if err != nil { + return nil, fmt.Errorf("cannot execute block on process proposal: %w", err) + } + // invalid txx on a proposed block, should actually never happened if proposer acts honestly + if len(resp.InvalidTransactions) > 0 { + log.Warnw("invalid transactions on process proposal", "height", app.Height(), "count", len(resp.InvalidTransactions), + "proposer", hex.EncodeToString(req.ProposerAddress), "action", "reject") + return &abcitypes.ResponseProcessProposal{ + Status: abcitypes.ResponseProcessProposal_REJECT, + }, nil + } + app.lastDeliverTxResponse = resp.Responses + app.lastRootHash = resp.Root + app.lastBlockHash = req.GetHash() + log.Debugw("process proposal", "height", app.Height(), "txs", len(req.Txs), "action", "accept", + "blockHash", hex.EncodeToString(app.lastBlockHash), + "hash", hex.EncodeToString(resp.Root), "elapsedSeconds", time.Since(startTime).Seconds()) + return &abcitypes.ResponseProcessProposal{ + Status: abcitypes.ResponseProcessProposal_ACCEPT, + }, nil +} + +// ListSnapshots returns a list of available snapshots. +func (*BaseApplication) ListSnapshots(context.Context, + *abcitypes.RequestListSnapshots) (*abcitypes.ResponseListSnapshots, error) { + return &abcitypes.ResponseListSnapshots{}, nil +} + +// OfferSnapshot returns the response to a snapshot offer. +func (*BaseApplication) OfferSnapshot(context.Context, + *abcitypes.RequestOfferSnapshot) (*abcitypes.ResponseOfferSnapshot, error) { + return &abcitypes.ResponseOfferSnapshot{}, nil +} + +// LoadSnapshotChunk returns the response to a snapshot chunk loading request. +func (*BaseApplication) LoadSnapshotChunk(context.Context, + *abcitypes.RequestLoadSnapshotChunk) (*abcitypes.ResponseLoadSnapshotChunk, error) { + return &abcitypes.ResponseLoadSnapshotChunk{}, nil +} + +// ApplySnapshotChunk returns the response to a snapshot chunk applying request. +func (*BaseApplication) ApplySnapshotChunk(context.Context, + *abcitypes.RequestApplySnapshotChunk) (*abcitypes.ResponseApplySnapshotChunk, error) { + return &abcitypes.ResponseApplySnapshotChunk{}, nil +} + +// Query does nothing +func (*BaseApplication) Query(_ context.Context, + _ *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) { + return &abcitypes.ResponseQuery{}, nil +} + +// ExtendVote creates application specific vote extension +func (*BaseApplication) ExtendVote(context.Context, + *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) { + return &abcitypes.ResponseExtendVote{}, nil +} + +// VerifyVoteExtension verifies application's vote extension data +func (*BaseApplication) VerifyVoteExtension(context.Context, + *abcitypes.RequestVerifyVoteExtension) (*abcitypes.ResponseVerifyVoteExtension, error) { + return &abcitypes.ResponseVerifyVoteExtension{}, nil +} diff --git a/vochain/genesis/genesis.go b/vochain/genesis/genesis.go index ea96f3603..20fb7abe1 100644 --- a/vochain/genesis/genesis.go +++ b/vochain/genesis/genesis.go @@ -39,8 +39,8 @@ var Genesis = map[string]Vochain{ } var devGenesis = Doc{ - GenesisTime: time.Date(2023, time.September, 21, 1, 0, 0, 0, time.UTC), - ChainID: "vocdoni-dev-20", + GenesisTime: time.Date(2023, time.October, 10, 1, 0, 0, 0, time.UTC), + ChainID: "vocdoni-dev-24", ConsensusParams: &ConsensusParams{ Block: BlockParams{ MaxBytes: 2097152, @@ -95,6 +95,10 @@ var devGenesis = Doc{ Address: types.HexStringToHexBytes("0xC7C6E17059801b6962cc144a374eCc3ba1b8A9e0"), Balance: 100000000, }, + { // faucet2 + Address: types.HexStringToHexBytes("0x536Da9ecd65Fc0248625b0BBDbB305d0DD841893"), + Balance: 100000000, + }, }, Treasurer: types.HexStringToHexBytes("0x309Bd6959bf4289CDf9c7198cF9f4494e0244b7d"), TxCost: TransactionCosts{ diff --git a/vochain/hysteresis_test.go b/vochain/hysteresis_test.go index 5ac6c7227..64e5b9cb3 100644 --- a/vochain/hysteresis_test.go +++ b/vochain/hysteresis_test.go @@ -3,6 +3,7 @@ package vochain import ( "encoding/json" "math/big" + "sync" "testing" qt "github.com/frankban/quicktest" @@ -28,7 +29,7 @@ func TestHysteresis(t *testing.T) { // initial accounts testWeight := big.NewInt(10) - accounts, censusRoot, proofs := testCreateKeysAndBuildWeightedZkCensus(t, 10, testWeight) + accounts, censusRoot, proofs := testCreateKeysAndBuildWeightedZkCensus(t, 3, testWeight) // add the test accounts siks to the test app for _, account := range accounts { @@ -64,38 +65,47 @@ func TestHysteresis(t *testing.T) { c.Assert(err, qt.IsNil) sikRoot, err := sikTree.Root() c.Assert(err, qt.IsNil) - for i, account := range accounts { - _, sikProof, err := sikTree.GenProof(account.Address().Bytes()) - c.Assert(err, qt.IsNil) - - sikSiblings, err := zk.ProofToCircomSiblings(sikProof) - c.Assert(err, qt.IsNil) - - censusSiblings, err := zk.ProofToCircomSiblings(proofs[i]) - c.Assert(err, qt.IsNil) - - // get zkproof - inputs, err := circuit.GenerateCircuitInput(circuit.CircuitInputsParameters{ - Account: account, - ElectionId: pid, - CensusRoot: censusRoot, - SIKRoot: sikRoot, - CensusSiblings: censusSiblings, - SIKSiblings: sikSiblings, - AvailableWeight: testWeight, - }) - c.Assert(err, qt.IsNil) - encInputs, err := json.Marshal(inputs) - c.Assert(err, qt.IsNil) - - zkProof, err := prover.Prove(devCircuit.ProvingKey, devCircuit.Wasm, encInputs) - c.Assert(err, qt.IsNil) - - protoZkProof, err := zk.ProverProofToProtobufZKProof(zkProof, nil, nil, nil, nil, nil) - c.Assert(err, qt.IsNil) - - zkProofs = append(zkProofs, protoZkProof) + wg := sync.WaitGroup{} + mtx := sync.Mutex{} + for i := range accounts { + wg.Add(1) + i := i + go func() { + _, sikProof, err := sikTree.GenProof(accounts[i].Address().Bytes()) + c.Assert(err, qt.IsNil) + + sikSiblings, err := zk.ProofToCircomSiblings(sikProof) + c.Assert(err, qt.IsNil) + + censusSiblings, err := zk.ProofToCircomSiblings(proofs[i]) + c.Assert(err, qt.IsNil) + + // get zkproof + inputs, err := circuit.GenerateCircuitInput(circuit.CircuitInputsParameters{ + Account: accounts[i], + ElectionId: pid, + CensusRoot: censusRoot, + SIKRoot: sikRoot, + CensusSiblings: censusSiblings, + SIKSiblings: sikSiblings, + AvailableWeight: testWeight, + }) + c.Assert(err, qt.IsNil) + encInputs, err := json.Marshal(inputs) + c.Assert(err, qt.IsNil) + + zkProof, err := prover.Prove(devCircuit.ProvingKey, devCircuit.Wasm, encInputs) + c.Assert(err, qt.IsNil) + + protoZkProof, err := zk.ProverProofToProtobufZKProof(zkProof, nil, nil, nil, nil, nil) + c.Assert(err, qt.IsNil) + mtx.Lock() + zkProofs = append(zkProofs, protoZkProof) + mtx.Unlock() + wg.Done() + }() } + wg.Wait() validVotes := len(accounts) / 2 for i, account := range accounts[:validVotes] { @@ -131,14 +141,14 @@ func TestHysteresis(t *testing.T) { } for i := 0; i < state.SIKROOT_HYSTERESIS_BLOCKS; i++ { + mockNewSIK() app.AdvanceTestBlock() } - mockNewSIK() for i := 0; i < state.SIKROOT_HYSTERESIS_BLOCKS; i++ { + mockNewSIK() app.AdvanceTestBlock() } - mockNewSIK() for i, account := range accounts[validVotes:] { nullifier, err := account.AccountSIKnullifier(pid, nil) diff --git a/vochain/indexer/indexer_test.go b/vochain/indexer/indexer_test.go index 0593d9ef0..0c9a4b252 100644 --- a/vochain/indexer/indexer_test.go +++ b/vochain/indexer/indexer_test.go @@ -981,6 +981,7 @@ func TestAfterSyncBootStrap(t *testing.T) { } // Save the current state with the 10 new votes + app.State.PrepareCommit() app.State.Save() // The results should not be up to date. diff --git a/vochain/ist/ist_test.go b/vochain/ist/ist_test.go index 8a57408c1..0c75a1eb3 100644 --- a/vochain/ist/ist_test.go +++ b/vochain/ist/ist_test.go @@ -155,6 +155,8 @@ func testAdvanceBlock(t *testing.T, s *state.State, istc *Controller) { height := s.CurrentHeight() err := istc.Commit(height) qt.Assert(t, err, qt.IsNil) + _, err = s.PrepareCommit() + qt.Assert(t, err, qt.IsNil) _, err = s.Save() qt.Assert(t, err, qt.IsNil) s.SetHeight(height + 1) diff --git a/vochain/process_test.go b/vochain/process_test.go index a1075bced..eab630a84 100644 --- a/vochain/process_test.go +++ b/vochain/process_test.go @@ -417,12 +417,11 @@ func createTestBaseApplicationAndAccounts(t *testing.T, qt.Assert(t, app.State.SetTxBaseCost(cost, txCostNumber), qt.IsNil) } - _, err := app.Commit(context.TODO(), nil) - qt.Assert(t, err, qt.IsNil) + testCommitState(t, app) return app, keys } -func testCheckTxDeliverTxCommit(_ *testing.T, app *BaseApplication, stx *models.SignedTx) error { +func testCheckTxDeliverTxCommit(t *testing.T, app *BaseApplication, stx *models.SignedTx) error { cktx := new(abcitypes.RequestCheckTx) var err error // checkTx() @@ -444,8 +443,8 @@ func testCheckTxDeliverTxCommit(_ *testing.T, app *BaseApplication, stx *models. return fmt.Errorf("deliverTx failed: %s", detxresp.Data) } // commit() - _, err = app.Commit(context.TODO(), nil) - return err + testCommitState(t, app) + return nil } func TestGlobalMaxProcessSize(t *testing.T) { diff --git a/vochain/proof_erc20_test.go b/vochain/proof_erc20_test.go index ba5e92e45..a1190c97c 100644 --- a/vochain/proof_erc20_test.go +++ b/vochain/proof_erc20_test.go @@ -138,7 +138,7 @@ func testEthSendVotes(t *testing.T, s testStorageProof, t.Fatalf("deliverTx success, but expected result is fail") } } - _, err = app.Commit(context.TODO(), nil) + _, err = app.CommitState() if err != nil { t.Fatal(err) } diff --git a/vochain/proof_minime_test.go b/vochain/proof_minime_test.go index c4a86168d..cb2425bfd 100644 --- a/vochain/proof_minime_test.go +++ b/vochain/proof_minime_test.go @@ -157,7 +157,7 @@ func testMinimeSendVotes(t *testing.T, s ethstorageproof.StorageProof, addr comm t.Fatalf("deliverTx uccess, but expected result is fail") } } - _, err = app.Commit(context.TODO(), nil) + _, err = app.CommitState() if err != nil { t.Fatal(err) } diff --git a/vochain/proof_test.go b/vochain/proof_test.go index b9c67cc63..5e5f6b9af 100644 --- a/vochain/proof_test.go +++ b/vochain/proof_test.go @@ -64,7 +64,7 @@ func TestMerkleTreeProof(t *testing.T) { qt.Assert(t, detxresp.Code, qt.Equals, uint32(0)) if i%5 == 0 { - _, err = app.Commit(context.TODO(), nil) + _, err = app.CommitState() qt.Assert(t, err, qt.IsNil) app.AdvanceTestBlock() } @@ -243,6 +243,6 @@ func testCSPsendVotes(t *testing.T, pid []byte, vp []byte, signer *ethereum.Sign t.Fatalf("deliverTx success, but expected result is fail") } } - _, err = app.Commit(context.TODO(), nil) + _, err = app.CommitState() qt.Assert(t, err, qt.IsNil) } diff --git a/vochain/proposal_test.go b/vochain/proposal_test.go new file mode 100644 index 000000000..ac67e08b8 --- /dev/null +++ b/vochain/proposal_test.go @@ -0,0 +1,106 @@ +package vochain + +import ( + "context" + "testing" + + abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/frankban/quicktest" + "go.vocdoni.io/dvote/crypto/ethereum" + vstate "go.vocdoni.io/dvote/vochain/state" + "go.vocdoni.io/dvote/vochain/transaction/vochaintx" + "go.vocdoni.io/proto/build/go/models" + "google.golang.org/protobuf/proto" +) + +// To test if the PrepareProposal method correctly sorts the transactions based on the sender's address and nonce +func TestTransactionsSorted(t *testing.T) { + qt := quicktest.New(t) + app := TestBaseApplication(t) + keys := ethereum.NewSignKeysBatch(50) + txs := [][]byte{} + // create the accounts + for i, key := range keys { + err := app.State.SetAccount(key.Address(), &vstate.Account{ + Account: models.Account{ + Balance: 500, + Nonce: uint32(i), + }, + }) + qt.Assert(err, quicktest.IsNil) + } + + // add first the transactions with nonce+1 + for i, key := range keys { + tx := models.Tx{ + Payload: &models.Tx_SendTokens{SendTokens: &models.SendTokensTx{ + Nonce: uint32(i), + From: key.Address().Bytes(), + To: keys[(i+1)%50].Address().Bytes(), + Value: 1, + }}} + txBytes, err := proto.Marshal(&tx) + qt.Assert(err, quicktest.IsNil) + + signature, err := key.SignVocdoniTx(txBytes, app.chainID) + qt.Assert(err, quicktest.IsNil) + + stx, err := proto.Marshal(&models.SignedTx{ + Tx: txBytes, + Signature: signature, + }) + qt.Assert(err, quicktest.IsNil) + + txs = append(txs, stx) + } + + // add the transactions with current once + for i, key := range keys { + tx := models.Tx{ + Payload: &models.Tx_SendTokens{SendTokens: &models.SendTokensTx{ + Nonce: uint32(i + 1), + From: key.Address().Bytes(), + To: keys[(i+1)%50].Address().Bytes(), + Value: 1, + }}} + txBytes, err := proto.Marshal(&tx) + qt.Assert(err, quicktest.IsNil) + + signature, err := key.SignVocdoniTx(txBytes, app.chainID) + qt.Assert(err, quicktest.IsNil) + + stx, err := proto.Marshal(&models.SignedTx{ + Tx: txBytes, + Signature: signature, + }) + qt.Assert(err, quicktest.IsNil) + + txs = append(txs, stx) + } + + req := &abcitypes.RequestPrepareProposal{ + Txs: txs, + } + + _, err := app.State.PrepareCommit() + qt.Assert(err, quicktest.IsNil) + _, err = app.CommitState() + qt.Assert(err, quicktest.IsNil) + + resp, err := app.PrepareProposal(context.Background(), req) + qt.Assert(err, quicktest.IsNil) + + txAddresses := make(map[string]uint32) + for _, tx := range resp.GetTxs() { + vtx := new(vochaintx.Tx) + err := vtx.Unmarshal(tx, app.chainID) + qt.Assert(err, quicktest.IsNil) + txSendTokens := vtx.Tx.GetSendTokens() + nonce, ok := txAddresses[string(txSendTokens.From)] + if ok && nonce >= txSendTokens.Nonce { + qt.Errorf("nonce is not sorted: %d, %d", nonce, txSendTokens.Nonce) + } + txAddresses[string(txSendTokens.From)] = txSendTokens.Nonce + } + qt.Assert(len(txs), quicktest.Equals, len(resp.Txs)) +} diff --git a/vochain/start.go b/vochain/start.go index 427d7feba..3a12ecf39 100644 --- a/vochain/start.go +++ b/vochain/start.go @@ -11,6 +11,7 @@ import ( "time" "go.vocdoni.io/dvote/config" + "go.vocdoni.io/dvote/crypto/ethereum" vocdoniGenesis "go.vocdoni.io/dvote/vochain/genesis" tmcfg "github.com/cometbft/cometbft/config" @@ -175,7 +176,7 @@ func newTendermint(app *BaseApplication, // mempool config tconfig.Mempool.Size = localConfig.MempoolSize tconfig.Mempool.Recheck = true - tconfig.Mempool.KeepInvalidTxsInCache = false + tconfig.Mempool.KeepInvalidTxsInCache = true tconfig.Mempool.MaxTxBytes = 1024 * 100 // 100 KiB tconfig.Mempool.MaxTxsBytes = int64(tconfig.Mempool.Size * tconfig.Mempool.MaxTxBytes) tconfig.Mempool.CacheSize = 100000 @@ -204,6 +205,17 @@ func newTendermint(app *BaseApplication, return nil, fmt.Errorf("cannot create validator key and state: (%v)", err) } pv.Save() + if pv.Key.PrivKey.Bytes() != nil { + fmt.Printf("privkey: %s\n", hex.EncodeToString(pv.Key.PrivKey.Bytes())) + signer := ethereum.SignKeys{} + if err := signer.AddHexKey(hex.EncodeToString(pv.Key.PrivKey.Bytes())); err != nil { + return nil, fmt.Errorf("cannot add private validator key: %w", err) + } + app.NodeAddress, err = NodePvKeyToAddress(pv.Key.PubKey) + if err != nil { + return nil, fmt.Errorf("cannot create node address from pubkey: %w", err) + } + } // nodekey is used for the p2p transport layer nodeKey := new(p2p.NodeKey) diff --git a/vochain/state/sik.go b/vochain/state/sik.go index 20bb091c5..194f6996d 100644 --- a/vochain/state/sik.go +++ b/vochain/state/sik.go @@ -82,7 +82,7 @@ func (v *State) SetAddressSIK(address common.Address, newSIK SIK) error { if err != nil { return fmt.Errorf("%w: %w", ErrSIKSet, err) } - return v.UpdateSIKRoots() + return nil } if err != nil { return fmt.Errorf("%w: %w", ErrSIKGet, err) @@ -101,7 +101,7 @@ func (v *State) SetAddressSIK(address common.Address, newSIK SIK) error { if err != nil { return fmt.Errorf("%w: %w", ErrSIKSet, err) } - return v.UpdateSIKRoots() + return nil } // InvalidateSIK function removes logically the registered SIK for the address @@ -126,7 +126,7 @@ func (v *State) InvalidateSIK(address common.Address) error { if err != nil { return fmt.Errorf("%w: %w", ErrSIKDelete, err) } - return v.UpdateSIKRoots() + return nil } // ValidSIKRoots method returns the current valid SIK roots that are cached in @@ -181,8 +181,28 @@ func (v *State) ExpiredSIKRoot(candidateRoot []byte) bool { func (v *State) UpdateSIKRoots() error { // instance the SIK's key-value DB and set the current block to the current // network height. + v.mtxValidSIKRoots.Lock() + defer v.mtxValidSIKRoots.Unlock() sikNoStateDB := v.NoState(false) currentBlock := v.CurrentHeight() + + // get sik roots key-value database associated to the siks tree + siksTree, err := v.tx.DeepSubTree(StateTreeCfg(TreeSIK)) + if err != nil { + return fmt.Errorf("%w: %w", ErrSIKSubTree, err) + } + // get new sik tree root hash + newSikRoot, err := siksTree.Root() + if err != nil { + return fmt.Errorf("%w: %w", ErrSIKRootsGet, err) + } + // check if the new sik root is already in the list of valid roots, if so return + for _, sikRoot := range v.validSIKRoots { + if bytes.Equal(sikRoot, newSikRoot) { + return nil + } + } + // purge the oldest sikRoots if the hysteresis is reached if currentBlock > SIKROOT_HYSTERESIS_BLOCKS { // calculate the current minimun block to purge useless sik roots minBlock := currentBlock - SIKROOT_HYSTERESIS_BLOCKS @@ -219,18 +239,6 @@ func (v *State) UpdateSIKRoots() error { "blockNumber", binary.LittleEndian.Uint32(blockToDelete)) } } - // get sik roots key-value database associated to the siks tree - v.tx.Lock() - defer v.tx.Unlock() - siksTree, err := v.tx.DeepSubTree(StateTreeCfg(TreeSIK)) - if err != nil { - return fmt.Errorf("%w: %w", ErrSIKSubTree, err) - } - // get new sik tree root hash - newSikRoot, err := siksTree.Root() - if err != nil { - return fmt.Errorf("%w: %w", ErrSIKRootsGet, err) - } // encode current blockNumber as key blockKey := make([]byte, 32) binary.LittleEndian.PutUint32(blockKey, currentBlock) @@ -240,9 +248,7 @@ func (v *State) UpdateSIKRoots() error { return fmt.Errorf("%w: %w", ErrSIKRootsSet, err) } // include the new root into the cached list - v.mtxValidSIKRoots.Lock() v.validSIKRoots = append(v.validSIKRoots, newSikRoot) - v.mtxValidSIKRoots.Unlock() log.Debugw("updateSIKRoots (created)", "newSikRoot", hex.EncodeToString(newSikRoot), "blockNumber", currentBlock) diff --git a/vochain/state/sik_test.go b/vochain/state/sik_test.go index 6cff2c78b..012a64d07 100644 --- a/vochain/state/sik_test.go +++ b/vochain/state/sik_test.go @@ -98,6 +98,7 @@ func Test_sikRoots(t *testing.T) { sik1, _ := hex.DecodeString("3a7806f4e0b5bda625d465abf5639ba42ac9b91bafea3b800a4a") s.SetHeight(1) c.Assert(s.SetAddressSIK(address1, sik1), qt.IsNil) + c.Assert(s.UpdateSIKRoots(), qt.IsNil) // check the results c.Assert(s.FetchValidSIKRoots(), qt.IsNil) validSIKs := s.ValidSIKRoots() @@ -113,6 +114,7 @@ func Test_sikRoots(t *testing.T) { sik2, _ := hex.DecodeString("5fb53c1f9b53fba0296f4e8306802d44235c1a11becc4e6853d0") s.SetHeight(33) c.Assert(s.SetAddressSIK(address2, sik2), qt.IsNil) + c.Assert(s.UpdateSIKRoots(), qt.IsNil) // check the results c.Assert(s.FetchValidSIKRoots(), qt.IsNil) validSIKs = s.ValidSIKRoots() @@ -127,6 +129,7 @@ func Test_sikRoots(t *testing.T) { sik3, _ := hex.DecodeString("7ccbc0da9e8d7e469ba60cd898a5b881c99a960c1e69990a3196") s.SetHeight(66) c.Assert(s.SetAddressSIK(address3, sik3), qt.IsNil) + c.Assert(s.UpdateSIKRoots(), qt.IsNil) // check the results c.Assert(s.FetchValidSIKRoots(), qt.IsNil) validSIKs = s.ValidSIKRoots() diff --git a/vochain/state/state.go b/vochain/state/state.go index 3cb98a019..ab99765c6 100644 --- a/vochain/state/state.go +++ b/vochain/state/state.go @@ -144,8 +144,10 @@ func NewState(dbType, dataDir string) (*State, error) { db: s.NoState(true), state: s, } - s.validSIKRoots = [][]byte{} s.mtxValidSIKRoots = &sync.Mutex{} + if err := s.FetchValidSIKRoots(); err != nil { + return nil, fmt.Errorf("cannot update valid SIK roots: %w", err) + } return s, os.MkdirAll(filepath.Join(dataDir, storageDirectory, snapshotsDirectory), 0750) } @@ -382,7 +384,18 @@ func (v *State) RevealProcessKeys(tx *models.AdminTx) error { return nil } +// PrepareCommit prepares the state for commit. It returns the new root hash. +func (v *State) PrepareCommit() ([]byte, error) { + v.tx.Lock() + defer v.tx.Unlock() + if err := v.tx.CommitOnTx(v.CurrentHeight()); err != nil { + return nil, err + } + return v.mainTreeViewer(false).Root() +} + // Save persistent save of vochain mem trees. It returns the new root hash. It also notifies the event listeners. +// Save should usually be called after PrepareCommit(). func (v *State) Save() ([]byte, error) { height := v.CurrentHeight() var pidsStartNextBlock [][]byte @@ -405,14 +418,19 @@ func (v *State) Save() ([]byte, error) { // the listeners may need to get the previous (not committed) state. v.tx.Lock() defer v.tx.Unlock() + // Update the SIK merkle-tree roots + if err := v.UpdateSIKRoots(); err != nil { + return nil, fmt.Errorf("cannot update SIK roots: %w", err) + } err := func() error { var err error - if err := v.tx.Commit(height); err != nil { + if err := v.tx.SaveWithoutCommit(); err != nil { return fmt.Errorf("cannot commit statedb tx: %w", err) } if v.tx.TreeTx, err = v.store.BeginTx(); err != nil { return fmt.Errorf("cannot begin statedb tx: %w", err) } + v.txCounter.Store(0) return nil }() if err != nil { @@ -425,8 +443,7 @@ func (v *State) Save() ([]byte, error) { return nil, fmt.Errorf("cannot get statedb mainTreeView: %w", err) } v.setMainTreeView(mainTreeView) - - return v.store.Hash() + return mainTreeView.Root() } // Rollback rollbacks to the last persistent db data version @@ -437,6 +454,7 @@ func (v *State) Rollback() { v.tx.Lock() defer v.tx.Unlock() v.tx.Discard() + v.store.NoStateWriteTx.Discard() var err error if v.tx.TreeTx, err = v.store.BeginTx(); err != nil { log.Errorf("cannot begin statedb tx: %s", err) @@ -449,6 +467,7 @@ func (v *State) Rollback() { func (v *State) Close() error { v.tx.Lock() v.tx.Discard() + v.store.NoStateWriteTx.Discard() v.tx.Unlock() return v.db.Close() @@ -470,11 +489,9 @@ func (v *State) SetHeight(height uint32) { v.currentHeight.Store(height) } -// WorkingHash returns the hash of the vochain StateDB (mainTree.Root) -func (v *State) WorkingHash() []byte { - v.tx.RLock() - defer v.tx.RUnlock() - hash, err := v.tx.Root() +// CommittedHash returns the hash of the last committed vochain StateDB +func (v *State) CommittedHash() []byte { + hash, err := v.mainTreeViewer(true).Root() if err != nil { panic(fmt.Sprintf("cannot get statedb mainTree root: %s", err)) } diff --git a/vochain/state/state_snapshot_test.go b/vochain/state/state_snapshot_test.go index 19cee2e65..084944f8b 100644 --- a/vochain/state/state_snapshot_test.go +++ b/vochain/state/state_snapshot_test.go @@ -34,6 +34,8 @@ func TestSnapshot(t *testing.T) { err = nostate.Set([]byte("nostate2"), []byte("value2")) qt.Assert(t, err, qt.IsNil) + _, err = st.PrepareCommit() + qt.Assert(t, err, qt.IsNil) hash, err := st.Save() qt.Assert(t, err, qt.IsNil) qt.Assert(t, hash, qt.Not(qt.IsNil)) diff --git a/vochain/state/state_test.go b/vochain/state/state_test.go index abd5cd5f2..545cd4a1e 100644 --- a/vochain/state/state_test.go +++ b/vochain/state/state_test.go @@ -16,11 +16,19 @@ import ( "go.vocdoni.io/proto/build/go/models" ) +func testSaveState(t *testing.T, s *State) []byte { + _, err := s.PrepareCommit() + qt.Assert(t, err, qt.IsNil) + hash, err := s.Save() + qt.Assert(t, err, qt.IsNil) + return hash +} + func TestStateReopen(t *testing.T) { dir := t.TempDir() s, err := NewState(db.TypePebble, dir) qt.Assert(t, err, qt.IsNil) - hash1Before, err := s.Save() + hash1Before := testSaveState(t, s) qt.Assert(t, err, qt.IsNil) s.Close() @@ -70,7 +78,7 @@ func TestStateBasic(t *testing.T) { qt.Assert(t, err, qt.IsNil) qt.Assert(t, totalVotes, qt.Equals, uint64(10*(i+1))) } - s.Save() + testSaveState(t, s) p, err := s.Process(pids[10], false) if err != nil { @@ -122,7 +130,7 @@ func TestBalanceTransfer(t *testing.T) { err = s.CreateAccount(addr2.Address(), "ipfs://", [][]byte{}, 0) qt.Assert(t, err, qt.IsNil) - s.Save() // Save to test committed value on next call + testSaveState(t, s) // Save to test committed value on next call b1, err := s.GetAccount(addr1.Address(), true) qt.Assert(t, err, qt.IsNil) qt.Assert(t, b1.Balance, qt.Equals, uint64(50)) @@ -163,7 +171,7 @@ func TestBalanceTransfer(t *testing.T) { qt.Assert(t, err, qt.IsNil) qt.Assert(t, b1.Balance, qt.Equals, uint64(45)) - s.Save() + testSaveState(t, s) b2, err = s.GetAccount(addr2.Address(), true) qt.Assert(t, err, qt.IsNil) qt.Assert(t, b2.Balance, qt.Equals, uint64(5)) @@ -208,8 +216,7 @@ func TestOnProcessStart(t *testing.T) { s.Rollback() s.SetHeight(height) fn() - _, err := s.Save() - qt.Assert(t, err, qt.IsNil) + testSaveState(t, s) } pid := rng.RandomBytes(32) @@ -267,8 +274,7 @@ func TestBlockMemoryUsage(t *testing.T) { } qt.Assert(t, s.AddProcess(p), qt.IsNil) - _, err = s.Save() - qt.Assert(t, err, qt.IsNil) + testSaveState(t, s) // block 2 height = 2 @@ -293,8 +299,7 @@ func TestBlockMemoryUsage(t *testing.T) { } } - _, err = s.Save() - qt.Assert(t, err, qt.IsNil) + testSaveState(t, s) } func TestStateTreasurer(t *testing.T) { @@ -323,8 +328,7 @@ func TestStateTreasurer(t *testing.T) { // key does not exist yet qt.Assert(t, err, qt.IsNotNil) - _, err = s.Save() - qt.Assert(t, err, qt.IsNil) + testSaveState(t, s) fetchedTreasurer, err = s.Treasurer(true) qt.Assert(t, err, qt.IsNil) @@ -403,9 +407,7 @@ func TestNoState(t *testing.T) { s.Rollback() s.SetHeight(height) fn() - h, err := s.Save() - qt.Assert(t, err, qt.IsNil) - return h + return testSaveState(t, s) } ns := s.NoState(true) diff --git a/vochain/transaction/account_tx.go b/vochain/transaction/account_tx.go index 0660126ef..4eb62c28b 100644 --- a/vochain/transaction/account_tx.go +++ b/vochain/transaction/account_tx.go @@ -122,9 +122,6 @@ func (t *TransactionHandler) SetAccountDelegateTxCheck(vtx *vochaintx.Tx) error tx.Txtype != models.TxType_DEL_DELEGATE_FOR_ACCOUNT { return fmt.Errorf("invalid tx type") } - if tx.Nonce == nil { - return fmt.Errorf("invalid nonce") - } if len(tx.Delegates) == 0 { return fmt.Errorf("invalid delegates") } @@ -135,9 +132,6 @@ func (t *TransactionHandler) SetAccountDelegateTxCheck(vtx *vochaintx.Tx) error if err := vstate.CheckDuplicateDelegates(tx.Delegates, txSenderAddress); err != nil { return fmt.Errorf("checkDuplicateDelegates: %w", err) } - if tx.GetNonce() != txSenderAccount.Nonce { - return fmt.Errorf("invalid nonce, expected %d got %d", txSenderAccount.Nonce, tx.Nonce) - } cost, err := t.state.TxBaseCost(tx.Txtype, false) if err != nil { return fmt.Errorf("cannot get tx cost: %w", err) @@ -196,14 +190,6 @@ func (t *TransactionHandler) SetAccountInfoTxCheck(vtx *vochaintx.Tx) error { if txSenderAccount == nil { return vstate.ErrAccountNotExist } - // check txSender nonce - if tx.GetNonce() != txSenderAccount.Nonce { - return fmt.Errorf( - "invalid nonce, expected %d got %d", - txSenderAccount.Nonce, - tx.GetNonce(), - ) - } // get setAccount tx cost costSetAccountInfoURI, err := t.state.TxBaseCost(models.TxType_SET_ACCOUNT_INFO_URI, false) if err != nil { diff --git a/vochain/transaction/election_tx.go b/vochain/transaction/election_tx.go index 7f85f30e6..7e8f4ac37 100644 --- a/vochain/transaction/election_tx.go +++ b/vochain/transaction/election_tx.go @@ -94,16 +94,11 @@ func (t *TransactionHandler) NewProcessTxCheck(vtx *vochaintx.Tx) (*models.Proce if acc.Balance < cost { return nil, ethereum.Address{}, fmt.Errorf("%w: required %d, got %d", vstate.ErrNotEnoughBalance, cost, acc.Balance) } - if acc.Nonce != tx.Nonce { - return nil, ethereum.Address{}, fmt.Errorf("%w: expected %d, got %d", vstate.ErrAccountNonceInvalid, acc.Nonce, tx.Nonce) - } // if organization ID is not set, use the sender address if tx.Process.EntityId == nil { tx.Process.EntityId = addr.Bytes() - } else if !bytes.Equal(tx.Process.EntityId, addr.Bytes()) { // check if process entityID matches tx sender - // check for a delegate entityAddress := ethereum.AddrFromBytes(tx.Process.EntityId) entityAccount, err := t.state.GetAccount(entityAddress, false) @@ -163,9 +158,6 @@ func (t *TransactionHandler) SetProcessTxCheck(vtx *vochaintx.Tx) (ethereum.Addr if acc.Balance < cost { return ethereum.Address{}, vstate.ErrNotEnoughBalance } - if acc.Nonce != tx.Nonce { - return ethereum.Address{}, vstate.ErrAccountNonceInvalid - } // get process process, err := t.state.Process(tx.ProcessId, false) if err != nil { diff --git a/vochain/transaction/nonce.go b/vochain/transaction/nonce.go new file mode 100644 index 000000000..69f5ce652 --- /dev/null +++ b/vochain/transaction/nonce.go @@ -0,0 +1,88 @@ +package transaction + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "go.vocdoni.io/dvote/crypto/ethereum" + "go.vocdoni.io/dvote/log" + "go.vocdoni.io/dvote/vochain/transaction/vochaintx" + "go.vocdoni.io/proto/build/go/models" +) + +// ExtractNonceAndSender extracts the nonce and sender address from a given Vochain transaction. +// The function uses the signature of the transaction to derive the sender's public key and subsequently +// the Ethereum address. The nonce is extracted based on the specific payload type of the transaction. +// If the transaction does not contain signature or nonce, it returns the default values (nil and 0). +func (t *TransactionHandler) ExtractNonceAndSender(vtx *vochaintx.Tx) (*common.Address, uint32, error) { + var ptx interface { + GetNonce() uint32 + } + + switch payload := vtx.Tx.Payload.(type) { + case *models.Tx_NewProcess: + ptx = payload.NewProcess + case *models.Tx_SetProcess: + ptx = payload.SetProcess + case *models.Tx_SendTokens: + ptx = payload.SendTokens + case *models.Tx_SetAccount: + if payload.SetAccount.Txtype == models.TxType_CREATE_ACCOUNT { + // create account tx is a special case where the nonce is not relevant + return nil, 0, nil + } + ptx = payload.SetAccount + case *models.Tx_CollectFaucet: + ptx = payload.CollectFaucet + case *models.Tx_Vote, *models.Tx_Admin, *models.Tx_MintTokens, *models.Tx_SetKeykeeper, + *models.Tx_SetTransactionCosts, *models.Tx_DelSIK, *models.Tx_RegisterKey, *models.Tx_SetSIK, + *models.Tx_RegisterSIK: + // these tx does not have incremental nonce + return nil, 0, nil + default: + log.Errorf("unknown payload type on extract nonce: %T", payload) + } + + if ptx == nil { + return nil, 0, fmt.Errorf("payload is nil") + } + + pubKey, err := ethereum.PubKeyFromSignature(vtx.SignedBody, vtx.Signature) + if err != nil { + return nil, 0, fmt.Errorf("cannot extract public key from vtx.Signature: %w", err) + } + addr, err := ethereum.AddrFromPublicKey(pubKey) + if err != nil { + return nil, 0, fmt.Errorf("cannot extract address from public key: %w", err) + } + + return &addr, ptx.GetNonce(), nil +} + +// checkAccountNonce checks if the nonce of the given transaction matches the nonce of the sender account. +// If the transactions does not require a nonce, it returns nil. +// The check is performed against the current (not committed) state. +func (t *TransactionHandler) checkAccountNonce(vtx *vochaintx.Tx) error { + addr, nonce, err := t.ExtractNonceAndSender(vtx) + if err != nil { + return err + } + if addr == nil && nonce == 0 { + // no nonce required + return nil + } + if addr == nil { + return fmt.Errorf("could not check nonce, address is nil") + } + account, err := t.state.GetAccount(*addr, false) + if err != nil { + return fmt.Errorf("could not check nonce, error getting account: %w", err) + } + if account == nil { + return fmt.Errorf("could not check nonce, account does not exist") + } + if account.Nonce != nonce { + return fmt.Errorf("nonce mismatch, expected %d, got %d", account.Nonce, nonce) + } + return nil +} diff --git a/vochain/transaction/tokens_tx.go b/vochain/transaction/tokens_tx.go index 8f78d440e..445fefd4a 100644 --- a/vochain/transaction/tokens_tx.go +++ b/vochain/transaction/tokens_tx.go @@ -27,10 +27,6 @@ func (t *TransactionHandler) SetTransactionCostsTxCheck(vtx *vochaintx.Tx) (uint if err != nil { return 0, err } - // check nonce - if tx.Nonce != treasurer.Nonce { - return 0, fmt.Errorf("invalid nonce %d, expected: %d", tx.Nonce, treasurer.Nonce) - } // check valid tx type if _, ok := vstate.TxTypeCostToStateKey[tx.Txtype]; !ok { return 0, fmt.Errorf("tx type not supported") @@ -86,9 +82,6 @@ func (t *TransactionHandler) MintTokensTxCheck(vtx *vochaintx.Tx) error { txSenderAddress.String(), ) } - if tx.Nonce != treasurer.Nonce { - return fmt.Errorf("invalid nonce %d, expected: %d", tx.Nonce, treasurer.Nonce) - } toAddr := common.BytesToAddress(tx.To) toAcc, err := t.state.GetAccount(toAddr, false) if err != nil { @@ -148,9 +141,6 @@ func (t *TransactionHandler) SendTokensTxCheck(vtx *vochaintx.Tx) error { if acc == nil { return vstate.ErrAccountNotExist } - if tx.Nonce != acc.Nonce { - return fmt.Errorf("invalid nonce, expected %d got %d", acc.Nonce, tx.Nonce) - } cost, err := t.state.TxBaseCost(models.TxType_SEND_TOKENS, false) if err != nil { return err diff --git a/vochain/transaction/transaction.go b/vochain/transaction/transaction.go index 795a6f8cb..0f3261ec9 100644 --- a/vochain/transaction/transaction.go +++ b/vochain/transaction/transaction.go @@ -23,7 +23,7 @@ var ( ErrInvalidURILength = fmt.Errorf("invalid URI length") // ErrorAlreadyExistInCache is returned if the transaction has been already processed // and stored in the vote cache. - ErrorAlreadyExistInCache = fmt.Errorf("vote already exist in cache") + ErrorAlreadyExistInCache = fmt.Errorf("transaction already exist in cache") ) // TransactionResponse is the response of a transaction check. @@ -77,6 +77,11 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa response := &TransactionResponse{ TxHash: vtx.TxID[:], } + if forCommit { + if err := t.checkAccountNonce(vtx); err != nil { + return nil, fmt.Errorf("checkAccountNonce: %w", err) + } + } switch vtx.Tx.Payload.(type) { case *models.Tx_Vote: v, err := t.VoteTxCheck(vtx, forCommit) diff --git a/vochain/transaction/vochaintx/vochaintx.go b/vochain/transaction/vochaintx/vochaintx.go index 4bb91412f..014967274 100644 --- a/vochain/transaction/vochaintx/vochaintx.go +++ b/vochain/transaction/vochaintx/vochaintx.go @@ -20,9 +20,12 @@ type Tx struct { TxModelType string } -// Unmarshal unarshal the content of a bytes serialized transaction. -// Returns the transaction struct, the original bytes and the signature -// of those bytes. +// Unmarshal decodes the content of a serialized transaction into the Tx struct. +// +// The function determines the type of the transaction using Protocol Buffers +// reflection and sets it to the TxModelType field. +// Extracts the signature. Prepares the signed body (ready to be checked) and +// computes the transaction ID (a hash of the data). func (tx *Tx) Unmarshal(content []byte, chainID string) error { stx := new(models.SignedTx) if err := proto.Unmarshal(content, stx); err != nil { diff --git a/vochain/transaction_zk_test.go b/vochain/transaction_zk_test.go index bdaeb4e56..74b70bfb4 100644 --- a/vochain/transaction_zk_test.go +++ b/vochain/transaction_zk_test.go @@ -66,6 +66,9 @@ func TestVoteCheckZkSNARK(t *testing.T) { c.Assert(err, qt.IsNil) _, err = app.State.Process(electionId, false) c.Assert(err, qt.IsNil) + // advance the app block so the SIK tree is updated + app.AdvanceTestBlock() + // generate circuit inputs and the zk proof sikRoot, err := app.State.SIKRoot() c.Assert(err, qt.IsNil) diff --git a/vochain/vochaininfo/vochaininfo.go b/vochain/vochaininfo/vochaininfo.go index c2fc1c70e..756818817 100644 --- a/vochain/vochaininfo/vochaininfo.go +++ b/vochain/vochaininfo/vochaininfo.go @@ -195,7 +195,7 @@ func (vi *VochainInfo) SIKTreeSize() uint64 { // TokensBurned returns the current balance of the burn address func (vi *VochainInfo) TokensBurned() uint64 { acc, err := vi.vnode.State.GetAccount(state.BurnAddress, true) - if err != nil { + if err != nil || acc == nil { return 0 } return acc.Balance diff --git a/vocone/vocone.go b/vocone/vocone.go index 04234ae55..b6149b613 100644 --- a/vocone/vocone.go +++ b/vocone/vocone.go @@ -176,26 +176,18 @@ func (vc *Vocone) Start() { vc.vcMtx.Lock() startTime := time.Now() height := vc.height.Load() + // Create and execute block - resp, err := vc.App.FinalizeBlock( - context.Background(), - &abcitypes.RequestFinalizeBlock{ - Txs: vc.prepareBlock(), - Height: height, - Time: time.Now(), - }, - ) + resp, err := vc.App.ExecuteBlock(vc.prepareBlock(), uint32(height), time.Now()) if err != nil { - log.Error(err, "finalize block error") + log.Error(err, "execute block error") } - // Commit block to persistent state - _, err = vc.App.Commit(context.Background(), &abcitypes.RequestCommit{}) - if err != nil { - log.Error(err, "commit error") + if _, err := vc.App.CommitState(); err != nil { + log.Fatalf("could not commit state: %v", err) } log.Debugw("block committed", "height", height, - "hash", hex.EncodeToString(resp.GetAppHash()), + "hash", hex.EncodeToString(resp.Root), "took", time.Since(startTime), ) vc.vcMtx.Unlock() @@ -227,12 +219,24 @@ func (vc *Vocone) CreateAccount(key common.Address, acc *state.Account) error { if err := vc.App.State.SetAccount(key, acc); err != nil { return err } - if _, err := vc.App.State.Save(); err != nil { + if _, err := vc.Commit(); err != nil { return err } return nil } +// Commit saves the current state and returns the hash. +func (vc *Vocone) Commit() ([]byte, error) { + hash, err := vc.App.State.PrepareCommit() + if err != nil { + return nil, err + } + if _, err := vc.App.State.Save(); err != nil { + return nil, err + } + return hash, nil +} + // SetTreasurer configures the vocone treasurer account address func (vc *Vocone) SetTreasurer(treasurer common.Address) error { vc.vcMtx.Lock() @@ -240,7 +244,7 @@ func (vc *Vocone) SetTreasurer(treasurer common.Address) error { if err := vc.App.State.SetTreasurer(treasurer, 0); err != nil { return err } - if _, err := vc.App.State.Save(); err != nil { + if _, err := vc.Commit(); err != nil { return err } return nil @@ -272,7 +276,7 @@ func (vc *Vocone) SetKeyKeeper(key *ethereum.SignKeys) error { return err } log.Infow("adding validator", "address", key.Address().Hex(), "keyIndex", 1) - if _, err := vc.App.State.Save(); err != nil { + if _, err := vc.Commit(); err != nil { return err } vc.KeyKeeper, err = keykeeper.NewKeyKeeper( @@ -293,7 +297,7 @@ func (vc *Vocone) MintTokens(to common.Address, amount uint64) error { if err := vc.App.State.IncrementTreasurerNonce(); err != nil { return err } - if _, err := vc.App.State.Save(); err != nil { + if _, err := vc.Commit(); err != nil { return err } return nil @@ -309,7 +313,7 @@ func (vc *Vocone) SetTxCost(txType models.TxType, cost uint64) error { if err := vc.App.State.IncrementTreasurerNonce(); err != nil { return err } - if _, err := vc.App.State.Save(); err != nil { + if _, err := vc.Commit(); err != nil { return err } return nil @@ -336,7 +340,7 @@ func (vc *Vocone) SetBulkTxCosts(txCost uint64, force bool) error { return err } } - if _, err := vc.App.State.Save(); err != nil { + if _, err := vc.Commit(); err != nil { return err } return nil