Skip to content

Commit 19c6d68

Browse files
authored
vochain: order transactions when creating new blocks (#1121)
refactor Vocdoni's Interaction with CometBFT 1. Implement transaction ordering during block creation. 2. Ensure blocks do not store invalid transactions. 3. Introduce mempool TTL mechanism and max attempts protection. 4. Implement Optimistic Execution for validators. 5. Resolve an app hash bug during block construction. **Transaction Ordering** - Transactions are sorted by nonce for the same sender during the "Prepare proposal" phase of CometBFT callbacks. **Block Transaction Validation** - A temporary state is constructed to execute proposal transactions, leading to hard-checks against this state. This eliminates the need for the previous soft-check approach. - `ProcessProposal` will return REJECT if any transaction in the block proposal is invalid. **Mempool Enhancements** - Transactions will be removed from the mempool after a set duration (TTL). - After three unsuccessful attempts to include a transaction in a block, it will be removed from the mempool. **Optimistic Execution** - If a block is executed during `ProcessProposal` by a validator and is accepted, its state is retained and not rolled back. - During `FinalizeBlock`, the system checks if the block was previously executed. If it was, there's no need for re-execution, potentially speeding up block production. **Block Construction Bug** - An issue was identified where the app hash reported by `FinalizeBlock` was incorrectly computed. - Due to the new CometBFT callback structure, block execution and hash return are separated from saving to persistent storage. - The previous `State.Save()` was incompatible with this two-step process. It has been modified to now include `PrepareCommit()` and `Commit()` functions. --- Signed-off-by: p4u <[email protected]>
1 parent a2f259f commit 19c6d68

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1202
-550
lines changed

apiclient/account.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,26 @@ func (c *HTTPclient) Account(address string) (*api.Account, error) {
6969
}
7070

7171
// Transfer sends tokens from the account associated with the client to the given address.
72+
// The nonce is automatically calculated from the account information.
7273
// Returns the transaction hash.
7374
func (c *HTTPclient) Transfer(to common.Address, amount uint64) (types.HexBytes, error) {
7475
acc, err := c.Account("")
7576
if err != nil {
7677
return nil, err
7778
}
79+
return c.TransferWithNonce(to, amount, acc.Nonce)
80+
}
81+
82+
// TransferWithNonce sends tokens from the account associated with the client to the given address.
83+
// Returns the transaction hash.
84+
func (c *HTTPclient) TransferWithNonce(to common.Address, amount uint64, nonce uint32) (types.HexBytes, error) {
85+
var err error
7886
stx := models.SignedTx{}
7987
stx.Tx, err = proto.Marshal(&models.Tx{
8088
Payload: &models.Tx_SendTokens{
8189
SendTokens: &models.SendTokensTx{
8290
Txtype: models.TxType_SET_ACCOUNT_INFO_URI,
83-
Nonce: acc.Nonce,
91+
Nonce: nonce,
8492
From: c.account.Address().Bytes(),
8593
To: to.Bytes(),
8694
Value: amount,

cmd/end2endtest/account.go

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"strings"
9+
"sync"
910
"time"
1011

1112
"github.com/ethereum/go-ethereum/common"
@@ -150,6 +151,8 @@ func testSendTokens(api *apiclient.HTTPclient, aliceKeys, bobKeys *ethereum.Sign
150151
// both pay 2 for each tx
151152
// resulting in balance 52 for alice
152153
// and 44 for bob
154+
// In addition, we send a couple of token txs to burn address to increase the nonce,
155+
// without waiting for them to be mined (this tests that the mempool transactions are properly ordered).
153156

154157
txCost, err := api.TransactionCost(models.TxType_SEND_TOKENS)
155158
if err != nil {
@@ -181,23 +184,47 @@ func testSendTokens(api *apiclient.HTTPclient, aliceKeys, bobKeys *ethereum.Sign
181184
// try to send tokens at the same time:
182185
// alice sends 1/4 of her balance to bob
183186
// sends 1/3 of his balance to alice
184-
amountAtoB := aliceAcc.Balance / 4
185-
amountBtoA := bobAcc.Balance / 3
186-
187-
txhasha, err := alice.Transfer(bobKeys.Address(), amountAtoB)
188-
if err != nil {
189-
return fmt.Errorf("cannot send tokens: %v", err)
190-
}
191-
log.Infof("alice sent %d tokens to bob", amountAtoB)
192-
log.Debugf("tx hash is %x", txhasha)
187+
// Subtract 1 + txCost from each since we are sending an extra tx to increase the nonce to the burn address
188+
amountAtoB := (aliceAcc.Balance) / 4
189+
amountBtoA := (bobAcc.Balance) / 3
190+
191+
// send a couple of token txs to increase the nonce, without waiting for them to be mined
192+
// this tests that the mempool transactions are properly ordered.
193+
wg := sync.WaitGroup{}
194+
wg.Add(1)
195+
go func() {
196+
log.Warnf("send transactions with nonce+1, should not be mined before the others")
197+
// send 1 token to burn address with nonce + 1 (should be mined after the other txs)
198+
if _, err = alice.TransferWithNonce(state.BurnAddress, 1, aliceAcc.Nonce+1); err != nil {
199+
log.Fatalf("cannot burn tokens: %v", err)
200+
}
201+
if _, err = bob.TransferWithNonce(state.BurnAddress, 1, bobAcc.Nonce+1); err != nil {
202+
log.Fatalf("cannot burn tokens: %v", err)
203+
}
204+
wg.Done()
205+
}()
206+
log.Warnf("waiting 6 seconds to let the burn txs be sent")
207+
time.Sleep(6 * time.Second)
208+
var txhasha, txhashb []byte
209+
wg.Add(1)
210+
go func() {
211+
txhasha, err = alice.TransferWithNonce(bobKeys.Address(), amountAtoB, aliceAcc.Nonce)
212+
if err != nil {
213+
log.Fatalf("cannot send tokens: %v", err)
214+
}
215+
log.Infof("alice sent %d tokens to bob", amountAtoB)
216+
log.Debugf("tx hash is %x", txhasha)
193217

194-
txhashb, err := bob.Transfer(aliceKeys.Address(), amountBtoA)
195-
if err != nil {
196-
return fmt.Errorf("cannot send tokens: %v", err)
197-
}
198-
log.Infof("bob sent %d tokens to alice", amountBtoA)
199-
log.Debugf("tx hash is %x", txhashb)
218+
txhashb, err = bob.TransferWithNonce(aliceKeys.Address(), amountBtoA, bobAcc.Nonce)
219+
if err != nil {
220+
log.Fatalf("cannot send tokens: %v", err)
221+
}
222+
log.Infof("bob sent %d tokens to alice", amountBtoA)
223+
log.Debugf("tx hash is %x", txhashb)
224+
wg.Done()
225+
}()
200226

227+
wg.Wait()
201228
ctx, cancel := context.WithTimeout(context.Background(), time.Second*40)
202229
defer cancel()
203230
txrefa, err := api.WaitUntilTxIsMined(ctx, txhasha)
@@ -216,12 +243,12 @@ func testSendTokens(api *apiclient.HTTPclient, aliceKeys, bobKeys *ethereum.Sign
216243
_ = api.WaitUntilNextBlock()
217244

218245
// now check the resulting state
219-
if err := checkAccountNonceAndBalance(alice, aliceAcc.Nonce+1,
220-
aliceAcc.Balance-amountAtoB-txCost+amountBtoA); err != nil {
246+
if err := checkAccountNonceAndBalance(alice, aliceAcc.Nonce+2,
247+
aliceAcc.Balance-amountAtoB-(2*txCost+1)+amountBtoA); err != nil {
221248
return err
222249
}
223-
if err := checkAccountNonceAndBalance(bob, bobAcc.Nonce+1,
224-
bobAcc.Balance-amountBtoA-txCost+amountAtoB); err != nil {
250+
if err := checkAccountNonceAndBalance(bob, bobAcc.Nonce+2,
251+
bobAcc.Balance-amountBtoA-(2*txCost+1)+amountAtoB); err != nil {
225252
return err
226253
}
227254

cmd/end2endtest/helpers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ func (t *e2eElection) generateProofs(csp *ethereum.SignKeys, voterAccts []*ether
186186
wg sync.WaitGroup
187187
vcount int32
188188
)
189+
// Wait for the next block to assure the SIK root is updated
190+
if err := t.api.WaitUntilNextBlock(); err != nil {
191+
return err
192+
}
189193
errorChan := make(chan error)
190194
t.voters = new(sync.Map)
191195
addNaccounts := func(accounts []*ethereum.SignKeys) {

cmd/node/main.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"encoding/hex"
45
"fmt"
56
"io"
67
"net"
@@ -174,13 +175,29 @@ func newConfig() (*config.Config, config.Error) {
174175
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
175176

176177
// set FlagVars first
177-
viper.BindPFlag("dataDir", flag.Lookup("dataDir"))
178+
if err = viper.BindPFlag("dataDir", flag.Lookup("dataDir")); err != nil {
179+
log.Fatalf("failed to bind dataDir flag to viper: %v", err)
180+
}
178181
globalCfg.DataDir = viper.GetString("dataDir")
179-
viper.BindPFlag("chain", flag.Lookup("chain"))
182+
183+
if err = viper.BindPFlag("chain", flag.Lookup("chain")); err != nil {
184+
log.Fatalf("failed to bind chain flag to viper: %v", err)
185+
}
180186
globalCfg.Vochain.Chain = viper.GetString("chain")
181-
viper.BindPFlag("dev", flag.Lookup("dev"))
187+
188+
if err = viper.BindPFlag("dev", flag.Lookup("dev")); err != nil {
189+
log.Fatalf("failed to bind dev flag to viper: %v", err)
190+
}
182191
globalCfg.Dev = viper.GetBool("dev")
183-
viper.BindPFlag("pprofPort", flag.Lookup("pprof"))
192+
193+
if err = viper.BindPFlag("pprofPort", flag.Lookup("pprof")); err != nil {
194+
log.Fatalf("failed to bind pprof flag to viper: %v", err)
195+
}
196+
197+
if err = viper.BindPFlag("dbType", flag.Lookup("dbType")); err != nil {
198+
log.Fatalf("failed to bind dbType flag to viper: %v", err)
199+
}
200+
globalCfg.Vochain.DBType = viper.GetString("dbType")
184201

185202
// use different datadirs for different chains
186203
globalCfg.DataDir = filepath.Join(globalCfg.DataDir, globalCfg.Vochain.Chain)
@@ -267,7 +284,7 @@ func newConfig() (*config.Config, config.Error) {
267284
}
268285
}
269286

270-
if len(globalCfg.SigningKey) < 32 {
287+
if globalCfg.SigningKey == "" {
271288
fmt.Println("no signing key, generating one...")
272289
signer := ethereum.NewSignKeys()
273290
err = signer.Generate()
@@ -283,6 +300,10 @@ func newConfig() (*config.Config, config.Error) {
283300
globalCfg.SaveConfig = true
284301
}
285302

303+
if globalCfg.Vochain.MinerKey == "" {
304+
globalCfg.Vochain.MinerKey = globalCfg.SigningKey
305+
}
306+
286307
if globalCfg.SaveConfig {
287308
viper.Set("saveConfig", false)
288309
if err := viper.WriteConfig(); err != nil {
@@ -381,7 +402,8 @@ func main() {
381402
log.Error(http.Serve(ln, nil))
382403
}()
383404
}
384-
log.Infow("starting vocdoni node", "version", internal.Version, "mode", globalCfg.Mode)
405+
log.Infow("starting vocdoni node", "version", internal.Version, "mode", globalCfg.Mode,
406+
"chain", globalCfg.Vochain.Chain, "dbType", globalCfg.Vochain.DBType)
385407
if globalCfg.Dev {
386408
log.Warn("developer mode is enabled!")
387409
}
@@ -497,9 +519,7 @@ func main() {
497519
if err != nil {
498520
log.Fatal(err)
499521
}
500-
if validator == nil {
501-
log.Warnw("node is not a validator", "address", signer.Address().Hex())
502-
} else {
522+
if validator != nil {
503523
// start keykeeper service (if key index specified)
504524
if validator.KeyIndex > 0 {
505525
srv.KeyKeeper, err = keykeeper.NewKeyKeeper(
@@ -511,12 +531,10 @@ func main() {
511531
log.Fatal(err)
512532
}
513533
go srv.KeyKeeper.RevealUnpublished()
514-
} else {
515-
log.Warnw("validator keyIndex disabled")
534+
log.Infow("configured keykeeper validator",
535+
"address", signer.Address().Hex(),
536+
"keyIndex", validator.KeyIndex)
516537
}
517-
log.Infow("configured vochain validator",
518-
"address", signer.Address().Hex(),
519-
"keyIndex", validator.KeyIndex)
520538
}
521539
}
522540

@@ -571,6 +589,17 @@ func main() {
571589
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
572590
<-c
573591
log.Warnf("received SIGTERM, exiting at %s", time.Now().Format(time.RFC850))
592+
height, err := srv.App.State.LastHeight()
593+
if err != nil {
594+
log.Warn(err)
595+
}
596+
hash, err := srv.App.State.MainTreeView().Root()
597+
if err != nil {
598+
log.Warn(err)
599+
}
600+
tmBlock := srv.App.GetBlockByHeight(int64(height))
601+
log.Infow("last block", "height", height, "appHash", hex.EncodeToString(hash),
602+
"time", tmBlock.Time, "tmAppHash", tmBlock.AppHash.String(), "tmHeight", tmBlock.Height)
574603
os.Exit(0)
575604
}
576605

dockerfiles/testsuite/env.gateway0

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ VOCDONI_VOCHAIN_NOWAITSYNC=True
1010
VOCDONI_METRICS_ENABLED=True
1111
VOCDONI_METRICS_REFRESHINTERVAL=5
1212
VOCDONI_CHAIN=dev
13-
VOCDONI_SIGNINGKEY=e0f1412b86d6ca9f2b318f1d243ef50be23d315a2e6c1c3035bc72d44c8b2f90
13+
VOCDONI_SIGNINGKEY=e0f1412b86d6ca9f2b318f1d243ef50be23d315a2e6c1c3035bc72d44c8b2f90 # 0x88a499cEf9D1330111b41360173967c9C1bf703f

dockerfiles/testsuite/env.seed

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
VOCDONI_DATADIR=/app/run
22
VOCDONI_MODE=seed
3-
VOCDONI_LOGLEVEL=info
3+
VOCDONI_LOGLEVEL=debug
44
VOCDONI_DEV=True
55
VOCDONI_VOCHAIN_PUBLICADDR=seed:26656
6-
VOCDONI_VOCHAIN_LOGLEVEL=debug
6+
VOCDONI_VOCHAIN_LOGLEVEL=info
77
VOCDONI_VOCHAIN_GENESIS=/app/misc/genesis.json
88
VOCDONI_VOCHAIN_NODEKEY=0x2060e20d1f0894d6b23901bce3f20f26107baf0335451ad75ef27b14e4fc56ae050a65ae3883c379b70d811d6e12db2fe1e3a5cf0cae4d03dbbbfebc68601bdd
99
VOCDONI_METRICS_ENABLED=True

dockerfiles/testsuite/genesis.json

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,8 @@
5757
],
5858
"accounts":[
5959
{
60-
"address":"0xccEc2c2D658261Fbdc40b04FEc06d49057242D39",
61-
"balance":10000000
62-
},
63-
{
64-
"address":"0x776d858D17C8018F07899dB535866EBf805a32E0",
65-
"balance":10000000
66-
},
67-
{
68-
"address":"0x074fcAacb8B01850539eaE7E9fEE8dc94549db96",
69-
"balance":10000000
70-
},
71-
{
7260
"address":"0x88a499cEf9D1330111b41360173967c9C1bf703f",
73-
"balance":10000000
61+
"balance":1000000000000
7462
}
7563
],
7664
"treasurer": "0xfe10DAB06D636647f4E40dFd56599da9eF66Db1c",

log/log.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path"
99
"runtime/debug"
10+
"strings"
1011
"time"
1112

1213
"github.com/rs/zerolog"
@@ -89,11 +90,9 @@ func (*invalidCharChecker) Write(p []byte) (int, error) {
8990
return len(p), nil
9091
}
9192

92-
// Init initializes the logger. Output can be either "stdout/stderr/<filePath>".
93-
// Log level can be "debug/info/warn/error".
94-
// errorOutput is an optional filename which only receives Warning and Error messages.
9593
func Init(level, output string, errorOutput io.Writer) {
9694
var out io.Writer
95+
outputs := []io.Writer{}
9796
switch output {
9897
case "stdout":
9998
out = os.Stdout
@@ -107,12 +106,16 @@ func Init(level, output string, errorOutput io.Writer) {
107106
panic(fmt.Sprintf("cannot create log output: %v", err))
108107
}
109108
out = f
109+
if strings.HasSuffix(output, ".json") {
110+
outputs = append(outputs, f)
111+
out = os.Stdout
112+
}
110113
}
111114
out = zerolog.ConsoleWriter{
112115
Out: out,
113116
TimeFormat: time.RFC3339Nano,
114117
}
115-
outputs := []io.Writer{out}
118+
outputs = append(outputs, out)
116119

117120
if errorOutput != nil {
118121
outputs = append(outputs, &errorLevelWriter{zerolog.ConsoleWriter{

statedb/treeupdate.go

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -150,33 +150,13 @@ func (u *TreeUpdate) SubTree(cfg TreeConfig) (treeUpdate *TreeUpdate, err error)
150150
if treeUpdate, ok := u.openSubs.Load(cfg.prefix); ok {
151151
return treeUpdate.(*TreeUpdate), nil
152152
}
153-
parentLeaf, err := u.tree.Get(u.tree.tx, cfg.parentLeafKey)
154-
if err != nil {
155-
return nil, err
156-
}
157-
root, err := cfg.parentLeafGetRoot(parentLeaf)
158-
if err != nil {
159-
return nil, err
160-
}
161153
tx := subWriteTx(u.tx, path.Join(subKeySubTree, cfg.prefix))
162154
txTree := subWriteTx(tx, subKeyTree)
163155
tree, err := tree.New(txTree,
164156
tree.Options{DB: nil, MaxLevels: cfg.maxLevels, HashFunc: cfg.hashFunc})
165157
if err != nil {
166158
return nil, err
167159
}
168-
lastRoot, err := tree.Root(txTree)
169-
if err != nil {
170-
return nil, err
171-
}
172-
if !bytes.Equal(root, lastRoot) {
173-
panic(fmt.Sprintf("root for %s mismatch: %x != %x", cfg.kindID, root, lastRoot))
174-
// Note (Pau): since we modified arbo to remove all unecessary intermediate nodes,
175-
// we cannot set a past root.We should probably remove this code.
176-
//if err := tree.SetRoot(txTree, root); err != nil {
177-
// return nil, err
178-
//}
179-
}
180160
treeUpdate = &TreeUpdate{
181161
tx: tx,
182162
tree: treeWithTx{
@@ -331,6 +311,15 @@ func propagateRoot(treeUpdate *TreeUpdate) ([]byte, error) {
331311
// version numbers, but overwriting an existing version can be useful in some
332312
// cases (for example, overwriting version 0 to setup a genesis state).
333313
func (t *TreeTx) Commit(version uint32) error {
314+
if err := t.CommitOnTx(version); err != nil {
315+
return err
316+
}
317+
return t.tx.Commit()
318+
}
319+
320+
// CommitOnTx do as Commit but without committing the transaction to database.
321+
// After CommitOnTx(), caller should call SaveWithoutCommit() to commit the transaction.
322+
func (t *TreeTx) CommitOnTx(version uint32) error {
334323
root, err := propagateRoot(&t.TreeUpdate)
335324
if err != nil {
336325
return fmt.Errorf("could not propagate root: %w", err)
@@ -350,7 +339,7 @@ func (t *TreeTx) Commit(version uint32) error {
350339
if err := setVersionRoot(t.tx, version, root); err != nil {
351340
return fmt.Errorf("could not set version root: %w", err)
352341
}
353-
return t.tx.Commit()
342+
return nil
354343
}
355344

356345
// Discard all the changes that have been made from the TreeTx. After calling

0 commit comments

Comments
 (0)