diff --git a/apiclient/account.go b/apiclient/account.go index 08784d251..abba79d07 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 507e9ff51..da6b17c9f 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/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/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 0bdf13b42..d86e23b22 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/tree/arbo/tree.go b/tree/arbo/tree.go index bc07c430e..18cac5c26 100644 --- a/tree/arbo/tree.go +++ b/tree/arbo/tree.go @@ -84,7 +84,7 @@ var ( // Tree defines the struct that implements the MerkleTree functionalities type Tree struct { - sync.Mutex + sync.RWMutex db db.Database maxLevels int @@ -181,7 +181,7 @@ func (t *Tree) RootWithTx(rTx db.Reader) ([]byte, error) { return rTx.Get(dbKeyRoot) } -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 +325,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 } @@ -521,6 +520,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.RLock() + defer t.RUnlock() + keyPath, err := keyPathFromKey(t.maxLevels, k) if err != nil { return nil, nil, err @@ -602,6 +604,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 +624,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 +678,8 @@ func (t *Tree) IterateWithTx(rTx db.Reader, fromRoot []byte, f func([]byte, []by return err } } + t.RLock() + defer t.RUnlock() return t.iter(rTx, fromRoot, f) } @@ -687,6 +695,8 @@ func (t *Tree) IterateWithStop(fromRoot []byte, f func(int, []byte, []byte) bool return err } } + t.RLock() + defer t.RUnlock() return t.iterWithStop(t.db, fromRoot, 0, f) } @@ -701,6 +711,8 @@ func (t *Tree) IterateWithStopWithTx(rTx db.Reader, fromRoot []byte, f func(int, return err } } + t.RLock() + defer t.RUnlock() return t.iterWithStop(rTx, fromRoot, 0, f) } diff --git a/vochain/account_test.go b/vochain/account_test.go index 5146966a7..2a8044214 100644 --- a/vochain/account_test.go +++ b/vochain/account_test.go @@ -626,10 +626,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) diff --git a/vochain/app.go b/vochain/app.go index 6121778eb..712c2bf2b 100644 --- a/vochain/app.go +++ b/vochain/app.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "path/filepath" + "sort" + "sync" "sync/atomic" "time" @@ -33,7 +35,10 @@ 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 ) var ( @@ -64,6 +69,8 @@ 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. + txTTLReferences 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,6 +81,9 @@ type BaseApplication struct { dataDir string genesisInfo *tmtypes.GenesisDoc + // 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 } @@ -133,6 +143,7 @@ func NewBaseApplication(dbType, dbpath string) (*BaseApplication, error) { // 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) @@ -257,11 +268,23 @@ func (app *BaseApplication) InitChain(_ context.Context, // 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) + // store the initial height of the tx + initialTTLheight, _ := app.txTTLReferences.LoadOrStore(txReference, app.Height()) + // check if the tx is referenced by a previous block and the TTL has expired + if app.Height() > initialTTLheight.(uint32)+transactionBlocksTTL { + // remove tx reference and return checkTx error + log.Debugw("pruning expired tx from mempool", "height", app.Height(), "hash", fmt.Sprintf("%x", txReference)) + app.txTTLReferences.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 @@ -289,6 +312,8 @@ func (app *BaseApplication) CheckTx(_ context.Context, // CometBFT calls it when a new block is decided. func (app *BaseApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) { + app.prepareProposalLock.Lock() + defer app.prepareProposalLock.Unlock() height := uint32(req.GetHeight()) app.beginBlock(req.GetTime(), height) txResults := make([]*abcitypes.ExecTxResult, len(req.Txs)) @@ -314,12 +339,15 @@ func (app *BaseApplication) FinalizeBlock(_ context.Context, app.endBlock(req.GetTime(), height) return &abcitypes.ResponseFinalizeBlock{ AppHash: app.State.WorkingHash(), - TxResults: txResults, // TODO: check if we can remove this + TxResults: txResults, }, nil } // Commit saves the current vochain state and returns a commit hash func (app *BaseApplication) Commit(_ context.Context, _ *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) { + if app.State.TxCounter() > 0 { + log.Infow("commit block", "height", app.Height(), "txs", app.State.TxCounter()) + } // save state _, err := app.State.Save() if err != nil { @@ -335,9 +363,6 @@ func (app *BaseApplication) Commit(_ context.Context, _ *abcitypes.RequestCommit 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 @@ -363,6 +388,7 @@ func (app *BaseApplication) deliverTx(rawTx []byte) *DeliverTxResponse { log.Errorw(err, "rejected tx") return &DeliverTxResponse{Code: 1, Data: []byte(err.Error())} } + app.txTTLReferences.Delete(tx.TxID) // call event listeners for _, e := range app.State.EventListeners() { e.OnNewTx(tx, app.Height(), app.State.TxCounter()) @@ -388,9 +414,6 @@ 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, @@ -474,25 +497,80 @@ func (*BaseApplication) Query(_ context.Context, // 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{} + app.prepareProposalLock.Lock() + defer app.prepareProposalLock.Unlock() + type txInfo struct { + Data []byte + Addr *ethcommon.Address + Nonce uint32 + DecodedTx *vochaintx.Tx + } + + // ensure the pending state is clean + if app.State.TxCounter() > 0 { + panic("found existing pending transactions on prepare proposal") + } + + validTxInfos := []txInfo{} for _, tx := range req.GetTxs() { - resp, err := app.CheckTx(ctx, &abcitypes.RequestCheckTx{ - Tx: tx, Type: abcitypes.CheckTxType_New, + vtx := new(vochaintx.Tx) + if err := vtx.Unmarshal(tx, app.ChainID()); err != nil { + // invalid transaction + 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, }) - if err != nil || resp.Code != 0 { + } + + // 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 validTxInfos[i].Addr.String() == validTxInfos[j].Addr.String() { + return validTxInfos[i].Nonce < validTxInfos[j].Nonce + } + return validTxInfos[i].Addr.String() < validTxInfos[j].Addr.String() + } + 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, - "code", resp.Code, - "data", string(resp.Data), - "info", resp.Info, - "log", resp.Log) + "hash", fmt.Sprintf("%x", txInfo.DecodedTx.TxID), + "data", func() string { + if resp != nil { + return string(resp.Data) + } + return "" + }()) continue } - validTxs = append(validTxs, tx) + validTxs = append(validTxs, txInfo.Data) } - + // Rollback the state to discard the changes made by CheckTx + app.State.Rollback() return &abcitypes.ResponsePrepareProposal{ Txs: validTxs, }, nil @@ -506,8 +584,47 @@ func (app *BaseApplication) PrepareProposal(ctx context.Context, // 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) { +func (app *BaseApplication) ProcessProposal(_ context.Context, + req *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) { + app.prepareProposalLock.Lock() + defer app.prepareProposalLock.Unlock() + // ensure the pending state is clean + if app.State.TxCounter() > 0 { + panic("found existing pending transactions on process proposal") + } + + valid := true + for _, tx := range req.Txs { + vtx := new(vochaintx.Tx) + if err := vtx.Unmarshal(tx, app.ChainID()); err != nil { + // invalid transaction + log.Warnw("could not unmarshal transaction", "err", err) + valid = false + break + } + // Check the validity of the transaction using forCommit true + resp, err := app.TransactionHandler.CheckTx(vtx, true) + if err != nil { + log.Warnw("discard invalid tx on process proposal", + "err", err, + "data", func() string { + if resp != nil { + return string(resp.Data) + } + return "" + }()) + valid = false + break + } + } + // Rollback the state to discard the changes made by CheckTx + app.State.Rollback() + + if !valid { + return &abcitypes.ResponseProcessProposal{ + Status: abcitypes.ResponseProcessProposal_REJECT, + }, nil + } return &abcitypes.ResponseProcessProposal{ Status: abcitypes.ResponseProcessProposal_ACCEPT, }, nil diff --git a/vochain/genesis/genesis.go b/vochain/genesis/genesis.go index c0c018e81..fc59e4a2f 100644 --- a/vochain/genesis/genesis.go +++ b/vochain/genesis/genesis.go @@ -40,8 +40,8 @@ var Genesis = map[string]VochainGenesis{ } var devGenesis = GenesisDoc{ - GenesisTime: time.Date(2023, time.September, 21, 1, 0, 0, 0, time.UTC), - ChainID: "vocdoni-dev-20", + GenesisTime: time.Date(2023, time.October, 3, 1, 0, 0, 0, time.UTC), + ChainID: "vocdoni-dev-21", ConsensusParams: &ConsensusParams{ Block: BlockParams{ MaxBytes: 2097152, 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/proposal_test.go b/vochain/proposal_test.go new file mode 100644 index 000000000..370c265d4 --- /dev/null +++ b/vochain/proposal_test.go @@ -0,0 +1,103 @@ +package vochain + +import ( + "context" + "encoding/hex" + "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, + } + + 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 + t.Logf("Address: %s Nonce: %d\n", hex.EncodeToString(txSendTokens.From), txSendTokens.Nonce) + } + qt.Assert(len(txs), quicktest.Equals, len(resp.Txs)) +} 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..a847a068e 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) } @@ -405,6 +407,10 @@ 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 { @@ -413,6 +419,7 @@ func (v *State) Save() ([]byte, error) { 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,7 +432,6 @@ func (v *State) Save() ([]byte, error) { return nil, fmt.Errorf("cannot get statedb mainTreeView: %w", err) } v.setMainTreeView(mainTreeView) - return v.store.Hash() } @@ -437,6 +443,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 +456,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() 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..6e455c1b3 --- /dev/null +++ b/vochain/transaction/nonce.go @@ -0,0 +1,91 @@ +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_SetSIK: + ptx = payload.SetSIK + case *models.Tx_DelSIK: + ptx = payload.DelSIK + case *models.Tx_Vote, *models.Tx_Admin, *models.Tx_MintTokens, *models.Tx_SetKeykeeper, + *models.Tx_SetTransactionCosts, *models.Tx_RegisterKey, *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 139eeaedd..8045214cb 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)