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