diff --git a/api/api_types.go b/api/api_types.go index 28f248a9e..0262268dc 100644 --- a/api/api_types.go +++ b/api/api_types.go @@ -254,11 +254,17 @@ type File struct { type ValidatorList struct { Validators []Validator `json:"validators"` } + type Validator struct { - Power uint64 `json:"power"` - PubKey types.HexBytes `json:"pubKey" ` - Address types.HexBytes `json:"address" ` - Name string `json:"name"` + Power uint64 `json:"power"` + PubKey types.HexBytes `json:"pubKey"` + AccountAddress types.HexBytes `json:"address"` + Name string `json:"name"` + ValidatorAddress types.HexBytes `json:"validatorAddress"` + JoinHeight uint64 `json:"joinHeight"` + Votes uint64 `json:"votes"` + Proposals uint64 `json:"proposals"` + Score uint32 `json:"score"` } // Protobuf wrappers diff --git a/api/chain.go b/api/chain.go index 942df9f8e..06e46e93d 100644 --- a/api/chain.go +++ b/api/chain.go @@ -708,10 +708,15 @@ func (a *API) chainValidatorsHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCon validators := ValidatorList{} for _, v := range stateValidators { validators.Validators = append(validators.Validators, Validator{ - Address: v.GetAddress(), - Power: v.GetPower(), - Name: v.GetName(), - PubKey: v.GetPubKey(), + AccountAddress: v.GetAddress(), + ValidatorAddress: v.GetValidatorAddress(), + Power: v.GetPower(), + Name: v.GetName(), + PubKey: v.GetPubKey(), + JoinHeight: v.GetHeight(), + Votes: v.GetVotes(), + Proposals: v.GetProposals(), + Score: v.GetScore(), }) } data, err := json.Marshal(&validators) diff --git a/apiclient/account.go b/apiclient/account.go index 900d77508..6a4176e30 100644 --- a/apiclient/account.go +++ b/apiclient/account.go @@ -166,6 +166,58 @@ func (c *HTTPclient) AccountBootstrap(faucetPkg *models.FaucetPackage, metadata return acc.TxHash, nil } +// AccountSetValidator upgrades to validator status the account associated with the public key provided. +// If the public key is nil, the public key associated with the account client is used. +// Returns the transaction hash. +func (c *HTTPclient) AccountSetValidator(pubKey []byte, name string) (types.HexBytes, error) { + acc, err := c.Account("") + if err != nil { + return nil, fmt.Errorf("account not configured: %w", err) + } + if pubKey == nil { + pubKey = c.account.PublicKey() + } + // Build the transaction + stx := models.SignedTx{} + stx.Tx, err = proto.Marshal(&models.Tx{ + Payload: &models.Tx_SetAccount{ + SetAccount: &models.SetAccountTx{ + Txtype: models.TxType_SET_ACCOUNT_VALIDATOR, + Nonce: &acc.Nonce, + PublicKey: pubKey, + Name: &name, + }, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("could not marshal transaction: %w", err) + } + stx.Signature, err = c.account.SignVocdoniTx(stx.Tx, c.ChainID()) + if err != nil { + return nil, err + } + stxb, err := proto.Marshal(&stx) + if err != nil { + return nil, err + } + resp, code, err := c.Request(HTTPPOST, &api.Transaction{ + Payload: stxb, + }, "chain", "transactions") + if err != nil { + return nil, err + } + if code != apirest.HTTPstatusOK { + return nil, fmt.Errorf("%s: %d (%s)", errCodeNot200, code, resp) + } + accv := &api.Transaction{} + err = json.Unmarshal(resp, accv) + if err != nil { + return nil, err + } + return accv.Hash, nil +} + // AccountSetMetadata updates the metadata associated with the account associated with the client. func (c *HTTPclient) AccountSetMetadata(metadata *api.AccountMetadata) (types.HexBytes, error) { var err error @@ -258,7 +310,9 @@ func (c *HTTPclient) SetSIK(secret []byte) (types.HexBytes, error) { Txtype: models.TxType_SET_ACCOUNT_SIK, SIK: sik, }, - }}) + }, + }, + ) if err != nil { return nil, err } @@ -273,7 +327,7 @@ func (c *HTTPclient) SetSIK(secret []byte) (types.HexBytes, error) { } resp, code, err := c.Request(HTTPPOST, &api.Transaction{ Payload: stxb, - }, "chain", "transaction") + }, "chain", "transactions") if err != nil { return nil, err } diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 0499b210e..354ae4c0e 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -16,10 +17,12 @@ import ( flag "github.com/spf13/pflag" "go.vocdoni.io/dvote/api" "go.vocdoni.io/dvote/apiclient" + "go.vocdoni.io/dvote/crypto/ethereum" "go.vocdoni.io/dvote/internal" "go.vocdoni.io/dvote/log" "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/util" + "go.vocdoni.io/dvote/vochain" "go.vocdoni.io/proto/build/go/models" ) @@ -80,10 +83,11 @@ func main() { items.Sprint("πŸ•ΈοΈ\tNetwork info"), // 5 items.Sprint("πŸ“\tBuild a new census"), // 6 items.Sprint("πŸ—³οΈ\tCreate an election"), // 7 - items.Sprint("β˜‘οΈ\tVote"), // 8 - items.Sprint("πŸ–§\tChange API endpoint host"), // 9 - items.Sprint("πŸ’Ύ\tSave config to file"), // 10 - items.Sprint("❌\tQuit"), // 11 + items.Sprint("β˜‘οΈ\tSet validator"), // 8 + items.Sprint("πŸ“ Generate faucet package"), // 9 + items.Sprint("πŸ–§\tChange API endpoint host"), // 10 + items.Sprint("πŸ’Ύ\tSave config to file"), // 11 + items.Sprint("❌\tQuit"), // 12 }, } @@ -141,15 +145,23 @@ func main() { if err := electionHandler(cli); err != nil { errorp.Println(err) } + case 8: + if err := accountSetValidator(cli); err != nil { + errorp.Println(err) + } case 9: - if err := hostHandler(cli); err != nil { + if err := faucetPkg(cli); err != nil { errorp.Println(err) } case 10: - if err := cli.save(); err != nil { + if err := hostHandler(cli); err != nil { errorp.Println(err) } case 11: + if err := cli.save(); err != nil { + errorp.Println(err) + } + case 12: os.Exit(0) default: errorp.Println("unknown option or not yet implemented") @@ -342,7 +354,6 @@ func transfer(cli *VocdoniCLI) error { } dstAddress = account.Address } else { - p := ui.Prompt{ Label: "destination address", } @@ -397,6 +408,52 @@ func transfer(cli *VocdoniCLI) error { return nil } +func faucetPkg(cli *VocdoniCLI) error { + // FaucetPackage represents the data of a faucet package + type FaucetPackage struct { + // FaucetPackagePayload is the Vocdoni faucet package payload + FaucetPayload []byte `json:"faucetPayload"` + // Signature is the signature for the vocdoni faucet payload + Signature []byte `json:"signature"` + } + signer := ethereum.SignKeys{} + if err := signer.AddHexKey(cli.getCurrentAccount().PrivKey.String()); err != nil { + return err + } + a := ui.Prompt{ + Label: "destination address", + } + addrString, err := a.Run() + if err != nil { + return err + } + to := common.HexToAddress(addrString) + n := ui.Prompt{ + Label: "amount", + } + amountString, err := n.Run() + if err != nil { + return err + } + amount, err := strconv.Atoi(amountString) + if err != nil { + return err + } + fpackage, err := vochain.GenerateFaucetPackage(&signer, to, uint64(amount)) + if err != nil { + return err + } + fpackageBytes, err := json.Marshal(FaucetPackage{ + FaucetPayload: fpackage.Payload, + Signature: fpackage.Signature, + }) + if err != nil { + return err + } + infoPrint.Printf("faucet package for %s with amount %d: [ %s ]\n", to.Hex(), amount, base64.StdEncoding.EncodeToString(fpackageBytes)) + return nil +} + func hostHandler(cli *VocdoniCLI) error { validateFunc := func(url string) error { log.Debugf("performing ping test to %s", url) @@ -431,6 +488,48 @@ func hostHandler(cli *VocdoniCLI) error { return cli.setAuthToken(token) } +func accountSetValidator(cli *VocdoniCLI) error { + infoPrint.Printf("enter the name and a public key of the validator, leave it bank for using the selected account\n") + + n := ui.Prompt{ + Label: "name", + } + name, err := n.Run() + if err != nil { + return err + } + + p := ui.Prompt{ + Label: "public key", + } + pubKeyStr, err := p.Run() + if err != nil { + return err + } + pubKey := cli.getCurrentAccount().PublicKey + if pubKeyStr != "" { + pubKey, err = hex.DecodeString(pubKeyStr) + if err != nil { + return err + } + } + + hash, err := cli.api.AccountSetValidator(pubKey, name) + if err != nil { + return err + } + + infoPrint.Printf("transaction sent! hash %s\n", hash.String()) + infoPrint.Printf("waiting for confirmation...") + ok := cli.waitForTransaction(hash) + if !ok { + return fmt.Errorf("transaction was not included") + } + infoPrint.Printf(" transaction confirmed!\n") + + return nil +} + func accountSetMetadata(cli *VocdoniCLI) error { currentAccount, err := cli.api.Account("") if err != nil { @@ -512,7 +611,6 @@ func accountSetMetadata(cli *VocdoniCLI) error { return fmt.Errorf("transaction was not included") } return nil - } func electionHandler(cli *VocdoniCLI) error { diff --git a/crypto/zk/circuit/config.go b/crypto/zk/circuit/config.go index 88f353519..e22afd8a6 100644 --- a/crypto/zk/circuit/config.go +++ b/crypto/zk/circuit/config.go @@ -82,7 +82,19 @@ func (conf *ZkCircuitConfig) SupportsCensusSize(maxCensusSize uint64) bool { var CircuitsConfigurations = map[string]*ZkCircuitConfig{ "dev": { URI: "https://raw.githubusercontent.com/vocdoni/" + - "zk-franchise-proof-circuit/feature/new-circuit", + "zk-franchise-proof-circuit/master", + CircuitPath: "artifacts/zkCensus/dev/160", + Levels: 160, // ZkCircuit number of levels + ProvingKeyHash: hexToBytes("0xe359b256e5e3c78acaccf8dab5dc4bea99a2f07b2a05e935b5ca658c714dea4a"), + ProvingKeyFilename: "proving_key.zkey", + VerificationKeyHash: hexToBytes("0x235e55571812f8e324e73e37e53829db0c4ac8f68469b9b953876127c97b425f"), + VerificationKeyFilename: "verification_key.json", + WasmHash: hexToBytes("0x80a73567f6a4655d4332301efcff4bc5711bb48176d1c71fdb1e48df222ac139"), + WasmFilename: "circuit.wasm", + }, + "prod": { + URI: "https://raw.githubusercontent.com/vocdoni/" + + "zk-franchise-proof-circuit/master", CircuitPath: "artifacts/zkCensus/dev/160", Levels: 160, // ZkCircuit number of levels ProvingKeyHash: hexToBytes("0xe359b256e5e3c78acaccf8dab5dc4bea99a2f07b2a05e935b5ca658c714dea4a"), diff --git a/dockerfiles/testsuite/env.gateway0 b/dockerfiles/testsuite/env.gateway0 index 82862cbfd..8b8e6a7ef 100755 --- a/dockerfiles/testsuite/env.gateway0 +++ b/dockerfiles/testsuite/env.gateway0 @@ -1,5 +1,6 @@ VOCDONI_DATADIR=/app/run VOCDONI_LOGLEVEL=debug +VOCDONI_VOCHAIN_LOGLEVEL=error VOCDONI_DEV=True VOCDONI_ENABLEAPI=True VOCDONI_ENABLERPC=True diff --git a/dockerfiles/testsuite/env.miner0 b/dockerfiles/testsuite/env.miner0 index 4aff8d112..9840d777c 100755 --- a/dockerfiles/testsuite/env.miner0 +++ b/dockerfiles/testsuite/env.miner0 @@ -3,7 +3,7 @@ VOCDONI_MODE=miner VOCDONI_LOGLEVEL=info VOCDONI_DEV=True VOCDONI_VOCHAIN_PUBLICADDR=miner0:26656 -VOCDONI_VOCHAIN_LOGLEVEL=error +VOCDONI_VOCHAIN_LOGLEVEL=info VOCDONI_VOCHAIN_SEEDS=3c3765494e758ae7baccb1f5b0661755302ddc47@seed:26656 VOCDONI_VOCHAIN_GENESIS=/app/misc/genesis.json VOCDONI_VOCHAIN_MINERKEY=cda909c34901c137e12bb7d0afbcb9d1c8abc66f03862a42344b1f509d1ae4ce diff --git a/dockerfiles/testsuite/genesis.json b/dockerfiles/testsuite/genesis.json index cee349070..4f7e5d6e9 100755 --- a/dockerfiles/testsuite/genesis.json +++ b/dockerfiles/testsuite/genesis.json @@ -31,28 +31,28 @@ "consensus_pub_key": "038faa051e8a726597549bb057f1d296947bb54378443ec8fce030001ece678e14", "power": 10, "key_index": 1, - "name": "" + "name": "validator1" }, { "signer_address": "032faef5d0f2c76bbd804215e822a5203e83385d", "consensus_pub_key": "03cf8d0d1afa561e01145a275d1e41ed1a6d652361509a4c93dfc6488fdf5eca38", "power": 10, "key_index": 2, - "name": "" + "name": "validator2" }, { "signer_address": "1f00b2ee957af530d44c8ceb1fecdd07c5702ad7", "consensus_pub_key": "031802916d945239a39a9a8ee3e2eb3fb91ee324ccdfd73659f482e644892b796f", "power": 10, "key_index": 3, - "name": "" + "name": "validator3" }, { "signer_address": "8992487e178fdcdc0dad95174e2a6d6229b3719c", "consensus_pub_key": "02a790726e98978b0ca2cde3a09cbb1af1b298191f46e051b86bcb1854deb58478", "power": 10, "key_index": 4, - "name": "" + "name": "validator4" } ], "accounts":[ @@ -74,7 +74,8 @@ "Tx_SetAccountInfoURI": 2, "Tx_AddDelegateForAccount": 2, "Tx_DelDelegateForAccount": 2, - "Tx_CollectFaucet": 1 + "Tx_CollectFaucet": 1, + "Tx_SetAccountValidator": 100 } } } diff --git a/go.mod b/go.mod index 4ce4074eb..4be8c8abf 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/vocdoni/storage-proofs-eth-go v0.1.6 go.mongodb.org/mongo-driver v1.12.1 go.uber.org/atomic v1.11.0 - go.vocdoni.io/proto v1.14.6-0.20230802094125-e07a41fda290 + go.vocdoni.io/proto v1.15.4-0.20231017174559-1d9ea54cd9ad golang.org/x/crypto v0.14.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/net v0.16.0 diff --git a/go.sum b/go.sum index 4b2858a94..8005de7b3 100644 --- a/go.sum +++ b/go.sum @@ -1646,8 +1646,8 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= -go.vocdoni.io/proto v1.14.6-0.20230802094125-e07a41fda290 h1:o5MrI+nI5GJDUYMEdzP/JJbMwdiOU1mr1LnrmXaN2q4= -go.vocdoni.io/proto v1.14.6-0.20230802094125-e07a41fda290/go.mod h1:oi/WtiBFJ6QwNDv2aUQYwOnUKzYuS/fBqXF8xDNwcGo= +go.vocdoni.io/proto v1.15.4-0.20231017174559-1d9ea54cd9ad h1:v+ixFtzq/7nwa/y7lY/LiFcVOPjK7aF9EpyX6RWCS2M= +go.vocdoni.io/proto v1.15.4-0.20231017174559-1d9ea54cd9ad/go.mod h1:oi/WtiBFJ6QwNDv2aUQYwOnUKzYuS/fBqXF8xDNwcGo= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= diff --git a/vochain/app.go b/vochain/app.go index 56c691ff9..4538b7db7 100644 --- a/vochain/app.go +++ b/vochain/app.go @@ -67,7 +67,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. + // txLReferences is a map indexed by hashed transactions and storing the height where the transaction + // was seen frist time and the number of attempts failed for including it into a block. txReferences sync.Map // endBlockTimestamp is the last block end timestamp calculated from local time. diff --git a/vochain/cometbft.go b/vochain/cometbft.go index 207f84c9c..0881c3e1b 100644 --- a/vochain/cometbft.go +++ b/vochain/cometbft.go @@ -12,9 +12,11 @@ import ( abcitypes "github.com/cometbft/cometbft/abci/types" crypto256k1 "github.com/cometbft/cometbft/crypto/secp256k1" + "github.com/cometbft/cometbft/proto/tendermint/types" ethcommon "github.com/ethereum/go-ethereum/common" "go.vocdoni.io/dvote/log" "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" "go.vocdoni.io/dvote/vochain/transaction/vochaintx" @@ -88,10 +90,12 @@ func (app *BaseApplication) InitChain(_ context.Context, "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), + Address: genesisAppState.Validators[i].Address, + PubKey: genesisAppState.Validators[i].PubKey, + Power: genesisAppState.Validators[i].Power, + KeyIndex: uint32(genesisAppState.Validators[i].KeyIndex), + Name: genesisAppState.Validators[i].Name, + ValidatorAddress: crypto256k1.PubKey(genesisAppState.Validators[i].PubKey).Address().Bytes(), } if err = app.State.AddValidator(v); err != nil { return nil, fmt.Errorf("cannot add validator %s: %w", log.FormatProto(v), err) @@ -114,10 +118,12 @@ func (app *BaseApplication) InitChain(_ context.Context, 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) + log.Infow("node is a validator!", "power", myValidator.Power, "name", myValidator.Name, + "address", hex.EncodeToString(myValidator.Address), "pubKey", hex.EncodeToString(myValidator.PubKey), + "validatorAddr", hex.EncodeToString(myValidator.ValidatorAddress)) } - // set treasurer address + // set treasurer address // TODO: Remove if genesisAppState.Treasurer != nil { log.Infof("adding genesis treasurer %x", genesisAppState.Treasurer) if err := app.State.SetTreasurer(ethcommon.BytesToAddress(genesisAppState.Treasurer), 0); err != nil { @@ -223,6 +229,7 @@ func (app *BaseApplication) FinalizeBlock(_ context.Context, 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) @@ -254,12 +261,51 @@ func (app *BaseApplication) FinalizeBlock(_ context.Context, "blockSeconds", time.Since(req.GetTime()).Seconds(), "elapsedSeconds", time.Since(start).Seconds(), "proposer", hex.EncodeToString(req.GetProposerAddress())) + + // update validator score as an IST action for the next block. Note that at this point, + // we cannot modify the state or we would break ProcessProposal optimistic execution + proposalVotes := [][]byte{} + for _, v := range req.GetDecidedLastCommit().Votes { + if idFlag := v.GetBlockIdFlag(); idFlag == types.BlockIDFlagAbsent || idFlag == types.BlockIDFlagUnknown { + // skip invalid votes + continue + } + proposalVotes = append(proposalVotes, v.GetValidator().Address) + } + if err := app.Istc.Schedule(height+1, []byte(fmt.Sprintf("validators-update-score-%d", height)), ist.Action{ + ID: ist.ActionUpdateValidatorScore, + ValidatorVotes: proposalVotes, + ValidatorProposer: req.GetProposerAddress(), + }); err != nil { + return nil, fmt.Errorf("finalize block: could not schedule IST action: %w", err) + } + + // update current validators + validators, err := app.State.Validators(false) + if err != nil { + return nil, fmt.Errorf("cannot get validators: %w", err) + } return &abcitypes.ResponseFinalizeBlock{ - AppHash: root, - TxResults: txResults, + AppHash: root, + TxResults: txResults, + ValidatorUpdates: validatorUpdate(validators), }, nil } +func validatorUpdate(validators map[string]*models.Validator) abcitypes.ValidatorUpdates { + validatorUpdate := []abcitypes.ValidatorUpdate{} + for _, v := range validators { + pubKey := make([]byte, len(v.PubKey)) + copy(pubKey, v.PubKey) + validatorUpdate = append(validatorUpdate, abcitypes.UpdateValidator( + pubKey, + int64(v.Power), + crypto256k1.KeyType, + )) + } + return validatorUpdate +} + // 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() @@ -460,8 +506,8 @@ func (*BaseApplication) Query(_ context.Context, } // ExtendVote creates application specific vote extension -func (*BaseApplication) ExtendVote(context.Context, - *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) { +func (*BaseApplication) ExtendVote(_ context.Context, + req *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) { return &abcitypes.ResponseExtendVote{}, nil } diff --git a/vochain/genesis/genesis.go b/vochain/genesis/genesis.go index 776307b1a..eb62ce874 100644 --- a/vochain/genesis/genesis.go +++ b/vochain/genesis/genesis.go @@ -26,15 +26,15 @@ var Genesis = map[string]Vochain{ Genesis: &stageGenesis, }, - // Apex production network - "apex": { + // LTS production network + "lts": { AutoUpdateGenesis: false, SeedNodes: []string{ - "32acbdcda649fbcd35775f1dd8653206d940eee4@seed1.apex.vocdoni.net:26656", - "02bfac9bd98bf25429d12edc50552cca5e975080@seed2.apex.vocdoni.net:26656", + "32acbdcda649fbcd35775f1dd8653206d940eee4@seed1.lts.vocdoni.net:26656", + "02bfac9bd98bf25429d12edc50552cca5e975080@seed2.lts.vocdoni.net:26656", }, - CircuitsConfigTag: "dev", - Genesis: &apexGenesis, + CircuitsConfigTag: "prod", + Genesis: <sGenesis, }, } @@ -115,6 +115,7 @@ var devGenesis = Doc{ CollectFaucet: 1, SetAccountSIK: 1, DelAccountSIK: 1, + SetAccountValidator: 10000, }, }, } @@ -234,13 +235,14 @@ var stageGenesis = Doc{ CollectFaucet: 1, SetAccountSIK: 1, DelAccountSIK: 1, + SetAccountValidator: 100000, }, }, } -var apexGenesis = Doc{ - GenesisTime: time.Date(2023, time.May, 24, 9, 0, 0, 0, time.UTC), - ChainID: "vocdoni-apex-v1", +var ltsGenesis = Doc{ + GenesisTime: time.Date(2023, time.October, 17, 17, 0, 0, 0, time.UTC), + ChainID: "vocdoni-lts-v1", ConsensusParams: &ConsensusParams{ Block: BlockParams{ MaxBytes: 2097152, @@ -328,7 +330,7 @@ var apexGenesis = Doc{ Accounts: []Account{ { // faucet Address: types.HexStringToHexBytes("863a75f41025f0c8878d3a100c8c16576fe8fe4f"), - Balance: 10000000, + Balance: 1000000000, }, }, Treasurer: types.HexStringToHexBytes("13987a54c434d33ec810eeedba4ed7a542e3df24"), @@ -344,6 +346,9 @@ var apexGenesis = Doc{ AddDelegateForAccount: 1, DelDelegateForAccount: 1, CollectFaucet: 1, + SetAccountSIK: 1, + DelAccountSIK: 1, + SetAccountValidator: 200000, }, }, } diff --git a/vochain/genesis/txcost.go b/vochain/genesis/txcost.go index 6f1bbd5bf..876989dd8 100644 --- a/vochain/genesis/txcost.go +++ b/vochain/genesis/txcost.go @@ -15,6 +15,7 @@ type TransactionCosts struct { NewProcess uint32 `json:"Tx_NewProcess"` SendTokens uint32 `json:"Tx_SendTokens"` SetAccountInfoURI uint32 `json:"Tx_SetAccountInfoURI"` + SetAccountValidator uint32 `json:"Tx_SetAccountValidator"` CreateAccount uint32 `json:"Tx_CreateAccount"` AddDelegateForAccount uint32 `json:"Tx_AddDelegateForAccount"` DelDelegateForAccount uint32 `json:"Tx_DelDelegateForAccount"` @@ -45,6 +46,7 @@ var TxCostNameToTxTypeMap = map[string]models.TxType{ "SetProcessQuestionIndex": models.TxType_SET_PROCESS_QUESTION_INDEX, "SendTokens": models.TxType_SEND_TOKENS, "SetAccountInfoURI": models.TxType_SET_ACCOUNT_INFO_URI, + "SetAccountValidator": models.TxType_SET_ACCOUNT_VALIDATOR, "CreateAccount": models.TxType_CREATE_ACCOUNT, "RegisterKey": models.TxType_REGISTER_VOTER_KEY, "NewProcess": models.TxType_NEW_PROCESS, @@ -70,6 +72,7 @@ var TxTypeToCostNameMap = map[models.TxType]string{ models.TxType_SET_PROCESS_QUESTION_INDEX: "SetProcessQuestionIndex", models.TxType_SEND_TOKENS: "SendTokens", models.TxType_SET_ACCOUNT_INFO_URI: "SetAccountInfoURI", + models.TxType_SET_ACCOUNT_VALIDATOR: "SetAccountValidator", models.TxType_CREATE_ACCOUNT: "CreateAccount", models.TxType_REGISTER_VOTER_KEY: "RegisterKey", models.TxType_NEW_PROCESS: "NewProcess", diff --git a/vochain/ist/ist.go b/vochain/ist/ist.go index 03be3d217..8221b9b13 100644 --- a/vochain/ist/ist.go +++ b/vochain/ist/ist.go @@ -74,12 +74,15 @@ const ( ActionCommitResults ActionID = iota // ActionEndProcess sets a process as ended. It schedules ActionComputeResults. ActionEndProcess + // ActionUpdateValidatorScore updates the validator score (votes and proposer) in the state. + ActionUpdateValidatorScore ) // ActionsToString translates the action identifiers to its corresponding human friendly string. var ActionsToString = map[ActionID]string{ - ActionCommitResults: "commit-results", - ActionEndProcess: "end-process", + ActionCommitResults: "commit-results", + ActionEndProcess: "end-process", + ActionUpdateValidatorScore: "update-validator-score", } // Actions is the model used to store the list of IST actions for @@ -88,9 +91,11 @@ type Actions map[string]Action // Action is the model used to store the IST actions into state. type Action struct { - ID ActionID - ElectionID []byte - Attempts uint32 + ID ActionID + ElectionID []byte + Attempts uint32 + ValidatorVotes [][]byte + ValidatorProposer []byte } // encode performs the encoding of the IST action using Gob. @@ -230,6 +235,11 @@ func (c *Controller) Commit(height uint32) error { return fmt.Errorf("cannot end election %x: %w", action.ElectionID, err) } + case ActionUpdateValidatorScore: + log.Debugw("update validator score", "height", height, "id", fmt.Sprintf("%x", id), "action", ActionsToString[action.ID]) + if err := c.updateValidatorScore(action.ValidatorVotes, action.ValidatorProposer); err != nil { + return fmt.Errorf("cannot update validator score: %w", err) + } default: return fmt.Errorf("unknown IST action %d", action.ID) } diff --git a/vochain/ist/validators.go b/vochain/ist/validators.go new file mode 100644 index 000000000..db38b9947 --- /dev/null +++ b/vochain/ist/validators.go @@ -0,0 +1,116 @@ +package ist + +import ( + "bytes" + "encoding/hex" + "fmt" + + "go.vocdoni.io/dvote/log" +) + +/* +This mechanism is responsible for the management and updating of validator power based on their voting and proposing performance over time. +It operates by evaluating the performance of validators in terms of votes on proposals they have accrued. + +As a general idea, when a validator does not participate on the block production, their power will decay over time. +On the other hand, if a validator participates in the block production, their power will increase over time until it reaches the maximum power. +When a new validator joins the validator set, it starts with the minimum power (currently 5). + +The mechanism is based on the following parameters: + +1. `maxPower`: This represents the maximum power that any validator can achieve. Currently set to 100. + +2. `updatePowerPeriod`: The frequency with which the validator power and score updates are made. + Power adjustments for each validator are carried out once every 10 blocks. + +3. `positiveScoreThreshold`: Validators need to maintain a score equal to or above this threshold for their power to increase. + The threshold is currently set at 80. + +4. `powerDecayRate`: If a validator underperforms, i.e., if their new score is lower than their previous score or if it's zero, + their power will decay at this rate. The current rate is set to 5%. + +Workflow: + +- Every `updatePowerPeriod` blocks, the mechanism calculates a new score for each validator based on their voting performance. +- If a validator's score is above or equal to the `positiveScoreThreshold` or if it has improved, their power is incremented by 1, until the `maxPower` is reached. +- If a validator's score drops or is zero, their power is subjected to decay at the rate of `powerDecayRate`. +- Finally, if a validator's power becomes zero (and there are more than 3 validators), they are removed from the validator set. Otherwise, their updated state is stored back. + + +Simulations: + +- Scenario A: Validator stops working after 100,000 blocks: + +With 5% exponential decay and considering an update every 10 blocks: +Approximately 1,320 blocks (or 132 adjustment periods) to decrease power from 100 to 0. + +- Scenario B: A new validator starts working after 100,000 blocks: + +The validator would take approximately 20,000 blocks (or 2,000 adjustment periods) to reach the maximum power of 100, +assuming they maintain an ideal score throughout. +*/ + +const ( + maxPower = 100 // maximum power of a validator + updatePowerPeriod = 10 // number of blocks to wait before updating validators power + positiveScoreThreshold = 80 // if this minimum score is kept, the validator power will be increased + powerDecayRate = 0.05 // 5% decay rate +) + +func (c *Controller) updateValidatorScore(voteAddresses [][]byte, proposer []byte) error { + // get the validators + validators, err := c.state.Validators(true) + if err != nil { + return fmt.Errorf("cannot update validator score: %w", err) + } + // get the validator score + log.Debugw("update validator score", "totalVoters", len(voteAddresses), "proposer", hex.EncodeToString(proposer)) + for _, voteAddr := range voteAddresses { + validator := "" + for k, v := range validators { + if bytes.Equal(voteAddr, v.ValidatorAddress) { + validator = k + break + } + } + if validator != "" { + validators[validator].Votes++ + if bytes.Equal(proposer, validators[validator].ValidatorAddress) { + validators[validator].Proposals++ + } + } + } + // compute the new power and score + for idx := range validators { + if c.state.CurrentHeight()%updatePowerPeriod == 0 { + newScore := uint32(float64(validators[idx].Votes) / + float64(c.state.CurrentHeight()-uint32(validators[idx].Height)) * 100) + if newScore > validators[idx].Score || + (newScore >= positiveScoreThreshold && validators[idx].Score == newScore) { + if validators[idx].Power < maxPower { + validators[idx].Power++ + } + } + if newScore < validators[idx].Score || newScore == 0 { + validators[idx].Power = uint64(float64(validators[idx].Power) * (1 - powerDecayRate)) + } + validators[idx].Score = newScore + } + // update or remove the validator + if validators[idx].Power <= 0 { + if len(validators) <= 3 { + // cannot remove the last 3 validators + validators[idx].Power = 1 + } else { + if err := c.state.RemoveValidator(validators[idx].Address); err != nil { + return fmt.Errorf("cannot remove validator: %w", err) + } + continue + } + } + if err := c.state.AddValidator(validators[idx]); err != nil { + return fmt.Errorf("cannot update validator score: %w", err) + } + } + return nil +} diff --git a/vochain/start.go b/vochain/start.go index c0e4ec04b..59ff5a352 100644 --- a/vochain/start.go +++ b/vochain/start.go @@ -229,11 +229,11 @@ func newTendermint(app *BaseApplication, return nil, fmt.Errorf("cannot create or load node key: %w", err) } } - log.Debugf("tendermint p2p config: %+v", tconfig.P2P) - - log.Infow("tendermint config", + log.Infow("vochain initialized", "db-backend", tconfig.DBBackend, - "pubkey", hex.EncodeToString(pv.Key.PubKey.Bytes()), + "publicKey", hex.EncodeToString(pv.Key.PubKey.Bytes()), + "accountAddr", app.NodeAddress, + "validatorAddr", pv.Key.PubKey.Address(), "external-address", tconfig.P2P.ExternalAddress, "nodeId", nodeKey.ID(), "seed", tconfig.P2P.SeedMode) diff --git a/vochain/state/balances.go b/vochain/state/balances.go index 97b05e7ba..d2039e43d 100644 --- a/vochain/state/balances.go +++ b/vochain/state/balances.go @@ -36,6 +36,7 @@ var ( models.TxType_SET_ACCOUNT_SIK: "c_setAccountSIK", models.TxType_DEL_ACCOUNT_SIK: "c_delAccountSIK", models.TxType_REGISTER_SIK: "c_registerSIK", + models.TxType_SET_ACCOUNT_VALIDATOR: "c_setAccountValidator", } ErrTxCostNotFound = fmt.Errorf("transaction cost is not set") ) diff --git a/vochain/state/validators.go b/vochain/state/validators.go index 97bb86dd0..f05a20a88 100644 --- a/vochain/state/validators.go +++ b/vochain/state/validators.go @@ -5,7 +5,7 @@ import ( "google.golang.org/protobuf/proto" ) -// AddValidator adds a tendemint validator if it is not already added +// AddValidator adds a tendemint validator. If it exists, it will be updated. func (v *State) AddValidator(validator *models.Validator) error { v.tx.Lock() defer v.tx.Unlock() diff --git a/vochain/transaction/account_tx.go b/vochain/transaction/account_tx.go index a67ebcc20..e9f9e912b 100644 --- a/vochain/transaction/account_tx.go +++ b/vochain/transaction/account_tx.go @@ -29,6 +29,7 @@ func (t *TransactionHandler) CreateAccountTxCheck(vtx *vochaintx.Tx) error { if tx.Txtype != models.TxType_CREATE_ACCOUNT { return fmt.Errorf("invalid tx type, expected %s, got %s", models.TxType_CREATE_ACCOUNT, tx.Txtype) } + // check account does not exist pubKey, err := ethereum.PubKeyFromSignature(vtx.SignedBody, vtx.Signature) if err != nil { return fmt.Errorf("cannot extract public key from vtx.Signature: %w", err) @@ -44,6 +45,7 @@ func (t *TransactionHandler) CreateAccountTxCheck(vtx *vochaintx.Tx) error { if txSenderAcc != nil { return vstate.ErrAccountAlreadyExists } + infoURI := tx.GetInfoURI() if len(infoURI) > types.MaxURLLength { return ErrInvalidURILength @@ -125,20 +127,14 @@ func (t *TransactionHandler) SetAccountDelegateTxCheck(vtx *vochaintx.Tx) error if len(tx.Delegates) == 0 { return fmt.Errorf("invalid delegates") } - txSenderAddress, txSenderAccount, err := t.state.AccountFromSignature(vtx.SignedBody, vtx.Signature) + txSenderAccount, txSenderAddr, err := t.checkAccountCanPayCost(tx.Txtype, vtx) if err != nil { return err } - if err := vstate.CheckDuplicateDelegates(tx.Delegates, txSenderAddress); err != nil { + if err := vstate.CheckDuplicateDelegates(tx.Delegates, txSenderAddr); err != nil { return fmt.Errorf("checkDuplicateDelegates: %w", err) } - cost, err := t.state.TxBaseCost(tx.Txtype, false) - if err != nil { - return fmt.Errorf("cannot get tx cost: %w", err) - } - if txSenderAccount.Balance < cost { - return vstate.ErrNotEnoughBalance - } + switch tx.Txtype { case models.TxType_ADD_DELEGATE_FOR_ACCOUNT: for _, delegate := range tx.Delegates { @@ -171,40 +167,20 @@ func (t *TransactionHandler) SetAccountInfoTxCheck(vtx *vochaintx.Tx) error { if tx == nil { return fmt.Errorf("invalid transaction") } - pubKey, err := ethereum.PubKeyFromSignature(vtx.SignedBody, vtx.Signature) - if err != nil { - return fmt.Errorf("cannot extract public key from vtx.Signature: %w", err) - } - txSenderAddress, err := ethereum.AddrFromPublicKey(pubKey) + txSenderAccount, txSenderAddress, err := t.checkAccountCanPayCost(models.TxType_SET_ACCOUNT_INFO_URI, vtx) if err != nil { - return fmt.Errorf("cannot extract address from public key: %w", err) + return err } txAccountAddress := common.BytesToAddress(tx.GetAccount()) if txAccountAddress == (common.Address{}) { - txAccountAddress = txSenderAddress - } - txSenderAccount, err := t.state.GetAccount(txSenderAddress, false) - if err != nil { - return fmt.Errorf("cannot check if account %s exists: %w", txSenderAddress, err) - } - if txSenderAccount == nil { - return vstate.ErrAccountNotExist - } - // get setAccount tx cost - costSetAccountInfoURI, err := t.state.TxBaseCost(models.TxType_SET_ACCOUNT_INFO_URI, false) - if err != nil { - return fmt.Errorf("cannot get tx cost: %w", err) - } - // check tx sender balance - if txSenderAccount.Balance < costSetAccountInfoURI { - return fmt.Errorf("unauthorized: %s", vstate.ErrNotEnoughBalance) + txAccountAddress = *txSenderAddress } // check info URI infoURI := tx.GetInfoURI() if len(infoURI) == 0 || len(infoURI) > types.MaxURLLength { return fmt.Errorf("invalid URI, cannot be empty") } - if txSenderAddress == txAccountAddress { + if bytes.Equal(txSenderAddress.Bytes(), txAccountAddress.Bytes()) { if infoURI == txSenderAccount.InfoURI { return fmt.Errorf("invalid URI, must be different") } @@ -223,7 +199,7 @@ func (t *TransactionHandler) SetAccountInfoTxCheck(vtx *vochaintx.Tx) error { return fmt.Errorf("invalid URI, must be different") } // check if delegate - if !txAccountAccount.IsDelegate(txSenderAddress) { + if !txAccountAccount.IsDelegate(*txSenderAddress) { return fmt.Errorf("tx sender is not a delegate") } return nil @@ -275,38 +251,29 @@ func (t *TransactionHandler) SetSIKTxCheck(vtx *vochaintx.Tx) (common.Address, v if tx == nil { return common.Address{}, nil, fmt.Errorf("invalid transaction") } - pubKey, err := ethereum.PubKeyFromSignature(vtx.SignedBody, vtx.Signature) + _, txAddress, err := t.checkAccountCanPayCost(models.TxType_SET_ACCOUNT_SIK, vtx) if err != nil { - return common.Address{}, nil, fmt.Errorf("cannot extract public key from vtx.Signature: %w", err) + return common.Address{}, nil, err } - txAddress, err := ethereum.AddrFromPublicKey(pubKey) - if err != nil { - return common.Address{}, nil, fmt.Errorf("cannot extract address from public key: %w", err) - } - - if _, err := t.state.GetAccount(txAddress, false); err != nil { - return common.Address{}, nil, fmt.Errorf("cannot get tx account: %w", err) - } - newSIK := vtx.Tx.GetSetSIK().GetSIK() if newSIK == nil { return common.Address{}, nil, fmt.Errorf("no sik value provided") } // check if the address already has invalidated sik to ensure that it is // not updated after reach the correct height to avoid double voting - if currentSIK, err := t.state.SIKFromAddress(txAddress); err == nil { + if currentSIK, err := t.state.SIKFromAddress(*txAddress); err == nil { maxEndBlock, err := t.state.ProcessBlockRegistry.MaxEndBlock(t.state.CurrentHeight()) if err != nil { if errors.Is(err, arbo.ErrKeyNotFound) { - return txAddress, newSIK, nil + return *txAddress, newSIK, nil } return common.Address{}, nil, err } if height := currentSIK.DecodeInvalidatedHeight(); height >= maxEndBlock { - return txAddress, nil, fmt.Errorf("the sik could not be changed yet") + return *txAddress, nil, fmt.Errorf("the sik could not be changed yet") } } - return txAddress, newSIK, nil + return *txAddress, newSIK, nil } // RegisterSIKTxCheck checks if the provided RegisterSIKTx is valid ensuring @@ -382,3 +349,30 @@ func (t *TransactionHandler) RegisterSIKTxCheck(vtx *vochaintx.Tx) (common.Addre } return txAddress, newSIK, pid, process.GetTempSIKs(), nil } + +// SetAccountValidatorTxCheck upgrades an account to a validator. +func (t *TransactionHandler) SetAccountValidatorTxCheck(vtx *vochaintx.Tx) error { + if vtx == nil || vtx.Signature == nil || vtx.SignedBody == nil || vtx.Tx == nil { + return ErrNilTx + } + _, _, err := t.checkAccountCanPayCost(models.TxType_SET_ACCOUNT_VALIDATOR, vtx) + if err != nil { + return err + } + validatorPubKey := vtx.Tx.GetSetAccount().GetPublicKey() + if validatorPubKey == nil { + return fmt.Errorf("invalid nil public key") + } + validatorAddress, err := ethereum.AddrFromPublicKey(validatorPubKey) + if err != nil { + return fmt.Errorf("cannot extract address from public key: %w", err) + } + validatorAccount, err := t.state.Validator(validatorAddress, false) + if err != nil { + return fmt.Errorf("cannot get validator: %w", err) + } + if validatorAccount != nil { + return fmt.Errorf("account is already a validator") + } + return nil +} diff --git a/vochain/transaction/transaction.go b/vochain/transaction/transaction.go index 6ac7717c1..510e5b06b 100644 --- a/vochain/transaction/transaction.go +++ b/vochain/transaction/transaction.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "fmt" + cometCrypto256k1 "github.com/cometbft/cometbft/crypto/secp256k1" "github.com/ethereum/go-ethereum/common" snarkTypes "github.com/vocdoni/go-snark/types" "go.vocdoni.io/dvote/crypto/ethereum" @@ -16,6 +17,11 @@ import ( "google.golang.org/protobuf/proto" ) +const ( + // newValidatorPower is the default power of a new validator + newValidatorPower = 5 +) + var ( // ErrNilTx is returned if the transaction is nil. ErrNilTx = fmt.Errorf("nil transaction") @@ -209,7 +215,10 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa if err := t.SetAccountDelegateTxCheck(vtx); err != nil { return nil, fmt.Errorf("setAccountDelegateTx: %w", err) } - + case models.TxType_SET_ACCOUNT_VALIDATOR: + if err := t.SetAccountValidatorTxCheck(vtx); err != nil { + return nil, fmt.Errorf("setAccountValidatorTx: %w", err) + } default: return nil, fmt.Errorf("setAccount: invalid transaction type") } @@ -275,7 +284,7 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa case models.TxType_SET_ACCOUNT_INFO_URI: txSenderAddress, err := ethereum.AddrFromSignature(vtx.SignedBody, vtx.Signature) if err != nil { - return nil, fmt.Errorf("createAccountTx: txSenderAddress %w", err) + return nil, fmt.Errorf("setAccountInfo: txSenderAddress %w", err) } // consume cost for setAccount if err := t.state.BurnTxCostIncrementNonce( @@ -284,7 +293,7 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa 0, tx.GetInfoURI(), ); err != nil { - return nil, fmt.Errorf("setAccountTx: burnCostIncrementNonce %w", err) + return nil, fmt.Errorf("setAccountInfo: burnCostIncrementNonce %w", err) } txAccount := common.BytesToAddress(tx.GetAccount()) if txAccount != (common.Address{}) { @@ -301,7 +310,7 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa case models.TxType_ADD_DELEGATE_FOR_ACCOUNT: txSenderAddress, err := ethereum.AddrFromSignature(vtx.SignedBody, vtx.Signature) if err != nil { - return nil, fmt.Errorf("createAccountTx: txSenderAddress %w", err) + return nil, fmt.Errorf("addDelegate: txSenderAddress %w", err) } if err := t.state.BurnTxCostIncrementNonce( txSenderAddress, @@ -309,19 +318,19 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa 0, "", ); err != nil { - return nil, fmt.Errorf("setAccountDelegateTx: burnTxCostIncrementNonce %w", err) + return nil, fmt.Errorf("addDelegate: burnTxCostIncrementNonce %w", err) } if err := t.state.SetAccountDelegate( txSenderAddress, tx.Delegates, models.TxType_ADD_DELEGATE_FOR_ACCOUNT, ); err != nil { - return nil, fmt.Errorf("setAccountDelegateTx: %w", err) + return nil, fmt.Errorf("addDelegate: %w", err) } case models.TxType_DEL_DELEGATE_FOR_ACCOUNT: txSenderAddress, err := ethereum.AddrFromSignature(vtx.SignedBody, vtx.Signature) if err != nil { - return nil, fmt.Errorf("createAccountTx: txSenderAddress %w", err) + return nil, fmt.Errorf("delDelegate: txSenderAddress %w", err) } if err := t.state.BurnTxCostIncrementNonce( txSenderAddress, @@ -329,14 +338,41 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa 0, "", ); err != nil { - return nil, fmt.Errorf("setAccountDelegate: burnTxCostIncrementNonce %w", err) + return nil, fmt.Errorf("delDelegate: burnTxCostIncrementNonce %w", err) } if err := t.state.SetAccountDelegate( txSenderAddress, tx.Delegates, models.TxType_DEL_DELEGATE_FOR_ACCOUNT, ); err != nil { - return nil, fmt.Errorf("setAccountDelegateTx: %w", err) + return nil, fmt.Errorf("delDelegate: %w", err) + } + case models.TxType_SET_ACCOUNT_VALIDATOR: + txSenderAddress, err := ethereum.AddrFromSignature(vtx.SignedBody, vtx.Signature) + if err != nil { + return nil, fmt.Errorf("setValidator: txSenderAddress %w", err) + } + validatorAddr, err := ethereum.AddrFromPublicKey(tx.GetPublicKey()) + if err != nil { + return nil, fmt.Errorf("setValidator: %w", err) + } + if err := t.state.BurnTxCostIncrementNonce( + txSenderAddress, + models.TxType_SET_ACCOUNT_VALIDATOR, + 0, + validatorAddr.Hex(), + ); err != nil { + return nil, fmt.Errorf("setValidator: burnTxCostIncrementNonce %w", err) + } + if err := t.state.AddValidator(&models.Validator{ + Address: validatorAddr.Bytes(), + PubKey: tx.GetPublicKey(), + Name: tx.GetName(), + Power: newValidatorPower, + ValidatorAddress: cometCrypto256k1.PubKey(tx.GetPublicKey()).Address().Bytes(), + Height: uint64(t.state.CurrentHeight()), + }); err != nil { + return nil, fmt.Errorf("setValidator: %w", err) } default: return nil, fmt.Errorf("setAccount: invalid transaction type") @@ -508,3 +544,34 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa return response, nil } + +// checkAccountCanPayCost checks if the account can pay the cost of the transaction. +// It returns the account and the address of the sender. +func (t *TransactionHandler) checkAccountCanPayCost(txType models.TxType, vtx *vochaintx.Tx) (*vstate.Account, *common.Address, error) { + // extract sender address from signature + pubKey, err := ethereum.PubKeyFromSignature(vtx.SignedBody, vtx.Signature) + if err != nil { + return nil, nil, fmt.Errorf("cannot extract public key from vtx.Signature: %w", err) + } + txSenderAddress, err := ethereum.AddrFromPublicKey(pubKey) + if err != nil { + return nil, nil, fmt.Errorf("cannot extract address from public key: %w", err) + } + txSenderAcc, err := t.state.GetAccount(txSenderAddress, false) + if err != nil { + return nil, nil, fmt.Errorf("cannot get account: %w", err) + } + if txSenderAcc == nil { + return nil, nil, vstate.ErrAccountNotExist + } + // get setAccount tx cost + cost, err := t.state.TxBaseCost(txType, false) + if err != nil { + return nil, nil, fmt.Errorf("cannot get tx cost for %s: %w", txType.String(), err) + } + // check tx sender balance + if txSenderAcc.Balance < cost { + return nil, nil, fmt.Errorf("unauthorized: %s", vstate.ErrNotEnoughBalance) + } + return txSenderAcc, &txSenderAddress, nil +}