diff --git a/CHANGELOG.md b/CHANGELOG.md index b022da273..87d99c1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.15.0 + +BREAKING CHANGES + +- [tendermint] Update to [v0.31.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0310) + +IMPROVEMENT + +- [invariants] Add invariants checker each 720 blocks +- [core] Delete coins with 0 reserves #217 +- [genesis] Add option to export/import state +- [api] Add ?include_stakes to /candidates endpoint #222 +- [api] Change `stake` to `value` in DelegateTx +- [api] Change `pubkey` to `pub_key` in all API resources and requests +- [events] Add CoinLiquidation event #221 +- [mempool] Recheck mempool once per minute + +BUG FIXES + +- [core] Fix double sign slashing issue #215 +- [core] Fix issue with slashing small stake #209 +- [core] Fix coin creation issue +- [core] Fix mempool issue #220 +- [api] Make block hash lowercase #214 + ## 0.14.3 BUG FIXES diff --git a/Gopkg.lock b/Gopkg.lock index 102e4c787..5c56e5161 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,11 +25,11 @@ revision = "ed77733ec07dfc8a513741138419b8d9d3de9d2d" [[projects]] - digest = "1:a87ce480584818a6a9672ccd2530f8aa87361c70588ffbde34dafbd267e15bfc" + digest = "1:7ca13b5c032dca181b9e28e7e29e7c09fdefd7cb5eb9ab32e0822dd1f856ccd3" name = "github.com/danil-lashin/iavl" packages = ["."] pruneopts = "UT" - revision = "83a81c94ee7eb3bd0eb14d6829bd04f22e530e6f" + revision = "98552e00562de7d0c62e0e0bf2fcedf5223517c5" [[projects]] digest = "1:58f28ece14b66e78bb3bfbeb806d47ac38b247272bcba8902d67656768d00bb2" @@ -47,14 +47,6 @@ revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" -[[projects]] - digest = "1:fed20bf7f0da387c96d4cfc140a95572e5aba4bb984beb7de910e090ae39849b" - name = "github.com/ethereum/go-ethereum" - packages = ["crypto/secp256k1"] - pruneopts = "UT" - revision = "7fa3509e2eaf1a4ebc12344590e5699406690f15" - version = "v1.8.22" - [[projects]] digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" name = "github.com/fsnotify/fsnotify" @@ -104,7 +96,7 @@ version = "v1.11.1" [[projects]] - digest = "1:35621fe20f140f05a0c4ef662c26c0ab4ee50bca78aa30fe87d33120bd28165e" + digest = "1:95e1006e41c641abd2f365dfa0f1213c04da294e7cd5f0bf983af234b775db64" name = "github.com/gogo/protobuf" packages = [ "gogoproto", @@ -115,11 +107,11 @@ "types", ] pruneopts = "UT" - revision = "636bf0302bc95575d69441b25a2603156ffdddf1" - version = "v1.1.1" + revision = "ba06b47c162d49f2af050fb4c75bcbc86a159d5c" + version = "v1.2.1" [[projects]] - digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260" + digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9" name = "github.com/golang/protobuf" packages = [ "proto", @@ -129,8 +121,8 @@ "ptypes/timestamp", ] pruneopts = "UT" - revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" - version = "v1.1.0" + revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30" + version = "v1.3.1" [[projects]] branch = "master" @@ -168,12 +160,12 @@ version = "v1.0.0" [[projects]] - branch = "master" - digest = "1:39b27d1381a30421f9813967a5866fba35dc1d4df43a6eefe3b7a5444cb07214" + digest = "1:a74b5a8e34ee5843cd6e65f698f3e75614f812ff170c2243425d75bc091e9af2" name = "github.com/jmhodges/levigo" packages = ["."] pruneopts = "UT" - revision = "c42d9e0ca023e2198120196f842701bb4c55d7b9" + revision = "853d788c5c416eaaee5b044570784a96c7a26975" + version = "v1.0.0" [[projects]] branch = "master" @@ -391,7 +383,7 @@ version = "v0.14.1" [[projects]] - digest = "1:a8544db2d244d8c51fd1991ef4daaa78e80c3ee23c1d0f4a99e77f4df3ad0fe8" + digest = "1:76fbe1f3b51f6bd7014bbf9c6c4427180be62b1715278993e9235d7d4552629f" name = "github.com/tendermint/tendermint" packages = [ "abci/client", @@ -410,6 +402,7 @@ "crypto/multisig", "crypto/multisig/bitarray", "crypto/secp256k1", + "crypto/secp256k1/internal/secp256k1", "crypto/tmhash", "evidence", "libs/autofile", @@ -446,8 +439,8 @@ "version", ] pruneopts = "UT" - revision = "e0f8936455029a40287a69d5b0e7baa4d5864da1" - version = "v0.30.1" + revision = "0d985ede28bd6937fa9d3613618e42cab6fc871c" + version = "v0.31.0" [[projects]] digest = "1:b6621a5e9003d7d809993d49217a841d27ef85b4bc0459115d3fd1f8c1518999" @@ -599,14 +592,15 @@ "github.com/tendermint/tendermint/config", "github.com/tendermint/tendermint/crypto", "github.com/tendermint/tendermint/crypto/ed25519", + "github.com/tendermint/tendermint/crypto/encoding/amino", "github.com/tendermint/tendermint/crypto/multisig", "github.com/tendermint/tendermint/crypto/secp256k1", + "github.com/tendermint/tendermint/evidence", "github.com/tendermint/tendermint/libs/cli/flags", "github.com/tendermint/tendermint/libs/common", "github.com/tendermint/tendermint/libs/db", "github.com/tendermint/tendermint/libs/log", "github.com/tendermint/tendermint/libs/pubsub", - "github.com/tendermint/tendermint/libs/pubsub/query", "github.com/tendermint/tendermint/node", "github.com/tendermint/tendermint/p2p", "github.com/tendermint/tendermint/privval", diff --git a/Gopkg.toml b/Gopkg.toml index 84361a4e4..d3d1a0438 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -12,11 +12,11 @@ [[constraint]] name = "github.com/danil-lashin/iavl" - revision = "83a81c94ee7eb3bd0eb14d6829bd04f22e530e6f" + revision = "98552e00562de7d0c62e0e0bf2fcedf5223517c5" [[constraint]] name = "github.com/tendermint/tendermint" - version = "0.30.1" + version = "0.31.0" [[constraint]] name = "github.com/MinterTeam/go-amino" diff --git a/api/api.go b/api/api.go index d7eb3d24d..9754ef582 100644 --- a/api/api.go +++ b/api/api.go @@ -32,8 +32,8 @@ var ( var Routes = map[string]*rpcserver.RPCFunc{ "status": rpcserver.NewRPCFunc(Status, ""), - "candidates": rpcserver.NewRPCFunc(Candidates, "height"), - "candidate": rpcserver.NewRPCFunc(Candidate, "pubkey,height"), + "candidates": rpcserver.NewRPCFunc(Candidates, "height,include_stakes"), + "candidate": rpcserver.NewRPCFunc(Candidate, "pub_key,height"), "validators": rpcserver.NewRPCFunc(Validators, "height"), "address": rpcserver.NewRPCFunc(Address, "address,height"), "addresses": rpcserver.NewRPCFunc(Addresses, "addresses,height"), @@ -122,7 +122,7 @@ type Response struct { func GetStateForHeight(height int) (*state.StateDB, error) { if height > 0 { - cState, err := blockchain.GetStateForHeight(height) + cState, err := blockchain.GetStateForHeight(uint64(height)) return cState, err } diff --git a/api/block.go b/api/block.go index e680e3c51..41b5d72a2 100644 --- a/api/block.go +++ b/api/block.go @@ -2,20 +2,20 @@ package api import ( "bytes" + "encoding/hex" "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "github.com/tendermint/tendermint/libs/common" types2 "github.com/tendermint/tendermint/types" "math/big" "time" ) type BlockResponse struct { - Hash common.HexBytes `json:"hash"` + Hash string `json:"hash"` Height int64 `json:"height"` Time time.Time `json:"time"` NumTxs int64 `json:"num_txs"` @@ -46,7 +46,7 @@ type BlockTransactionResponse struct { } type BlockValidatorResponse struct { - Pubkey string `json:"pubkey"` + Pubkey string `json:"pub_key"` Signed bool `json:"signed"` } @@ -136,7 +136,7 @@ func Block(height int64) (*BlockResponse, error) { } return &BlockResponse{ - Hash: block.Block.Hash(), + Hash: hex.EncodeToString(block.Block.Hash()), Height: block.Block.Height, Time: block.Block.Time, NumTxs: block.Block.NumTxs, diff --git a/api/candidate.go b/api/candidate.go index 968d92b8d..0ef58edb7 100644 --- a/api/candidate.go +++ b/api/candidate.go @@ -18,7 +18,7 @@ type CandidateResponse struct { RewardAddress types.Address `json:"reward_address"` OwnerAddress types.Address `json:"owner_address"` TotalStake *big.Int `json:"total_stake"` - PubKey types.Pubkey `json:"pubkey"` + PubKey types.Pubkey `json:"pub_key"` Commission uint `json:"commission"` Stakes []Stake `json:"stakes,omitempty"` CreatedAtBlock uint `json:"created_at_block"` diff --git a/api/candidates.go b/api/candidates.go index 5c519b36c..962c9230e 100644 --- a/api/candidates.go +++ b/api/candidates.go @@ -1,6 +1,6 @@ package api -func Candidates(height int) (*[]CandidateResponse, error) { +func Candidates(height int, includeStakes bool) (*[]CandidateResponse, error) { cState, err := GetStateForHeight(height) if err != nil { return nil, err @@ -10,7 +10,7 @@ func Candidates(height int) (*[]CandidateResponse, error) { result := make([]CandidateResponse, len(candidates)) for i, candidate := range candidates { - result[i] = makeResponseCandidate(candidate, false) + result[i] = makeResponseCandidate(candidate, includeStakes) } return &result, nil diff --git a/api/events.go b/api/events.go index b7377ffde..dd98a221e 100644 --- a/api/events.go +++ b/api/events.go @@ -8,7 +8,7 @@ type EventsResponse struct { Events eventsdb.Events `json:"events"` } -func Events(height int64) (*EventsResponse, error) { +func Events(height uint64) (*EventsResponse, error) { return &EventsResponse{ Events: eventsdb.NewEventsDB(eventsdb.GetCurrentDB()).LoadEvents(height), }, nil diff --git a/api/transaction.go b/api/transaction.go index b0b9a3195..8be35e4ee 100644 --- a/api/transaction.go +++ b/api/transaction.go @@ -13,7 +13,7 @@ func Transaction(hash []byte) (*TransactionResponse, error) { return nil, err } - if tx.Height > blockchain.LastCommittedHeight() { + if uint64(tx.Height) > blockchain.LastCommittedHeight() { return nil, rpctypes.RPCError{Code: 404, Message: "Tx not found"} } diff --git a/api/validators.go b/api/validators.go index 9cb3c56d5..47fc8d379 100644 --- a/api/validators.go +++ b/api/validators.go @@ -5,18 +5,19 @@ import ( ) type ValidatorResponse struct { - Pubkey types.Pubkey `json:"pubkey"` + Pubkey types.Pubkey `json:"pub_key"` VotingPower int64 `json:"voting_power"` } type ResponseValidators []ValidatorResponse -func Validators(height int64) (*ResponseValidators, error) { +func Validators(height uint64) (*ResponseValidators, error) { if height == 0 { height = blockchain.Height() } - tmVals, err := client.Validators(&height) + h := int64(height) + tmVals, err := client.Validators(&h) if err != nil { return nil, err } diff --git a/cmd/export/main.go b/cmd/export/main.go new file mode 100644 index 000000000..6b50dadf8 --- /dev/null +++ b/cmd/export/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "github.com/MinterTeam/go-amino" + "github.com/MinterTeam/minter-go-node/cmd/utils" + "github.com/MinterTeam/minter-go-node/core/appdb" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/db" +) + +func main() { + err := common.EnsureDir(utils.GetMinterHome()+"/config", 0777) + if err != nil { + panic(err) + } + + ldb, err := db.NewGoLevelDB("state", utils.GetMinterHome()+"/data") + if err != nil { + panic(err) + } + + applicationDB := appdb.NewAppDB() + height := applicationDB.GetLastHeight() + currentState, err := state.New(height, ldb) + if err != nil { + panic(err) + } + + cdc := amino.NewCodec() + + jsonBytes, err := cdc.MarshalJSONIndent(currentState.Export(height), "", " ") + if err != nil { + panic(err) + } + + println(string(jsonBytes)) +} diff --git a/cmd/make_genesis/main.go b/cmd/make_genesis/main.go new file mode 100644 index 000000000..9596ae0bc --- /dev/null +++ b/cmd/make_genesis/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "encoding/json" + "github.com/MinterTeam/minter-go-node/genesis" +) + +func main() { + gen, _ := genesis.GetTestnetGenesis() + genesisJson, _ := json.MarshalIndent(gen, "", " ") + println(string(genesisJson)) +} diff --git a/cmd/minter/main.go b/cmd/minter/main.go index 4e2e5520d..e8e121e43 100644 --- a/cmd/minter/main.go +++ b/cmd/minter/main.go @@ -19,6 +19,7 @@ import ( "github.com/tendermint/tendermint/proxy" rpc "github.com/tendermint/tendermint/rpc/client" "os" + "time" ) var cfg = config.GetConfig() @@ -42,11 +43,11 @@ func main() { panic(err) } blockStore := bc.NewBlockStore(blockStoreDB) - height := blockStore.Height() - count := int64(3) + height := uint64(blockStore.Height()) + count := uint64(3) if _, err := app.GetBlocksTimeDelta(height, count); height >= 20 && err != nil { - blockA := blockStore.LoadBlockMeta(height - count - 1) - blockB := blockStore.LoadBlockMeta(height - 1) + blockA := blockStore.LoadBlockMeta(int64(height - count - 1)) + blockB := blockStore.LoadBlockMeta(int64(height - 1)) delta := int(blockB.Header.Time.Sub(blockA.Header.Time).Seconds()) app.SetBlocksTimeDelta(height, delta) @@ -70,8 +71,24 @@ func main() { go gui.Run(cfg.GUIListenAddress) } - // Wait forever - common.TrapSignal(func() { + // Recheck mempool. Currently kind a hack. TODO: refactor + go func() { + ticker := time.NewTicker(time.Minute) + mempool := node.MempoolReactor().Mempool + for { + select { + case <-ticker.C: + txs := mempool.ReapMaxTxs(cfg.Mempool.Size) + mempool.Flush() + + for _, tx := range txs { + _ = mempool.CheckTx(tx, func(res *types.Response) {}) + } + } + } + }() + + common.TrapSignal(log.With("module", "trap"), func() { // Cleanup err := node.Stop() app.Stop() @@ -79,6 +96,9 @@ func main() { panic(err) } }) + + // Run forever + select {} } func startTendermintNode(app types.Application, cfg *tmCfg.Config) *tmNode.Node { diff --git a/config/config.go b/config/config.go index 3effe990d..987445252 100644 --- a/config/config.go +++ b/config/config.go @@ -53,7 +53,7 @@ func init() { func DefaultConfig() *Config { cfg := defaultConfig() - cfg.P2P.Seeds = "647e32df3b9c54809b5aca2877d9ba60900bc2d9@minter-node-1.testnet.minter.network:26656" + cfg.P2P.Seeds = "647e32df3b9c54809b5aca2877d9ba60900bc2d9@minter-node-1.testnet.minter.network:26656,d20522aa7ba4af8139749c5e724063c4ba18c58b@minter-node-2.testnet.minter.network,249c62818bf4601605a65b5adc35278236bd5312@minter-node-3.testnet.minter.network,b698b07f13f2210dfc82967bfa2a127d1cdfdc54@minter-node-4.testnet.minter.network" cfg.TxIndex = &tmConfig.TxIndexConfig{ Indexer: "kv", @@ -64,7 +64,7 @@ func DefaultConfig() *Config { cfg.DBPath = "tmdata" cfg.Mempool.CacheSize = 100000 - cfg.Mempool.Recheck = true + cfg.Mempool.Recheck = false cfg.Mempool.Size = 10000 cfg.Consensus.WalPath = "tmdata/cs.wal/wal" @@ -254,10 +254,6 @@ type BaseConfig struct { APISimultaneousRequests int `mapstructure:"api_simultaneous_requests"` - APIPerIPLimit int `mapstructure:"api_per_ip_limit"` - - APIPerIPLimitWindow time.Duration `mapstructure:"api_per_ip_limit_window"` - LogPath string `mapstructure:"log_path"` } diff --git a/core/appdb/appdb.go b/core/appdb/appdb.go index 14c85debe..d0fdf61b0 100644 --- a/core/appdb/appdb.go +++ b/core/appdb/appdb.go @@ -43,20 +43,20 @@ func (appDB *AppDB) SetLastBlockHash(hash []byte) { appDB.db.Set([]byte(hashPath), hash) } -func (appDB *AppDB) GetLastHeight() int64 { +func (appDB *AppDB) GetLastHeight() uint64 { result := appDB.db.Get([]byte(heightPath)) - var height int64 + var height uint64 if result != nil { - height = int64(binary.BigEndian.Uint64(result)) + height = binary.BigEndian.Uint64(result) } return height } -func (appDB *AppDB) SetLastHeight(height int64) { +func (appDB *AppDB) SetLastHeight(height uint64) { h := make([]byte, 8) - binary.BigEndian.PutUint64(h, uint64(height)) + binary.BigEndian.PutUint64(h, height) appDB.db.Set([]byte(heightPath), h) } @@ -89,11 +89,11 @@ func (appDB *AppDB) SaveValidators(vals types.ValidatorUpdates) { } type LastBlocksTimeDelta struct { - Height int64 + Height uint64 Delta int } -func (appDB *AppDB) GetLastBlocksTimeDelta(height int64) (int, error) { +func (appDB *AppDB) GetLastBlocksTimeDelta(height uint64) (int, error) { result := appDB.db.Get([]byte(blockTimeDeltaPath)) if result == nil { return 0, errors.New("no info about LastBlocksTimeDelta is available") @@ -112,7 +112,7 @@ func (appDB *AppDB) GetLastBlocksTimeDelta(height int64) (int, error) { return data.Delta, nil } -func (appDB *AppDB) SetLastBlocksTimeDelta(height int64, delta int) { +func (appDB *AppDB) SetLastBlocksTimeDelta(height uint64, delta int) { data, err := cdc.MarshalBinaryBare(LastBlocksTimeDelta{ Height: height, Delta: delta, diff --git a/core/minter/minter.go b/core/minter/minter.go index f8c2e9b1c..e4c824b9d 100644 --- a/core/minter/minter.go +++ b/core/minter/minter.go @@ -2,21 +2,21 @@ package minter import ( "bytes" - "encoding/json" - "fmt" + "github.com/MinterTeam/go-amino" "github.com/MinterTeam/minter-go-node/cmd/utils" "github.com/MinterTeam/minter-go-node/core/appdb" - "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/core/validators" "github.com/MinterTeam/minter-go-node/eventsdb" - "github.com/MinterTeam/minter-go-node/genesis" + "github.com/MinterTeam/minter-go-node/log" "github.com/MinterTeam/minter-go-node/version" "github.com/danil-lashin/tendermint/rpc/lib/types" abciTypes "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/libs/db" tmNode "github.com/tendermint/tendermint/node" types2 "github.com/tendermint/tendermint/types" @@ -48,8 +48,8 @@ type Blockchain struct { appDB *appdb.AppDB stateDeliver *state.StateDB stateCheck *state.StateDB - height int64 // current Blockchain height - lastCommittedHeight int64 // Blockchain.height updated in the at begin of block processing, while + height uint64 // current Blockchain height + lastCommittedHeight uint64 // Blockchain.height updated in the at begin of block processing, while // lastCommittedHeight updated at the end of block processing rewards *big.Int // Rewards pool validatorsStatuses map[[20]byte]int8 @@ -58,7 +58,7 @@ type Blockchain struct { tmNode *tmNode.Node // currentMempool is responsive for prevent sending multiple transactions from one address in one block - currentMempool map[types.Address]struct{} + currentMempool sync.Map lock sync.RWMutex wg sync.WaitGroup // wg is used for graceful node shutdown @@ -80,11 +80,11 @@ func NewMinterBlockchain() *Blockchain { appDB: applicationDB, height: applicationDB.GetLastHeight(), lastCommittedHeight: applicationDB.GetLastHeight(), - currentMempool: map[types.Address]struct{}{}, + currentMempool: sync.Map{}, } // Set stateDeliver and stateCheck - blockchain.stateDeliver, err = state.New(int64(blockchain.height), blockchain.stateDB) + blockchain.stateDeliver, err = state.New(blockchain.height, blockchain.stateDB) if err != nil { panic(err) } @@ -96,52 +96,36 @@ func NewMinterBlockchain() *Blockchain { // Initialize blockchain with validators and other info. Only called once. func (app *Blockchain) InitChain(req abciTypes.RequestInitChain) abciTypes.ResponseInitChain { - var genesisState genesis.AppState - err := json.Unmarshal(req.AppStateBytes, &genesisState) - if err != nil { + var genesisState types.AppState + if err := amino.UnmarshalJSON(req.AppStateBytes, &genesisState); err != nil { panic(err) } - // Filling genesis accounts with given amount of coins - for _, account := range genesisState.InitialBalances { - for coin, value := range account.Balance { - bigIntValue, success := big.NewInt(0).SetString(value, 10) - if !success { - panic(fmt.Sprintf("%s is not a corrent int", value)) - } + app.stateDeliver.Import(genesisState) - coinSymbol := types.StrToCoinSymbol(coin) - app.stateDeliver.SetBalance(account.Address, coinSymbol, bigIntValue) - } + totalPower := big.NewInt(0) + for _, val := range genesisState.Validators { + totalPower.Add(totalPower, val.TotalBipStake) } - // Set initial Blockchain validators - commission := uint(100) - currentBlock := uint(1) - initialStake := big.NewInt(1) // 1 pip - for _, validator := range req.Validators { - app.stateDeliver.CreateCandidate(genesisState.FirstValidatorAddress, genesisState.FirstValidatorAddress, - validator.PubKey.Data, commission, currentBlock, types.GetBaseCoin(), initialStake) - app.stateDeliver.CreateValidator(genesisState.FirstValidatorAddress, validator.PubKey.Data, commission, - currentBlock, types.GetBaseCoin(), initialStake) - app.stateDeliver.SetCandidateOnline(validator.PubKey.Data) - } + vals := make([]abciTypes.ValidatorUpdate, len(genesisState.Validators)) + for i, val := range genesisState.Validators { + var validatorPubKey ed25519.PubKeyEd25519 + copy(validatorPubKey[:], val.PubKey) + pkey, err := cryptoAmino.PubKeyFromBytes(validatorPubKey.Bytes()) + if err != nil { + panic(err) + } - app.stateDeliver.SetMaxGas(DefaultMaxGas) + vals[i] = abciTypes.ValidatorUpdate{ + PubKey: types2.TM2PB.PubKey(pkey), + Power: big.NewInt(0).Div(big.NewInt(0).Mul(val.TotalBipStake, + big.NewInt(100000000)), totalPower).Int64(), + } + } return abciTypes.ResponseInitChain{ - ConsensusParams: &abciTypes.ConsensusParams{ - BlockSize: &abciTypes.BlockSizeParams{ - MaxBytes: BlockMaxBytes, - MaxGas: DefaultMaxGas, - }, - Evidence: &abciTypes.EvidenceParams{ - MaxAge: 1000, - }, - Validator: &abciTypes.ValidatorParams{ - PubKeyTypes: []string{types2.ABCIPubKeyTypeEd25519}, - }, - }, + Validators: vals, } } @@ -152,12 +136,14 @@ func (app *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.Res panic("Application stopped") } + height := uint64(req.Header.Height) + // compute max gas - app.updateBlocksTimeDelta(req.Header.Height, 3) - maxGas := app.calcMaxGas(req.Header.Height) + app.updateBlocksTimeDelta(height, 3) + maxGas := app.calcMaxGas(height) app.stateDeliver.SetMaxGas(maxGas) - atomic.StoreInt64(&app.height, req.Header.Height) + atomic.StoreUint64(&app.height, height) app.rewards = big.NewInt(0) // clear absent candidates @@ -169,10 +155,10 @@ func (app *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.Res copy(address[:], v.Validator.Address) if v.SignedLastBlock { - app.stateDeliver.SetValidatorPresent(req.Header.Height, address) + app.stateDeliver.SetValidatorPresent(address) app.validatorsStatuses[address] = ValidatorPresent } else { - app.stateDeliver.SetValidatorAbsent(req.Header.Height, address) + app.stateDeliver.SetValidatorAbsent(address) app.validatorsStatuses[address] = ValidatorAbsent } } @@ -182,16 +168,21 @@ func (app *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.Res var address [20]byte copy(address[:], byzVal.Validator.Address) - // TODO: switch places - app.stateDeliver.PunishByzantineValidator(req.Header.Height, address) - app.stateDeliver.PunishFrozenFundsWithAddress(uint64(req.Header.Height), uint64(req.Header.Height+518400), address) + // skip already offline candidates to prevent double punishing + candidate := app.stateDeliver.GetStateCandidateByTmAddress(address) + if candidate == nil && candidate.Status == state.CandidateStatusOffline { + continue + } + + app.stateDeliver.PunishFrozenFundsWithAddress(height, height+state.UnbondPeriod, address) + app.stateDeliver.PunishByzantineValidator(address) } // apply frozen funds (used for unbond stakes) frozenFunds := app.stateDeliver.GetStateFrozenFunds(uint64(req.Header.Height)) if frozenFunds != nil { for _, item := range frozenFunds.List() { - eventsdb.GetCurrent().AddEvent(req.Header.Height, eventsdb.UnbondEvent{ + eventsdb.GetCurrent().AddEvent(uint64(req.Header.Height), eventsdb.UnbondEvent{ Address: item.Address, Amount: item.Value.Bytes(), Coin: item.Coin, @@ -209,6 +200,8 @@ func (app *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.Res // Signals the end of a block, returns changes to the validator set func (app *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.ResponseEndBlock { + height := uint64(req.Height) + var updates []abciTypes.ValidatorUpdate stateValidators := app.stateDeliver.GetStateValidators() @@ -235,7 +228,7 @@ func (app *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.Respons continue } - reward := rewards.GetRewardForBlock(uint64(req.Height)) + reward := rewards.GetRewardForBlock(height) reward.Add(reward, app.rewards) @@ -250,7 +243,7 @@ func (app *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.Respons // pay rewards if req.Height%12 == 0 { - app.stateDeliver.PayRewards(req.Height) + app.stateDeliver.PayRewards() } hasDroppedValidators := false @@ -265,13 +258,20 @@ func (app *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.Respons if req.Height%120 == 0 || hasDroppedValidators { app.stateDeliver.RecalculateTotalStakeValues() - app.stateDeliver.ClearCandidates(req.Height) - app.stateDeliver.ClearStakes(req.Height) + app.stateDeliver.ClearCandidates() + app.stateDeliver.ClearStakes() - valsCount := validators.GetValidatorsCountForBlock(req.Height) + valsCount := validators.GetValidatorsCountForBlock(height) newCandidates := app.stateDeliver.GetCandidates(valsCount, req.Height) + // remove candidates with 0 total stake + for i, candidate := range newCandidates { + if candidate.TotalBipStake.Cmp(big.NewInt(0)) != 1 { + newCandidates = append(newCandidates[:i], newCandidates[i+1:]...) + } + } + if len(newCandidates) < valsCount { valsCount = len(newCandidates) } @@ -323,12 +323,10 @@ func (app *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.Respons } } - _ = eventsdb.GetCurrent().FlushEvents(req.Height) - return abciTypes.ResponseEndBlock{ ValidatorUpdates: updates, ConsensusParamUpdates: &abciTypes.ConsensusParams{ - BlockSize: &abciTypes.BlockSizeParams{ + Block: &abciTypes.BlockParams{ MaxBytes: BlockMaxBytes, MaxGas: int64(app.stateDeliver.GetMaxGas()), }, @@ -341,14 +339,14 @@ func (app *Blockchain) Info(req abciTypes.RequestInfo) (resInfo abciTypes.Respon return abciTypes.ResponseInfo{ Version: version.Version, AppVersion: version.AppVer, - LastBlockHeight: app.appDB.GetLastHeight(), + LastBlockHeight: int64(app.appDB.GetLastHeight()), LastBlockAppHash: app.appDB.GetLastBlockHash(), } } // Deliver a tx for full processing func (app *Blockchain) DeliverTx(rawTx []byte) abciTypes.ResponseDeliverTx { - response := transaction.RunTx(app.stateDeliver, false, rawTx, app.rewards, app.height, nil) + response := transaction.RunTx(app.stateDeliver, false, rawTx, app.rewards, app.height, sync.Map{}, nil) return abciTypes.ResponseDeliverTx{ Code: response.Code, @@ -363,14 +361,7 @@ func (app *Blockchain) DeliverTx(rawTx []byte) abciTypes.ResponseDeliverTx { // Validate a tx for the mempool func (app *Blockchain) CheckTx(rawTx []byte) abciTypes.ResponseCheckTx { - response := transaction.RunTx(app.stateCheck, true, rawTx, nil, app.height, app.currentMempool) - - if response.Code == code.OK && response.GasPrice.Cmp(app.MinGasPrice()) == -1 { - return abciTypes.ResponseCheckTx{ - Code: code.TooLowGasPrice, - Log: fmt.Sprintf("Gas price of tx is too low to be included in mempool. Expected %s", app.MinGasPrice().String()), - } - } + response := transaction.RunTx(app.stateCheck, true, rawTx, nil, app.height, app.currentMempool, app.MinGasPrice()) return abciTypes.ResponseCheckTx{ Code: response.Code, @@ -391,6 +382,9 @@ func (app *Blockchain) Commit() abciTypes.ResponseCommit { panic(err) } + // Flush events db + _ = eventsdb.GetCurrent().FlushEvents() + // Persist application hash and height app.appDB.SetLastBlockHash(hash) app.appDB.SetLastHeight(app.height) @@ -399,10 +393,17 @@ func (app *Blockchain) Commit() abciTypes.ResponseCommit { app.resetCheckState() // Update LastCommittedHeight - atomic.StoreInt64(&app.lastCommittedHeight, app.Height()) + atomic.StoreUint64(&app.lastCommittedHeight, app.Height()) // Clear mempool - app.currentMempool = map[types.Address]struct{}{} + app.currentMempool = sync.Map{} + + // Check invariants + if app.height%720 == 0 { + if err := state.NewForCheck(app.stateCheck).CheckForInvariants(); err != nil { + log.With("module", "invariants").Error("Invariants error", "msg", err.Error()) + } + } // Releasing wg app.wg.Done() @@ -440,11 +441,11 @@ func (app *Blockchain) CurrentState() *state.StateDB { } // Get immutable state of Minter Blockchain for given height -func (app *Blockchain) GetStateForHeight(height int) (*state.StateDB, error) { +func (app *Blockchain) GetStateForHeight(height uint64) (*state.StateDB, error) { app.lock.RLock() defer app.lock.RUnlock() - s, err := state.New(int64(height), app.stateDB) + s, err := state.New(height, app.stateDB) if err != nil { return nil, rpctypes.RPCError{Code: 404, Message: "State at given height not found", Data: err.Error()} } @@ -453,13 +454,13 @@ func (app *Blockchain) GetStateForHeight(height int) (*state.StateDB, error) { } // Get current height of Minter Blockchain -func (app *Blockchain) Height() int64 { - return atomic.LoadInt64(&app.height) +func (app *Blockchain) Height() uint64 { + return atomic.LoadUint64(&app.height) } // Get last committed height of Minter Blockchain -func (app *Blockchain) LastCommittedHeight() int64 { - return atomic.LoadInt64(&app.lastCommittedHeight) +func (app *Blockchain) LastCommittedHeight() uint64 { + return atomic.LoadUint64(&app.lastCommittedHeight) } // Set Tendermint node @@ -505,34 +506,34 @@ func (app *Blockchain) saveCurrentValidators(vals abciTypes.ValidatorUpdates) { app.appDB.SaveValidators(vals) } -func (app *Blockchain) updateBlocksTimeDelta(height, count int64) { +func (app *Blockchain) updateBlocksTimeDelta(height uint64, count int64) { // should do this because tmNode is unavailable during Tendermint's replay mode if app.tmNode == nil { return } - if height-count-1 < 1 { + if int64(height)-count-1 < 1 { return } blockStore := app.tmNode.BlockStore() - blockA := blockStore.LoadBlockMeta(height - count - 1) - blockB := blockStore.LoadBlockMeta(height - 1) + blockA := blockStore.LoadBlockMeta(int64(height) - count - 1) + blockB := blockStore.LoadBlockMeta(int64(height) - 1) delta := int(blockB.Header.Time.Sub(blockA.Header.Time).Seconds()) app.appDB.SetLastBlocksTimeDelta(height, delta) } -func (app *Blockchain) SetBlocksTimeDelta(height int64, value int) { +func (app *Blockchain) SetBlocksTimeDelta(height uint64, value int) { app.appDB.SetLastBlocksTimeDelta(height, value) } -func (app *Blockchain) GetBlocksTimeDelta(height, count int64) (int, error) { +func (app *Blockchain) GetBlocksTimeDelta(height, count uint64) (int, error) { return app.appDB.GetLastBlocksTimeDelta(height) } -func (app *Blockchain) calcMaxGas(height int64) uint64 { +func (app *Blockchain) calcMaxGas(height uint64) uint64 { const targetTime = 7 const blockDelta = 3 diff --git a/core/minter/minter_test.go b/core/minter/minter_test.go index a77ee57e3..767db82d5 100644 --- a/core/minter/minter_test.go +++ b/core/minter/minter_test.go @@ -3,9 +3,11 @@ package minter import ( "context" "crypto/ecdsa" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" + "github.com/MinterTeam/go-amino" "github.com/MinterTeam/minter-go-node/cmd/utils" "github.com/MinterTeam/minter-go-node/config" "github.com/MinterTeam/minter-go-node/core/transaction" @@ -16,7 +18,7 @@ import ( "github.com/MinterTeam/minter-go-node/log" "github.com/MinterTeam/minter-go-node/rlp" tmConfig "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/libs/pubsub/query" + log2 "github.com/tendermint/tendermint/libs/log" tmNode "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/privval" @@ -27,6 +29,7 @@ import ( "math/big" "os" "path/filepath" + "sync" "testing" "time" ) @@ -34,9 +37,18 @@ import ( var pv *privval.FilePV var cfg *tmConfig.Config var client *rpc.Local +var app *Blockchain var privateKey *ecdsa.PrivateKey +var l sync.Mutex +var nonce = uint64(1) func init() { + l.Lock() + go initNode() + l.Lock() +} + +func initNode() { *utils.MinterHome = os.ExpandEnv(filepath.Join("$HOME", ".minter_test")) _ = os.RemoveAll(*utils.MinterHome) @@ -44,7 +56,13 @@ func init() { cfg.Consensus.TimeoutPropose = 0 cfg.Consensus.TimeoutPrecommit = 0 cfg.Consensus.TimeoutPrevote = 0 + cfg.Consensus.TimeoutCommit = 0 + cfg.Consensus.TimeoutPrecommitDelta = 0 + cfg.Consensus.TimeoutPrevoteDelta = 0 + cfg.Consensus.TimeoutProposeDelta = 0 cfg.Consensus.SkipTimeoutCommit = true + cfg.P2P.Seeds = "" + cfg.P2P.PersistentPeers = "" pv = privval.GenFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()) pv.Save() @@ -52,7 +70,7 @@ func init() { b, _ := hex.DecodeString("825ca965c34ef1c8343e8e377959108370c23ba6194d858452b63432456403f9") privateKey, _ = crypto.ToECDSA(b) - app := NewMinterBlockchain() + app = NewMinterBlockchain() nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) if err != nil { panic(err) @@ -66,7 +84,7 @@ func init() { getGenesis, tmNode.DefaultDBProvider, tmNode.DefaultMetricsProvider(cfg.Instrumentation), - log.With("module", "tendermint"), + log2.NewTMLogger(os.Stdout), ) if err != nil { @@ -80,12 +98,12 @@ func init() { log.Info("Started node", "nodeInfo", node.Switch().NodeInfo()) app.SetTmNode(node) client = rpc.NewLocal(node) + l.Unlock() } func TestBlocksCreation(t *testing.T) { // Wait for blocks - blocks := make(chan interface{}) - err := client.Subscribe(context.TODO(), "test-client", query.MustParse("tm.event = 'NewBlock'"), blocks) + blocks, err := client.Subscribe(context.TODO(), "test-client", "tm.event = 'NewBlock'") if err != nil { panic(err) } @@ -123,13 +141,14 @@ func TestSendTx(t *testing.T) { } tx := transaction.Transaction{ - Nonce: 1, + Nonce: nonce, GasPrice: big.NewInt(1), GasCoin: types.GetBaseCoin(), Type: transaction.TypeSend, Data: encodedData, SignatureType: transaction.SigTypeSingle, } + nonce++ if err := tx.Sign(privateKey); err != nil { t.Fatal(err) @@ -146,8 +165,7 @@ func TestSendTx(t *testing.T) { t.Fatalf("CheckTx code is not 0: %d", res.Code) } - txs := make(chan interface{}) - err = client.Subscribe(context.TODO(), "test-client", query.MustParse(fmt.Sprintf("tm.event = 'Tx'")), txs) + txs, err := client.Subscribe(context.TODO(), "test-client", "tm.event = 'Tx'") if err != nil { panic(err) } @@ -165,40 +183,221 @@ func TestSendTx(t *testing.T) { } } -func getGenesis() (*types2.GenesisDoc, error) { - validators := []types2.GenesisValidator{ - { - PubKey: pv.Key.PubKey, - Power: 100000000, - }, +// TODO: refactor +func TestSmallStakeValidator(t *testing.T) { + for blockchain.Height() < 2 { + time.Sleep(time.Millisecond) + } + + pubkey := types.Pubkey{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + + data := transaction.DeclareCandidacyData{ + Address: crypto.PubkeyToAddress(privateKey.PublicKey), + PubKey: pubkey, + Commission: 10, + Coin: types.GetBaseCoin(), + Stake: big.NewInt(1), + } + + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := transaction.Transaction{ + Nonce: nonce, + GasPrice: big.NewInt(1), + GasCoin: types.GetBaseCoin(), + Type: transaction.TypeDeclareCandidacy, + Data: encodedData, + SignatureType: transaction.SigTypeSingle, + } + nonce++ + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + txBytes, _ := tx.Serialize() + res, err := client.BroadcastTxSync(txBytes) + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + if res.Code != 0 { + t.Fatalf("CheckTx code is not 0: %d", res.Code) + } + + time.Sleep(time.Second) + + setOnData := transaction.SetCandidateOnData{ + PubKey: pubkey, + } + + encodedData, err = rlp.EncodeToBytes(setOnData) + if err != nil { + t.Fatal(err) + } + + tx = transaction.Transaction{ + Nonce: nonce, + GasPrice: big.NewInt(1), + GasCoin: types.GetBaseCoin(), + Type: transaction.TypeSetCandidateOnline, + Data: encodedData, + SignatureType: transaction.SigTypeSingle, + } + nonce++ + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + txBytes, _ = tx.Serialize() + res, err = client.BroadcastTxSync(txBytes) + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + if res.Code != 0 { + t.Fatalf("CheckTx code is not 0: %d", res.Code) + } + + status, _ := client.Status() + targetBlockHeight := status.SyncInfo.LatestBlockHeight - (status.SyncInfo.LatestBlockHeight % 120) + 150 + println("target block", targetBlockHeight) + + blocks, err := client.Subscribe(context.TODO(), "test-client", "tm.event = 'NewBlock'") + if err != nil { + panic(err) } + ready := false + for !ready { + select { + case block := <-blocks: + if block.Data.(types2.EventDataNewBlock).Block.Height < targetBlockHeight { + continue + } + + vals, _ := client.Validators(&targetBlockHeight) + + if len(vals.Validators) > 1 { + t.Errorf("There are should be 1 validator (has %d)", len(vals.Validators)) + } + + if len(app.stateDeliver.GetStateValidators().Data()) > 1 { + t.Errorf("There are should be 1 validator (has %d)", len(app.stateDeliver.GetStateValidators().Data())) + } + + ready = true + case <-time.After(10 * time.Second): + t.Fatalf("Timeout waiting for the block") + } + } + err = client.UnsubscribeAll(context.TODO(), "test-client") + if err != nil { + panic(err) + } + + time.Sleep(time.Second) + + encodedData, err = rlp.EncodeToBytes(setOnData) + if err != nil { + t.Fatal(err) + } + + tx = transaction.Transaction{ + Nonce: nonce, + GasPrice: big.NewInt(1), + GasCoin: types.GetBaseCoin(), + Type: transaction.TypeSetCandidateOnline, + Data: encodedData, + SignatureType: transaction.SigTypeSingle, + } + nonce++ + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + txBytes, _ = tx.Serialize() + res, err = client.BroadcastTxSync(txBytes) + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + if res.Code != 0 { + t.Fatalf("CheckTx code is not 0: %d", res.Code) + } + + status, _ = client.Status() + targetBlockHeight = status.SyncInfo.LatestBlockHeight - (status.SyncInfo.LatestBlockHeight % 120) + 120 + 5 + println("target block", targetBlockHeight) + + blocks, err = client.Subscribe(context.TODO(), "test-client", "tm.event = 'NewBlock'") + if err != nil { + panic(err) + } + +FORLOOP2: + for { + select { + case block := <-blocks: + if block.Data.(types2.EventDataNewBlock).Block.Height < targetBlockHeight { + continue FORLOOP2 + } + + vals, _ := client.Validators(&targetBlockHeight) + + if len(vals.Validators) > 1 { + t.Errorf("There are should be only 1 validator") + } + + if len(app.stateDeliver.GetStateValidators().Data()) > 1 { + t.Errorf("There are should be only 1 validator") + } + + break FORLOOP2 + case <-time.After(10 * time.Second): + t.Fatalf("Timeout waiting for the block") + } + } + + err = client.UnsubscribeAll(context.TODO(), "test-client") + if err != nil { + panic(err) + } +} + +func getGenesis() (*types2.GenesisDoc, error) { appHash := [32]byte{} - appState := genesis.AppState{ - FirstValidatorAddress: crypto.PubkeyToAddress(privateKey.PublicKey), - InitialBalances: []genesis.Account{ + validators, candidates := genesis.MakeValidatorsAndCandidates([]string{base64.StdEncoding.EncodeToString(pv.Key.PubKey.Bytes()[5:])}, big.NewInt(10000000)) + + appState := types.AppState{ + Accounts: []types.Account{ { Address: crypto.PubkeyToAddress(privateKey.PublicKey), - Balance: map[string]string{ - "MNT": helpers.BipToPip(big.NewInt(100000000)).String(), + Balance: []types.Balance{ + { + Coin: types.GetBaseCoin(), + Value: helpers.BipToPip(big.NewInt(1000000)), + }, }, }, }, + Validators: validators, + Candidates: candidates, } - appStateJSON, err := json.Marshal(appState) + appStateJSON, err := amino.MarshalJSON(appState) if err != nil { return nil, err } genesisDoc := types2.GenesisDoc{ - ChainID: "minter-test-network", - GenesisTime: time.Now(), - ConsensusParams: nil, - Validators: validators, - AppHash: appHash[:], - AppState: json.RawMessage(appStateJSON), + ChainID: "minter-test-network", + GenesisTime: time.Now(), + AppHash: appHash[:], + AppState: json.RawMessage(appStateJSON), } err = genesisDoc.ValidateAndComplete() diff --git a/core/state/state_coin.go b/core/state/state_coin.go index cef492ea2..9556321aa 100644 --- a/core/state/state_coin.go +++ b/core/state/state_coin.go @@ -5,17 +5,15 @@ import ( "fmt" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/crypto" "github.com/MinterTeam/minter-go-node/rlp" "math/big" ) // stateCoin represents a coin which is being modified. type stateCoin struct { - symbol types.CoinSymbol - symbolHash types.Hash - data Coin - db *StateDB + symbol types.CoinSymbol + data Coin + db *StateDB onDirty func(symbol types.CoinSymbol) // Callback method to mark a state coin newly dirty } @@ -36,11 +34,10 @@ func (coin Coin) String() string { // newCoin creates a state coin. func newCoin(db *StateDB, symbol types.CoinSymbol, data Coin, onDirty func(symbol types.CoinSymbol)) *stateCoin { coin := &stateCoin{ - db: db, - symbol: symbol, - symbolHash: crypto.Keccak256Hash(symbol[:]), - data: data, - onDirty: onDirty, + db: db, + symbol: symbol, + data: data, + onDirty: onDirty, } coin.onDirty(coin.symbol) diff --git a/core/state/state_frozen_fund.go b/core/state/state_frozen_fund.go index 81c17aa0b..221974ad6 100644 --- a/core/state/state_frozen_fund.go +++ b/core/state/state_frozen_fund.go @@ -113,9 +113,13 @@ func (c *stateFrozenFund) punishFund(context *StateDB, candidateAddress [20]byte context.SubCoinVolume(coin.Symbol, slashed) context.SubCoinReserve(coin.Symbol, ret) + + context.AddTotalSlashed(ret) + } else { + context.AddTotalSlashed(slashed) } - edb.AddEvent(int64(fromBlock), eventsdb.SlashEvent{ + edb.AddEvent(fromBlock, eventsdb.SlashEvent{ Address: item.Address, Amount: slashed.Bytes(), Coin: item.Coin, @@ -123,6 +127,7 @@ func (c *stateFrozenFund) punishFund(context *StateDB, candidateAddress [20]byte }) item.Value = newValue + context.DeleteCoinIfZeroReserve(item.Coin) } newList[i] = item diff --git a/core/state/state_validator.go b/core/state/state_validator.go index 8ac082b5e..f8f43b228 100644 --- a/core/state/state_validator.go +++ b/core/state/state_validator.go @@ -30,7 +30,7 @@ type Validator struct { PubKey types.Pubkey Commission uint AccumReward *big.Int - AbsentTimes *BitArray + AbsentTimes *types.BitArray tmAddress *[20]byte toDrop bool diff --git a/core/state/statedb.go b/core/state/statedb.go index 221108117..c1a8f996d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1,8 +1,10 @@ package state import ( + "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/config" + "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/core/validators" "github.com/MinterTeam/minter-go-node/eventsdb" @@ -35,6 +37,7 @@ var ( candidatesKey = []byte("t") validatorsKey = []byte("v") maxGasKey = []byte("g") + totalSlashedKey = []byte("s") cfg = config.GetConfig() ) @@ -43,6 +46,8 @@ type StateDB struct { db dbm.DB iavl Tree + height uint64 + // This map holds 'live' objects, which will get modified while processing a state transition. stateAccounts map[types.Address]*stateAccount stateAccountsDirty map[types.Address]struct{} @@ -59,6 +64,9 @@ type StateDB struct { stateValidators *stateValidators stateValidatorsDirty bool + totalSlashed *big.Int + totalSlashedDirty bool + stakeCache map[types.CoinSymbol]StakeCache lock sync.Mutex @@ -72,6 +80,7 @@ type StakeCache struct { func NewForCheck(s *StateDB) *StateDB { return &StateDB{ db: s.db, + height: s.height, iavl: s.iavl.GetImmutable(), stateAccounts: make(map[types.Address]*stateAccount), stateAccountsDirty: make(map[types.Address]struct{}), @@ -81,14 +90,18 @@ func NewForCheck(s *StateDB) *StateDB { stateFrozenFundsDirty: make(map[uint64]struct{}), stateCandidates: nil, stateCandidatesDirty: false, + stateValidators: nil, + stateValidatorsDirty: false, + totalSlashed: nil, + totalSlashedDirty: false, stakeCache: make(map[types.CoinSymbol]StakeCache), } } -func New(height int64, db dbm.DB) (*StateDB, error) { +func New(height uint64, db dbm.DB) (*StateDB, error) { tree := NewMutableTree(db) - _, err := tree.LoadVersion(height) + _, err := tree.LoadVersion(int64(height)) if err != nil { return nil, err @@ -96,6 +109,7 @@ func New(height int64, db dbm.DB) (*StateDB, error) { return &StateDB{ db: db, + height: height + 1, iavl: tree, stateAccounts: make(map[types.Address]*stateAccount), stateAccountsDirty: make(map[types.Address]struct{}), @@ -105,6 +119,10 @@ func New(height int64, db dbm.DB) (*StateDB, error) { stateFrozenFundsDirty: make(map[uint64]struct{}), stateCandidates: nil, stateCandidatesDirty: false, + stateValidators: nil, + stateValidatorsDirty: false, + totalSlashed: nil, + totalSlashedDirty: false, stakeCache: make(map[types.CoinSymbol]StakeCache), }, nil } @@ -118,10 +136,35 @@ func (s *StateDB) Clear() { s.stateFrozenFundsDirty = make(map[uint64]struct{}) s.stateCandidates = nil s.stateCandidatesDirty = false + s.stateValidators = nil + s.stateValidatorsDirty = false + s.totalSlashed = nil + s.totalSlashedDirty = false s.stakeCache = make(map[types.CoinSymbol]StakeCache) s.lock = sync.Mutex{} } +func (s *StateDB) GetTotalSlashed() *big.Int { + // Prefer 'live' object. + if s.totalSlashed != nil { + return s.totalSlashed + } + + // Load the object from the database. + _, enc := s.iavl.Get(totalSlashedKey) + if len(enc) == 0 { + return big.NewInt(0) + } + var data *big.Int + if err := rlp.DecodeBytes(enc, &data); err != nil { + log.Error("Failed to decode total slashed", "err", err) + return nil + } + + s.setTotalSlashed(data) + return data +} + // Retrieve the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr types.Address, coinSymbol types.CoinSymbol) *big.Int { stateObject := s.getStateAccount(addr) @@ -241,6 +284,15 @@ func (s *StateDB) updateStateValidators(validators *stateValidators) { s.iavl.Set(validatorsKey, data) } +func (s *StateDB) updateTotalSlashed(value *big.Int) { + data, err := rlp.EncodeToBytes(value) + if err != nil { + panic(fmt.Errorf("can't encode total slashed: %v", err)) + } + + s.iavl.Set(totalSlashedKey, data) +} + // deleteStateObject removes the given object from the state trie. func (s *StateDB) deleteStateObject(stateObject *stateAccount) { stateObject.deleted = true @@ -252,6 +304,9 @@ func (s *StateDB) deleteStateObject(stateObject *stateAccount) { // deleteStateCoin removes the given object from the state trie. func (s *StateDB) deleteStateCoin(stateCoin *stateCoin) { symbol := stateCoin.Symbol() + eventsdb.GetCurrent().AddEvent(s.height, eventsdb.CoinLiquidationEvent{ + Coin: symbol, + }) s.iavl.Remove(append(coinPrefix, symbol[:]...)) } @@ -398,6 +453,21 @@ func (s *StateDB) setStateObject(object *stateAccount) { s.stateAccounts[object.Address()] = object } +func (s *StateDB) setTotalSlashed(object *big.Int) { + s.lock.Lock() + defer s.lock.Unlock() + + s.totalSlashed = object +} + +func (s *StateDB) AddTotalSlashed(value *big.Int) { + current := s.GetTotalSlashed() + current.Add(current, value) + + s.setTotalSlashed(current) + s.totalSlashedDirty = true +} + func (s *StateDB) setStateCoin(coin *stateCoin) { s.lock.Lock() defer s.lock.Unlock() @@ -545,7 +615,7 @@ func (s *StateDB) CreateValidator( PubKey: pubkey, Commission: commission, AccumReward: big.NewInt(0), - AbsentTimes: NewBitArray(ValidatorMaxAbsentWindow), + AbsentTimes: types.NewBitArray(ValidatorMaxAbsentWindow), }) s.MarkStateValidatorsDirty() @@ -575,9 +645,10 @@ func (s *StateDB) CreateCandidate( Commission: commission, Stakes: []Stake{ { - Owner: rewardAddress, - Coin: coin, - Value: initialStake, + Owner: rewardAddress, + Coin: coin, + Value: initialStake, + BipValue: big.NewInt(0), }, }, CreatedAtBlock: currentBlock, @@ -642,6 +713,11 @@ func (s *StateDB) Commit() (root []byte, version int64, err error) { s.stateValidatorsDirty = false } + if s.totalSlashedDirty { + s.updateTotalSlashed(s.totalSlashed) + s.totalSlashedDirty = false + } + hash, version, err := s.iavl.SaveVersion() if !cfg.KeepStateHistory && version > 1 { @@ -653,6 +729,7 @@ func (s *StateDB) Commit() (root []byte, version int64, err error) { } s.Clear() + s.height++ return hash, version, err } @@ -726,7 +803,6 @@ func (s *StateDB) CandidateExists(key types.Pubkey) bool { func (s *StateDB) GetStateCandidate(key types.Pubkey) *Candidate { stateCandidates := s.getStateCandidates() - if stateCandidates == nil { return nil } @@ -740,6 +816,21 @@ func (s *StateDB) GetStateCandidate(key types.Pubkey) *Candidate { return nil } +func (s *StateDB) GetStateCandidateByTmAddress(address [20]byte) *Candidate { + stateCandidates := s.getStateCandidates() + if stateCandidates == nil { + return nil + } + + for i, candidate := range stateCandidates.data { + if candidate.GetAddress() == address { + return &(stateCandidates.data[i]) + } + } + + return nil +} + func (s *StateDB) GetStateCoin(symbol types.CoinSymbol) *stateCoin { return s.getStateCoin(symbol) } @@ -834,7 +925,7 @@ func (s *StateDB) AddAccumReward(pubkey types.Pubkey, reward *big.Int) { } } -func (s *StateDB) PayRewards(height int64) { +func (s *StateDB) PayRewards() { edb := eventsdb.GetCurrent() validators := s.getStateValidators() @@ -851,7 +942,7 @@ func (s *StateDB) PayRewards(height int64) { DAOReward.Mul(DAOReward, big.NewInt(int64(dao.Commission))) DAOReward.Div(DAOReward, big.NewInt(100)) s.AddBalance(dao.Address, types.GetBaseCoin(), DAOReward) - edb.AddEvent(height, eventsdb.RewardEvent{ + edb.AddEvent(s.height, eventsdb.RewardEvent{ Role: eventsdb.RoleDAO, Address: dao.Address, Amount: DAOReward.Bytes(), @@ -863,7 +954,7 @@ func (s *StateDB) PayRewards(height int64) { DevelopersReward.Mul(DevelopersReward, big.NewInt(int64(developers.Commission))) DevelopersReward.Div(DevelopersReward, big.NewInt(100)) s.AddBalance(developers.Address, types.GetBaseCoin(), DevelopersReward) - edb.AddEvent(height, eventsdb.RewardEvent{ + edb.AddEvent(s.height, eventsdb.RewardEvent{ Role: eventsdb.RoleDevelopers, Address: developers.Address, Amount: DevelopersReward.Bytes(), @@ -879,7 +970,7 @@ func (s *StateDB) PayRewards(height int64) { validatorReward.Div(validatorReward, big.NewInt(100)) totalReward.Sub(totalReward, validatorReward) s.AddBalance(validator.RewardAddress, types.GetBaseCoin(), validatorReward) - edb.AddEvent(height, eventsdb.RewardEvent{ + edb.AddEvent(s.height, eventsdb.RewardEvent{ Role: eventsdb.RoleValidator, Address: validator.RewardAddress, Amount: validatorReward.Bytes(), @@ -892,7 +983,7 @@ func (s *StateDB) PayRewards(height int64) { for j := range candidate.Stakes { stake := candidate.Stakes[j] - if stake.BipValue == nil { + if stake.BipValue.Cmp(big.NewInt(0)) == 0 { continue } @@ -907,7 +998,7 @@ func (s *StateDB) PayRewards(height int64) { s.AddBalance(stake.Owner, types.GetBaseCoin(), reward) - edb.AddEvent(height, eventsdb.RewardEvent{ + edb.AddEvent(s.height, eventsdb.RewardEvent{ Role: eventsdb.RoleDelegator, Address: stake.Owner, Amount: reward.Bytes(), @@ -974,9 +1065,10 @@ func (s *StateDB) Delegate(sender types.Address, pubkey []byte, coin types.CoinS if !exists { candidate.Stakes = append(candidate.Stakes, Stake{ - Owner: sender, - Coin: coin, - Value: value, + Owner: sender, + Coin: coin, + Value: value, + BipValue: big.NewInt(0), }) } } @@ -1010,7 +1102,11 @@ func (s *StateDB) IsCheckUsed(check *check.Check) bool { func (s *StateDB) UseCheck(check *check.Check) { checkHash := check.Hash().Bytes() - trieHash := append(usedCheckPrefix, checkHash...) + s.useCheckHash(checkHash) +} + +func (s *StateDB) useCheckHash(hash []byte) { + trieHash := append(usedCheckPrefix, hash...) s.iavl.Set(trieHash, []byte{0x1}) } @@ -1080,13 +1176,13 @@ func (s *StateDB) SetCandidateOffline(pubkey []byte) { s.MarkStateValidatorsDirty() } -func (s *StateDB) SetValidatorPresent(height int64, address [20]byte) { +func (s *StateDB) SetValidatorPresent(address [20]byte) { validators := s.getStateValidators() for i := range validators.data { validator := &validators.data[i] if validator.GetAddress() == address { - validator.AbsentTimes.SetIndex(int(height)%ValidatorMaxAbsentWindow, false) + validator.AbsentTimes.SetIndex(int(s.height)%ValidatorMaxAbsentWindow, false) } } @@ -1094,7 +1190,7 @@ func (s *StateDB) SetValidatorPresent(height int64, address [20]byte) { s.MarkStateValidatorsDirty() } -func (s *StateDB) SetValidatorAbsent(height int64, address [20]byte) { +func (s *StateDB) SetValidatorAbsent(address [20]byte) { edb := eventsdb.GetCurrent() validators := s.getStateValidators() @@ -1117,11 +1213,11 @@ func (s *StateDB) SetValidatorAbsent(height int64, address [20]byte) { return } - validator.AbsentTimes.SetIndex(int(height)%ValidatorMaxAbsentWindow, true) + validator.AbsentTimes.SetIndex(int(s.height)%ValidatorMaxAbsentWindow, true) if validator.CountAbsentTimes() > ValidatorMaxAbsentTimes { candidate.Status = CandidateStatusOffline - validator.AbsentTimes = NewBitArray(ValidatorMaxAbsentWindow) + validator.AbsentTimes = types.NewBitArray(ValidatorMaxAbsentWindow) validator.toDrop = true totalStake := big.NewInt(0) @@ -1140,9 +1236,14 @@ func (s *StateDB) SetValidatorAbsent(height int64, address [20]byte) { s.SubCoinVolume(coin.Symbol, slashed) s.SubCoinReserve(coin.Symbol, ret) + s.DeleteCoinIfZeroReserve(stake.Coin) + + s.AddTotalSlashed(ret) + } else { + s.AddTotalSlashed(slashed) } - edb.AddEvent(height, eventsdb.SlashEvent{ + edb.AddEvent(s.height, eventsdb.SlashEvent{ Address: stake.Owner, Amount: slashed.Bytes(), Coin: stake.Coin, @@ -1150,9 +1251,10 @@ func (s *StateDB) SetValidatorAbsent(height int64, address [20]byte) { }) candidate.Stakes[j] = Stake{ - Owner: stake.Owner, - Coin: stake.Coin, - Value: newValue, + Owner: stake.Owner, + Coin: stake.Coin, + Value: newValue, + BipValue: big.NewInt(0), } totalStake.Add(totalStake, newValue) } @@ -1169,7 +1271,7 @@ func (s *StateDB) SetValidatorAbsent(height int64, address [20]byte) { s.MarkStateValidatorsDirty() } -func (s *StateDB) PunishByzantineValidator(currentBlock int64, address [20]byte) { +func (s *StateDB) PunishByzantineValidator(address [20]byte) { edb := eventsdb.GetCurrent() vals := s.getStateValidators() @@ -1203,25 +1305,29 @@ func (s *StateDB) PunishByzantineValidator(currentBlock int64, address [20]byte) s.SubCoinVolume(coin.Symbol, slashed) s.SubCoinReserve(coin.Symbol, ret) + + s.AddTotalSlashed(ret) + } else { + s.AddTotalSlashed(slashed) } - edb.AddEvent(int64(currentBlock), eventsdb.SlashEvent{ + edb.AddEvent(s.height, eventsdb.SlashEvent{ Address: stake.Owner, Amount: slashed.Bytes(), Coin: stake.Coin, ValidatorPubKey: candidate.PubKey, }) - s.GetOrNewStateFrozenFunds(uint64(currentBlock+UnbondPeriod)).AddFund(stake.Owner, candidate.PubKey, + s.GetOrNewStateFrozenFunds(s.height+UnbondPeriod).AddFund(stake.Owner, candidate.PubKey, stake.Coin, newValue) + s.DeleteCoinIfZeroReserve(stake.Coin) } candidate.Stakes = []Stake{} candidate.Status = CandidateStatusOffline validator.AccumReward = big.NewInt(0) validator.TotalBipStake = big.NewInt(0) - // TODO: uncomment - //validator.toDrop = true + validator.toDrop = true s.setStateCandidates(candidates) s.MarkStateCandidateDirty() @@ -1251,7 +1357,7 @@ func (s *StateDB) SetNewValidators(candidates []Candidate) { for _, candidate := range candidates { accumReward := big.NewInt(0) - absentTimes := NewBitArray(ValidatorMaxAbsentWindow) + absentTimes := types.NewBitArray(ValidatorMaxAbsentWindow) for _, oldVal := range oldVals.data { if oldVal.GetAddress() == candidate.GetAddress() { @@ -1336,8 +1442,9 @@ func (s *StateDB) MultisigAccountExists(address types.Address) bool { func (s *StateDB) IsNewCandidateStakeSufficient(coinSymbol types.CoinSymbol, stake *big.Int) bool { bipValue := (&Stake{ - Coin: coinSymbol, - Value: stake, + Coin: coinSymbol, + Value: stake, + BipValue: big.NewInt(0), }).CalcBipValue(s) candidates := s.getStateCandidates() @@ -1361,8 +1468,8 @@ func (s *StateDB) CandidatesCount() int { return len(candidates.data) } -func (s *StateDB) ClearCandidates(height int64) { - maxCandidates := validators.GetCandidatesCountForBlock(height) +func (s *StateDB) ClearCandidates() { + maxCandidates := validators.GetCandidatesCountForBlock(s.height) candidates := s.getStateCandidates() @@ -1375,7 +1482,7 @@ func (s *StateDB) ClearCandidates(height int64) { dropped := candidates.data[maxCandidates:] candidates.data = candidates.data[:maxCandidates] - unbondAtBlock := uint64(height + UnbondPeriod) + unbondAtBlock := s.height + UnbondPeriod for _, candidate := range dropped { for _, stake := range candidate.Stakes { s.GetOrNewStateFrozenFunds(unbondAtBlock).AddFund(stake.Owner, candidate.PubKey, stake.Coin, stake.Value) @@ -1387,7 +1494,7 @@ func (s *StateDB) ClearCandidates(height int64) { s.MarkStateCandidateDirty() } -func (s *StateDB) ClearStakes(height int64) { +func (s *StateDB) ClearStakes() { candidates := s.getStateCandidates() for i := range candidates.data { @@ -1402,7 +1509,7 @@ func (s *StateDB) ClearStakes(height int64) { candidates.data[i].Stakes = candidates.data[i].Stakes[:MaxDelegatorsPerCandidate] for _, stake := range dropped { - eventsdb.GetCurrent().AddEvent(height, eventsdb.UnbondEvent{ + eventsdb.GetCurrent().AddEvent(s.height, eventsdb.UnbondEvent{ Address: stake.Owner, Amount: stake.Value.Bytes(), Coin: stake.Coin, @@ -1424,8 +1531,9 @@ func (s *StateDB) IsDelegatorStakeSufficient(sender types.Address, pubKey []byte } bipValue := (&Stake{ - Coin: coinSymbol, - Value: value, + Coin: coinSymbol, + Value: value, + BipValue: big.NewInt(0), }).CalcBipValue(s) candidates := s.getStateCandidates() @@ -1433,11 +1541,6 @@ func (s *StateDB) IsDelegatorStakeSufficient(sender types.Address, pubKey []byte for _, candidate := range candidates.data { if bytes.Equal(candidate.PubKey, pubKey) { for _, stake := range candidate.Stakes[:MaxDelegatorsPerCandidate] { - // TODO: delete at v0.15.0 - if stake.BipValue == nil { - continue - } - if stake.BipValue.Cmp(bipValue) == -1 { return true } @@ -1486,3 +1589,358 @@ func (s *StateDB) GetMaxGas() uint64 { return binary.BigEndian.Uint64(b) } + +func (s *StateDB) DeleteCoinIfZeroReserve(symbol types.CoinSymbol) { + if symbol.IsBaseCoin() { + return + } + + coin := s.GetStateCoin(symbol) + if coin.ReserveBalance().Cmp(big.NewInt(0)) == 0 { + s.deleteCoin(symbol) + } +} + +func (s *StateDB) deleteCoin(symbol types.CoinSymbol) { + s.iavl.Iterate(func(key []byte, value []byte) bool { + // remove coin from accounts + if key[0] == addressPrefix[0] { + account := s.GetOrNewStateObject(types.BytesToAddress(key[1:])) + for _, coin := range account.Balances().getCoins() { + if coin == symbol { + account.SetBalance(symbol, big.NewInt(0)) + } + } + } + + // remove coin from frozen funds + if key[0] == frozenFundsPrefix[0] { + frozenFunds := s.GetStateFrozenFunds(binary.BigEndian.Uint64(key[1:])) + + for i, ff := range frozenFunds.data.List { + if ff.Coin == symbol { + frozenFunds.data.List = append(frozenFunds.data.List[:i], frozenFunds.data.List[i+1:]...) + } + } + } + + return false + }) + + // remove coin from stakes + candidates := s.getStateCandidates() + if candidates != nil { + for i := range candidates.data { + candidate := &candidates.data[i] + for j, stake := range candidate.Stakes { + if stake.Coin == symbol { + candidate.Stakes[j].Value = big.NewInt(0) + } + } + } + s.setStateCandidates(candidates) + s.MarkStateCandidateDirty() + } + + // set coin volume to 0 + s.SubCoinVolume(symbol, s.GetStateCoin(symbol).Volume()) +} + +func (s *StateDB) Export(currentHeight uint64) types.AppState { + appState := types.AppState{} + + s.iavl.Iterate(func(key []byte, value []byte) bool { + // export accounts + if key[0] == addressPrefix[0] { + account := s.GetOrNewStateObject(types.BytesToAddress(key[1:])) + + balance := make([]types.Balance, len(account.Balances().Data)) + i := 0 + for coin, value := range account.Balances().Data { + balance[i] = types.Balance{ + Coin: coin, + Value: value, + } + i++ + } + + acc := types.Account{ + Address: account.address, + Balance: balance, + Nonce: account.data.Nonce, + } + + if account.IsMultisig() { + acc.MultisigData = &types.Multisig{ + Weights: account.data.MultisigData.Weights, + Threshold: account.data.MultisigData.Threshold, + Addresses: account.data.MultisigData.Addresses, + } + } + + appState.Accounts = append(appState.Accounts, acc) + } + + // export coins + if key[0] == coinPrefix[0] { + coin := s.GetStateCoin(types.StrToCoinSymbol(string(key[1:]))) + + appState.Coins = append(appState.Coins, types.Coin{ + Name: coin.Name(), + Symbol: coin.Symbol(), + Volume: coin.Volume(), + Crr: coin.Crr(), + ReserveBalance: coin.ReserveBalance(), + }) + } + + // export used checks + if key[0] == usedCheckPrefix[0] { + appState.UsedChecks = append(appState.UsedChecks, types.UsedCheck(fmt.Sprintf("%x", key[1:]))) + } + + // export frozen funds + if key[0] == frozenFundsPrefix[0] { + height := binary.BigEndian.Uint64(key[1:]) + frozenFunds := s.GetStateFrozenFunds(height) + + for _, frozenFund := range frozenFunds.List() { + appState.FrozenFunds = append(appState.FrozenFunds, types.FrozenFund{ + Height: height - uint64(currentHeight), + Address: frozenFund.Address, + CandidateKey: frozenFund.CandidateKey, + Coin: frozenFund.Coin, + Value: frozenFund.Value, + }) + } + } + + return false + }) + + candidates := s.getStateCandidates() + for _, candidate := range candidates.data { + var stakes []types.Stake + for _, s := range candidate.Stakes { + stakes = append(stakes, types.Stake{ + Owner: s.Owner, + Coin: s.Coin, + Value: s.Value, + BipValue: s.BipValue, + }) + } + + appState.Candidates = append(appState.Candidates, types.Candidate{ + RewardAddress: candidate.RewardAddress, + OwnerAddress: candidate.OwnerAddress, + TotalBipStake: candidate.TotalBipStake, + PubKey: candidate.PubKey, + Commission: candidate.Commission, + Stakes: stakes, + CreatedAtBlock: candidate.CreatedAtBlock, + Status: candidate.Status, + }) + } + + vals := s.getStateValidators() + for _, val := range vals.data { + appState.Validators = append(appState.Validators, types.Validator{ + RewardAddress: val.RewardAddress, + TotalBipStake: val.TotalBipStake, + PubKey: val.PubKey, + Commission: val.Commission, + AccumReward: val.AccumReward, + AbsentTimes: val.AbsentTimes, + }) + } + + appState.MaxGas = s.GetMaxGas() + + return appState +} + +func (s *StateDB) Import(appState types.AppState) { + s.SetMaxGas(appState.MaxGas) + + for _, a := range appState.Accounts { + account := s.GetOrNewStateObject(a.Address) + + account.data.Nonce = a.Nonce + + if a.MultisigData != nil { + account.data.MultisigData.Addresses = a.MultisigData.Addresses + account.data.MultisigData.Threshold = a.MultisigData.Threshold + account.data.MultisigData.Weights = a.MultisigData.Weights + } + + for _, b := range a.Balance { + account.SetBalance(b.Coin, b.Value) + } + + s.setStateObject(account) + s.MarkStateObjectDirty(a.Address) + } + + for _, c := range appState.Coins { + s.CreateCoin(c.Symbol, c.Name, c.Volume, c.Crr, c.ReserveBalance) + } + + vals := &stateValidators{} + for _, v := range appState.Validators { + vals.data = append(vals.data, Validator{ + RewardAddress: v.RewardAddress, + TotalBipStake: v.TotalBipStake, + PubKey: v.PubKey, + Commission: v.Commission, + AccumReward: v.AccumReward, + AbsentTimes: v.AbsentTimes, + }) + } + s.SetStateValidators(vals) + s.MarkStateValidatorsDirty() + + cands := &stateCandidates{} + for _, c := range appState.Candidates { + stakes := make([]Stake, len(c.Stakes)) + for i, stake := range c.Stakes { + stakes[i] = Stake{ + Owner: stake.Owner, + Coin: stake.Coin, + Value: stake.Value, + BipValue: stake.BipValue, + } + } + cands.data = append(cands.data, Candidate{ + RewardAddress: c.RewardAddress, + OwnerAddress: c.OwnerAddress, + TotalBipStake: c.TotalBipStake, + PubKey: c.PubKey, + Commission: c.Commission, + Stakes: stakes, + CreatedAtBlock: 1, + Status: c.Status, + }) + } + s.setStateCandidates(cands) + s.MarkStateCandidateDirty() + + for _, hashString := range appState.UsedChecks { + hash, _ := hex.DecodeString(string(hashString)) + s.useCheckHash(hash) + } + + for _, ff := range appState.FrozenFunds { + frozenFunds := s.GetOrNewStateFrozenFunds(ff.Height) + frozenFunds.AddFund(ff.Address, ff.CandidateKey, ff.Coin, ff.Value) + s.setStateFrozenFunds(frozenFunds) + } +} + +func (s *StateDB) CheckForInvariants() error { + height := s.height - 1 + + totalBasecoinVolume := big.NewInt(0) + + coinSupplies := map[types.CoinSymbol]*big.Int{} + coinTotalOwned := map[types.CoinSymbol]*big.Int{} + + s.iavl.Iterate(func(key []byte, value []byte) bool { + if key[0] == addressPrefix[0] { + account := s.GetOrNewStateObject(types.BytesToAddress(key[1:])) + + for coin, value := range account.Balances().Data { + if coin.IsBaseCoin() { + totalBasecoinVolume.Add(totalBasecoinVolume, value) + continue + } + + if coinTotalOwned[coin] == nil { + coinTotalOwned[coin] = big.NewInt(0) + } + coinTotalOwned[coin].Add(coinTotalOwned[coin], value) + } + + } + + if key[0] == coinPrefix[0] { + coin := s.GetStateCoin(types.StrToCoinSymbol(string(key[1:]))) + + totalBasecoinVolume.Add(totalBasecoinVolume, coin.ReserveBalance()) + coinSupplies[coin.symbol] = coin.Volume() + } + + if key[0] == frozenFundsPrefix[0] { + height := binary.BigEndian.Uint64(key[1:]) + frozenFunds := s.GetStateFrozenFunds(height) + + for _, frozenFund := range frozenFunds.List() { + if frozenFund.Coin.IsBaseCoin() { + totalBasecoinVolume.Add(totalBasecoinVolume, frozenFund.Value) + continue + } + + if coinTotalOwned[frozenFund.Coin] == nil { + coinTotalOwned[frozenFund.Coin] = big.NewInt(0) + } + coinTotalOwned[frozenFund.Coin].Add(coinTotalOwned[frozenFund.Coin], frozenFund.Value) + } + } + + return false + }) + + candidates := s.getStateCandidates() + if candsCount := len(candidates.data); candsCount > validators.GetCandidatesCountForBlock(height) { + return fmt.Errorf("too many candidates in blockchain. Expected %d, got %d", + validators.GetCandidatesCountForBlock(height), candsCount) + } + + for _, candidate := range candidates.data { + for _, stake := range candidate.Stakes { + if stake.Coin.IsBaseCoin() { + totalBasecoinVolume.Add(totalBasecoinVolume, stake.Value) + continue + } + + if coinTotalOwned[stake.Coin] == nil { + coinTotalOwned[stake.Coin] = big.NewInt(0) + } + coinTotalOwned[stake.Coin].Add(coinTotalOwned[stake.Coin], stake.Value) + } + } + + vals := s.getStateValidators() + if valsCount := len(vals.data); valsCount > validators.GetValidatorsCountForBlock(height) { + return fmt.Errorf("too many validators in blockchain. Expected %d, got %d", + validators.GetValidatorsCountForBlock(height), valsCount) + } + + for _, val := range vals.data { + totalBasecoinVolume.Add(totalBasecoinVolume, val.AccumReward) + } + + predictedBasecoinVolume := big.NewInt(0) + for i := uint64(1); i < height; i++ { + predictedBasecoinVolume.Add(predictedBasecoinVolume, rewards.GetRewardForBlock(i)) + } + predictedBasecoinVolume.Sub(predictedBasecoinVolume, s.GetTotalSlashed()) + + // TODO: compute from genesis + GenesisAlloc, _ := big.NewInt(0).SetString("200000000000000000000000000", 10) + predictedBasecoinVolume.Add(predictedBasecoinVolume, GenesisAlloc) + + delta := big.NewInt(0).Abs(big.NewInt(0).Sub(predictedBasecoinVolume, totalBasecoinVolume)) + if delta.Cmp(big.NewInt(1000000000)) == 1 { + return fmt.Errorf("smth wrong with total base coins in blockchain. Expected total supply to be %s, got %s", + predictedBasecoinVolume, totalBasecoinVolume) + } + + for coin, volume := range coinSupplies { + if volume.Cmp(coinTotalOwned[coin]) != 0 { + return fmt.Errorf("smth wrong with %s coin in blockchain. Total supply (%s) does not match total owned (%s)", + coin, volume, coinTotalOwned[coin]) + } + } + + return nil +} diff --git a/core/state/tree.go b/core/state/tree.go index 1ca82539f..42ae6b762 100644 --- a/core/state/tree.go +++ b/core/state/tree.go @@ -17,6 +17,7 @@ type Tree interface { GetImmutable() *ImmutableTree Version() int64 Hash() []byte + Iterate(fn func(key []byte, value []byte) bool) (stopped bool) } func NewMutableTree(db dbm.DB) *MutableTree { @@ -31,6 +32,10 @@ type MutableTree struct { lock sync.RWMutex } +func (t *MutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped bool) { + return t.tree.Iterate(fn) +} + func (t *MutableTree) Hash() []byte { t.lock.RLock() defer t.lock.RUnlock() @@ -107,6 +112,10 @@ type ImmutableTree struct { tree *iavl.ImmutableTree } +func (t *ImmutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped bool) { + return t.tree.Iterate(fn) +} + func (t *ImmutableTree) Hash() []byte { return t.tree.Hash() } diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index f64898447..dfc6ef4e7 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -264,7 +264,7 @@ func (data BuyCoinData) BasicCheck(tx *Transaction, context *state.StateDB) *Res return nil } -func (data BuyCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data BuyCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) @@ -304,6 +304,9 @@ func (data BuyCoinData) Run(tx *Transaction, context *state.StateDB, isCheck boo rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) context.AddBalance(sender, data.CoinToBuy, data.ValueToBuy) context.SetNonce(sender, tx.Nonce) + + context.DeleteCoinIfZeroReserve(data.CoinToBuy) + context.DeleteCoinIfZeroReserve(data.CoinToSell) } tags := common.KVPairs{ diff --git a/core/transaction/buy_coin_test.go b/core/transaction/buy_coin_test.go index d2f8278cb..acc7a4664 100644 --- a/core/transaction/buy_coin_test.go +++ b/core/transaction/buy_coin_test.go @@ -11,6 +11,7 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "github.com/tendermint/tendermint/libs/db" "math/big" + "sync" "testing" ) @@ -87,7 +88,7 @@ func TestBuyCoinTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) @@ -150,7 +151,7 @@ func TestBuyCoinTxInsufficientFunds(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.InsufficientFunds { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) @@ -193,7 +194,7 @@ func TestBuyCoinTxEqualCoins(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.CrossConvert { t.Fatalf("Response code is not %d. Error %s", code.CrossConvert, response.Log) @@ -236,7 +237,7 @@ func TestBuyCoinTxNotExistsBuyCoin(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.CoinNotExists { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) @@ -279,7 +280,7 @@ func TestBuyCoinTxNotExistsSellCoin(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.CoinNotExists { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) @@ -324,7 +325,7 @@ func TestBuyCoinTxNotExistsGasCoin(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.CoinNotExists { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) @@ -373,7 +374,7 @@ func TestBuyCoinTxNotGasCoin(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/create_coin.go b/core/transaction/create_coin.go index 128975d77..60e19a121 100644 --- a/core/transaction/create_coin.go +++ b/core/transaction/create_coin.go @@ -71,16 +71,16 @@ func (data CreateCoinData) BasicCheck(tx *Transaction, context *state.StateDB) * Log: fmt.Sprintf("Constant Reserve Ratio should be between 10 and 100")} } - if data.InitialAmount.Cmp(MaxCoinSupply) != -1 || data.InitialAmount.Cmp(minCoinSupply) != 1 { + if data.InitialAmount.Cmp(minCoinSupply) == -1 || data.InitialAmount.Cmp(MaxCoinSupply) == 1 { return &Response{ Code: code.WrongCoinSupply, Log: fmt.Sprintf("Coin supply should be between %s and %s", minCoinSupply.String(), MaxCoinSupply.String())} } - if data.InitialReserve.Cmp(minCoinReserve) != 1 { + if -1*data.InitialReserve.Cmp(minCoinReserve) != -1 { return &Response{ Code: code.WrongCoinSupply, - Log: fmt.Sprintf("Coin reserve should be greater than %s", minCoinReserve.String())} + Log: fmt.Sprintf("Coin reserve should be greater than or equal to %s", minCoinReserve.String())} } return nil @@ -115,7 +115,7 @@ func (data CreateCoinData) Commission() int64 { return 0 } -func (data CreateCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data CreateCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/create_coin_test.go b/core/transaction/create_coin_test.go index d794abbc0..e7430f33e 100644 --- a/core/transaction/create_coin_test.go +++ b/core/transaction/create_coin_test.go @@ -6,6 +6,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -59,7 +60,7 @@ func TestCreateCoinTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/create_multisig.go b/core/transaction/create_multisig.go index 3f80d8e6c..da792a473 100644 --- a/core/transaction/create_multisig.go +++ b/core/transaction/create_multisig.go @@ -52,7 +52,7 @@ func (data CreateMultisigData) Gas() int64 { return commissions.CreateMultisig } -func (data CreateMultisigData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data CreateMultisigData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/create_multisig_test.go b/core/transaction/create_multisig_test.go index e45a48e38..78f628dcf 100644 --- a/core/transaction/create_multisig_test.go +++ b/core/transaction/create_multisig_test.go @@ -7,6 +7,7 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "math/big" "reflect" + "sync" "testing" ) @@ -61,7 +62,7 @@ func TestCreateMultisigTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/declare_candidacy.go b/core/transaction/declare_candidacy.go index 3455002ac..2bec409e1 100644 --- a/core/transaction/declare_candidacy.go +++ b/core/transaction/declare_candidacy.go @@ -71,7 +71,7 @@ func (data DeclareCandidacyData) Gas() int64 { return commissions.DeclareCandidacyTx } -func (data DeclareCandidacyData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data DeclareCandidacyData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/declare_candidacy_test.go b/core/transaction/declare_candidacy_test.go index 68edb46c4..116b3360f 100644 --- a/core/transaction/declare_candidacy_test.go +++ b/core/transaction/declare_candidacy_test.go @@ -7,6 +7,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -58,7 +59,7 @@ func TestDeclareCandidacyTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/delegate.go b/core/transaction/delegate.go index 587f1fd78..1d6822a41 100644 --- a/core/transaction/delegate.go +++ b/core/transaction/delegate.go @@ -16,7 +16,7 @@ import ( type DelegateData struct { PubKey types.Pubkey `json:"pub_key"` Coin types.CoinSymbol `json:"coin"` - Stake *big.Int `json:"stake"` + Value *big.Int `json:"value"` } func (data DelegateData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { @@ -24,7 +24,7 @@ func (data DelegateData) TotalSpend(tx *Transaction, context *state.StateDB) (To } func (data DelegateData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { - if data.PubKey == nil || data.Stake == nil { + if data.PubKey == nil || data.Value == nil { return &Response{ Code: code.DecodeError, Log: "Incorrect tx data"} @@ -36,7 +36,7 @@ func (data DelegateData) BasicCheck(tx *Transaction, context *state.StateDB) *Re Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} } - if data.Stake.Cmp(types.Big0) < 1 { + if data.Value.Cmp(types.Big0) < 1 { return &Response{ Code: code.StakeShouldBePositive, Log: fmt.Sprintf("Stake should be positive")} @@ -50,7 +50,7 @@ func (data DelegateData) BasicCheck(tx *Transaction, context *state.StateDB) *Re } sender, _ := tx.Sender() - if len(candidate.Stakes) >= state.MaxDelegatorsPerCandidate && !context.IsDelegatorStakeSufficient(sender, data.PubKey, data.Coin, data.Stake) { + if len(candidate.Stakes) >= state.MaxDelegatorsPerCandidate && !context.IsDelegatorStakeSufficient(sender, data.PubKey, data.Coin, data.Value) { return &Response{ Code: code.TooLowStake, Log: fmt.Sprintf("Stake is too low")} @@ -68,7 +68,7 @@ func (data DelegateData) Gas() int64 { return commissions.DelegateTx } -func (data DelegateData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data DelegateData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) @@ -97,15 +97,15 @@ func (data DelegateData) Run(tx *Transaction, context *state.StateDB, isCheck bo Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission, tx.GasCoin)} } - if context.GetBalance(sender, data.Coin).Cmp(data.Stake) < 0 { + if context.GetBalance(sender, data.Coin).Cmp(data.Value) < 0 { return Response{ Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), data.Stake, data.Coin)} + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), data.Value, data.Coin)} } if data.Coin == tx.GasCoin { totalTxCost := big.NewInt(0) - totalTxCost.Add(totalTxCost, data.Stake) + totalTxCost.Add(totalTxCost, data.Value) totalTxCost.Add(totalTxCost, commission) if context.GetBalance(sender, tx.GasCoin).Cmp(totalTxCost) < 0 { @@ -122,8 +122,8 @@ func (data DelegateData) Run(tx *Transaction, context *state.StateDB, isCheck bo context.SubCoinVolume(tx.GasCoin, commission) context.SubBalance(sender, tx.GasCoin, commission) - context.SubBalance(sender, data.Coin, data.Stake) - context.Delegate(sender, data.PubKey, data.Coin, data.Stake) + context.SubBalance(sender, data.Coin, data.Value) + context.Delegate(sender, data.PubKey, data.Coin, data.Value) context.SetNonce(sender, tx.Nonce) } diff --git a/core/transaction/delegate_test.go b/core/transaction/delegate_test.go index 6f1a02ac6..ba8d59525 100644 --- a/core/transaction/delegate_test.go +++ b/core/transaction/delegate_test.go @@ -8,6 +8,7 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "math/big" "math/rand" + "sync" "testing" ) @@ -38,7 +39,7 @@ func TestDelegateTx(t *testing.T) { data := DelegateData{ PubKey: pubkey, Coin: coin, - Stake: value, + Value: value, } encodedData, err := rlp.EncodeToBytes(data) @@ -66,7 +67,7 @@ func TestDelegateTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/edit_candidate.go b/core/transaction/edit_candidate.go index 3dded77dc..a3e23b902 100644 --- a/core/transaction/edit_candidate.go +++ b/core/transaction/edit_candidate.go @@ -44,7 +44,7 @@ func (data EditCandidateData) Gas() int64 { return commissions.EditCandidate } -func (data EditCandidateData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data EditCandidateData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/edit_candidate_test.go b/core/transaction/edit_candidate_test.go index 436d513d5..6dc791946 100644 --- a/core/transaction/edit_candidate_test.go +++ b/core/transaction/edit_candidate_test.go @@ -7,6 +7,7 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "math/big" "math/rand" + "sync" "testing" ) @@ -58,7 +59,7 @@ func TestEditCandidateTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/executor.go b/core/transaction/executor.go index 0231b0b7f..649c6940a 100644 --- a/core/transaction/executor.go +++ b/core/transaction/executor.go @@ -8,6 +8,7 @@ import ( "github.com/MinterTeam/minter-go-node/log" "github.com/tendermint/tendermint/libs/common" "math/big" + "sync" ) var ( @@ -31,7 +32,7 @@ type Response struct { GasPrice *big.Int } -func RunTx(context *state.StateDB, isCheck bool, rawTx []byte, rewardPool *big.Int, currentBlock int64, currentMempool map[types.Address]struct{}) Response { +func RunTx(context *state.StateDB, isCheck bool, rawTx []byte, rewardPool *big.Int, currentBlock uint64, currentMempool sync.Map, minGasPrice *big.Int) Response { if len(rawTx) > maxTxLength { return Response{ Code: code.TxTooLarge, @@ -45,6 +46,13 @@ func RunTx(context *state.StateDB, isCheck bool, rawTx []byte, rewardPool *big.I Log: err.Error()} } + if isCheck && tx.GasPrice.Cmp(minGasPrice) == -1 { + return Response{ + Code: code.TooLowGasPrice, + Log: fmt.Sprintf("Gas price of tx is too low to be included in mempool. Expected %s", minGasPrice), + } + } + if !isCheck { log.Info("Deliver tx", "tx", tx.String()) } @@ -69,14 +77,14 @@ func RunTx(context *state.StateDB, isCheck bool, rawTx []byte, rewardPool *big.I } // check if mempool already has transactions from this address - if _, has := currentMempool[sender]; isCheck && has { + if _, has := currentMempool.Load(sender); isCheck && has { return Response{ Code: code.TxFromSenderAlreadyInMempool, Log: fmt.Sprintf("Tx from %s already exists in mempool", sender.String())} } if isCheck { - currentMempool[sender] = struct{}{} + currentMempool.Store(sender, true) } // check multi-signature @@ -136,10 +144,14 @@ func RunTx(context *state.StateDB, isCheck bool, rawTx []byte, rewardPool *big.I response := tx.decodedData.Run(tx, context, isCheck, rewardPool, currentBlock) if response.Code != code.TxFromSenderAlreadyInMempool && response.Code != code.OK { - delete(currentMempool, sender) + currentMempool.Delete(sender) } response.GasPrice = tx.GasPrice + if !isCheck && response.Code == code.OK { + context.DeleteCoinIfZeroReserve(tx.GasCoin) + } + return response } diff --git a/core/transaction/executor_test.go b/core/transaction/executor_test.go index b065da842..b857f1954 100644 --- a/core/transaction/executor_test.go +++ b/core/transaction/executor_test.go @@ -8,13 +8,14 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "math/big" "math/rand" + "sync" "testing" ) func TestTooLongTx(t *testing.T) { fakeTx := make([]byte, 10000) - response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.TxTooLarge { t.Fatalf("Response code is not correct") @@ -25,7 +26,7 @@ func TestIncorrectTx(t *testing.T) { fakeTx := make([]byte, 1) rand.Read(fakeTx) - response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.DecodeError { t.Fatalf("Response code is not correct") @@ -64,7 +65,7 @@ func TestTooLongPayloadTx(t *testing.T) { fakeTx, _ := rlp.EncodeToBytes(tx) - response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.TxPayloadTooLarge { t.Fatalf("Response code is not correct. Expected %d, got %d", code.TxPayloadTooLarge, response.Code) @@ -102,7 +103,7 @@ func TestTooLongServiceDataTx(t *testing.T) { fakeTx, _ := rlp.EncodeToBytes(tx) - response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.TxServiceDataTooLarge { t.Fatalf("Response code is not correct. Expected %d, got %d", code.TxServiceDataTooLarge, response.Code) @@ -136,7 +137,7 @@ func TestUnexpectedNonceTx(t *testing.T) { fakeTx, _ := rlp.EncodeToBytes(tx) - response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.WrongNonce { t.Fatalf("Response code is not correct. Expected %d, got %d", code.WrongNonce, response.Code) @@ -173,7 +174,7 @@ func TestInvalidSigTx(t *testing.T) { fakeTx, _ := rlp.EncodeToBytes(tx) - response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.DecodeError { t.Fatalf("Response code is not correct. Expected %d, got %d", code.DecodeError, response.Code) @@ -211,7 +212,7 @@ func TestNotExistMultiSigTx(t *testing.T) { fakeTx, _ := rlp.EncodeToBytes(tx) - response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(getState(), false, fakeTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.MultisigNotExists { t.Fatalf("Response code is not correct. Expected %d, got %d", code.MultisigNotExists, response.Code) @@ -254,7 +255,7 @@ func TestMultiSigTx(t *testing.T) { txBytes, _ := rlp.EncodeToBytes(tx) - response := RunTx(cState, false, txBytes, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, txBytes, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Error code is not 0. Error: %s", response.Log) @@ -301,7 +302,7 @@ func TestMultiSigDoubleSignTx(t *testing.T) { txBytes, _ := rlp.EncodeToBytes(tx) - response := RunTx(cState, false, txBytes, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, txBytes, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.IncorrectMultiSignature { t.Fatalf("Error code is not %d, got %d", code.IncorrectMultiSignature, response.Code) @@ -351,7 +352,7 @@ func TestMultiSigTooManySignsTx(t *testing.T) { txBytes, _ := rlp.EncodeToBytes(tx) - response := RunTx(cState, false, txBytes, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, txBytes, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.IncorrectMultiSignature { t.Fatalf("Error code is not %d, got %d", code.IncorrectMultiSignature, response.Code) @@ -394,7 +395,7 @@ func TestMultiSigNotEnoughTx(t *testing.T) { txBytes, _ := rlp.EncodeToBytes(tx) - response := RunTx(cState, false, txBytes, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, txBytes, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.IncorrectMultiSignature { t.Fatalf("Error code is not %d. Error: %d", code.IncorrectMultiSignature, response.Code) @@ -438,7 +439,7 @@ func TestMultiSigIncorrectSignsTx(t *testing.T) { txBytes, _ := rlp.EncodeToBytes(tx) - response := RunTx(cState, false, txBytes, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, txBytes, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != code.IncorrectMultiSignature { t.Fatalf("Error code is not %d, got %d", code.IncorrectMultiSignature, response.Code) diff --git a/core/transaction/multisend.go b/core/transaction/multisend.go index 294957928..11c6f27d6 100644 --- a/core/transaction/multisend.go +++ b/core/transaction/multisend.go @@ -59,7 +59,7 @@ func (data MultisendData) Gas() int64 { return commissions.SendTx + ((int64(len(data.List)) - 1) * commissions.MultisendDelta) } -func (data MultisendData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data MultisendData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/multisend_test.go b/core/transaction/multisend_test.go index d364ec716..fc9e4386a 100644 --- a/core/transaction/multisend_test.go +++ b/core/transaction/multisend_test.go @@ -6,6 +6,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -56,7 +57,7 @@ func TestMultisendTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error: %s", response.Log) diff --git a/core/transaction/redeem_check.go b/core/transaction/redeem_check.go index 0c875ff05..26a1f779d 100644 --- a/core/transaction/redeem_check.go +++ b/core/transaction/redeem_check.go @@ -48,7 +48,7 @@ func (data RedeemCheckData) Gas() int64 { return commissions.RedeemCheckTx } -func (data RedeemCheckData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data RedeemCheckData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/redeem_check_test.go b/core/transaction/redeem_check_test.go index 480e9a323..a7346f4ae 100644 --- a/core/transaction/redeem_check_test.go +++ b/core/transaction/redeem_check_test.go @@ -9,6 +9,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -101,7 +102,7 @@ func TestRedeemCheckTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/sell_all_coin.go b/core/transaction/sell_all_coin.go index 9254d9ad6..394d5c779 100644 --- a/core/transaction/sell_all_coin.go +++ b/core/transaction/sell_all_coin.go @@ -162,7 +162,7 @@ func (data SellAllCoinData) Gas() int64 { return commissions.ConvertTx } -func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) @@ -204,6 +204,9 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) context.AddBalance(sender, data.CoinToBuy, value) context.SetNonce(sender, tx.Nonce) + + context.DeleteCoinIfZeroReserve(data.CoinToBuy) + context.DeleteCoinIfZeroReserve(data.CoinToSell) } tags := common.KVPairs{ diff --git a/core/transaction/sell_all_coin_test.go b/core/transaction/sell_all_coin_test.go index 843d7d1e4..44f25d0d5 100644 --- a/core/transaction/sell_all_coin_test.go +++ b/core/transaction/sell_all_coin_test.go @@ -6,6 +6,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -52,7 +53,7 @@ func TestSellAllCoinTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index d17b3344e..e821b1406 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -273,7 +273,7 @@ func (data SellCoinData) Gas() int64 { return commissions.ConvertTx } -func (data SellCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SellCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) @@ -313,6 +313,9 @@ func (data SellCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bo rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) context.AddBalance(sender, data.CoinToBuy, value) context.SetNonce(sender, tx.Nonce) + + context.DeleteCoinIfZeroReserve(data.CoinToBuy) + context.DeleteCoinIfZeroReserve(data.CoinToSell) } tags := common.KVPairs{ diff --git a/core/transaction/sell_coin_test.go b/core/transaction/sell_coin_test.go index 6e368a80a..565f2194d 100644 --- a/core/transaction/sell_coin_test.go +++ b/core/transaction/sell_coin_test.go @@ -6,6 +6,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -53,7 +54,7 @@ func TestSellCoinTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error: %s", response.Log) @@ -71,3 +72,68 @@ func TestSellCoinTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", getTestCoinSymbol(), targetTestBalance, testBalance) } } + +func TestSellCoinTxWithCoinRemoval(t *testing.T) { + cState := getState() + + volume, _ := big.NewInt(0).SetString("673449859091115734468033", 10) + reserve, _ := big.NewInt(0).SetString("4991502952461582748", 10) + + cState.CreateCoin(getTestCoinSymbol(), "TEST COIN", volume, 10, reserve) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + coin := getTestCoinSymbol() + + toSell, _ := big.NewInt(0).SetString("672849068640650013513552", 10) + cState.AddBalance(addr, coin, toSell) + + minValToBuy := big.NewInt(0) + + data := SellAllCoinData{ + CoinToSell: getTestCoinSymbol(), + CoinToBuy: types.GetBaseCoin(), + MinimumValueToBuy: minValToBuy, + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: big.NewInt(1), + GasCoin: coin, + Type: TypeSellAllCoin, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) + + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + targetBalance := big.NewInt(0) + balance := cState.GetBalance(addr, coin) + if balance.Cmp(targetBalance) != 0 { + t.Fatalf("Target %s balance is not correct. Expected %s, got %s", coin, targetBalance, balance) + } + + if cState.GetStateCoin(coin).Volume().Cmp(big.NewInt(0)) != 0 { + t.Fatalf("Target %s volume is not correct. Expected %s, got %s", coin, big.NewInt(0), cState.GetStateCoin(coin).Volume()) + } +} diff --git a/core/transaction/send.go b/core/transaction/send.go index 8cb1df830..35306eb18 100644 --- a/core/transaction/send.go +++ b/core/transaction/send.go @@ -82,7 +82,7 @@ func (data SendData) Gas() int64 { return commissions.SendTx } -func (data SendData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SendData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/send_test.go b/core/transaction/send_test.go index de6665b23..f1d1c10e0 100644 --- a/core/transaction/send_test.go +++ b/core/transaction/send_test.go @@ -6,6 +6,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -50,7 +51,7 @@ func TestSendTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error: %s", response.Log) } diff --git a/core/transaction/switch_candidate_status.go b/core/transaction/switch_candidate_status.go index 250e610e5..4250e39c8 100644 --- a/core/transaction/switch_candidate_status.go +++ b/core/transaction/switch_candidate_status.go @@ -37,7 +37,7 @@ func (data SetCandidateOnData) Gas() int64 { return commissions.ToggleCandidateStatus } -func (data SetCandidateOnData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SetCandidateOnData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) @@ -115,7 +115,7 @@ func (data SetCandidateOffData) Gas() int64 { return commissions.ToggleCandidateStatus } -func (data SetCandidateOffData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SetCandidateOffData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/switch_candidate_status_test.go b/core/transaction/switch_candidate_status_test.go index 3ac43d64f..a66ab6731 100644 --- a/core/transaction/switch_candidate_status_test.go +++ b/core/transaction/switch_candidate_status_test.go @@ -8,6 +8,7 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "math/big" "math/rand" + "sync" "testing" ) @@ -53,7 +54,7 @@ func TestSwitchCandidateStatusTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go index fb95bb7ad..724501322 100644 --- a/core/transaction/transaction.go +++ b/core/transaction/transaction.go @@ -106,7 +106,7 @@ type Data interface { Gas() int64 TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) BasicCheck(tx *Transaction, context *state.StateDB) *Response - Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response + Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response } func (tx *Transaction) Serialize() ([]byte, error) { diff --git a/core/transaction/unbond.go b/core/transaction/unbond.go index 1b782ee83..c6af21065 100644 --- a/core/transaction/unbond.go +++ b/core/transaction/unbond.go @@ -73,7 +73,7 @@ func (data UnbondData) Gas() int64 { return commissions.UnbondTx } -func (data UnbondData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data UnbondData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock uint64) Response { sender, _ := tx.Sender() response := data.BasicCheck(tx, context) diff --git a/core/transaction/unbond_test.go b/core/transaction/unbond_test.go index e9e0ee87f..fc0cb8f40 100644 --- a/core/transaction/unbond_test.go +++ b/core/transaction/unbond_test.go @@ -6,6 +6,7 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" "math/big" + "sync" "testing" ) @@ -54,7 +55,7 @@ func TestUnbondTx(t *testing.T) { t.Fatal(err) } - response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, make(map[types.Address]struct{})) + response := RunTx(cState, false, encodedTx, big.NewInt(0), 0, sync.Map{}, big.NewInt(0)) if response.Code != 0 { t.Fatalf("Response code is not 0. Error %s", response.Log) diff --git a/core/types/appstate.go b/core/types/appstate.go new file mode 100644 index 000000000..eeb98c949 --- /dev/null +++ b/core/types/appstate.go @@ -0,0 +1,78 @@ +package types + +import ( + "math/big" +) + +type AppState struct { + Validators []Validator `json:"validators,omitempty"` + Candidates []Candidate `json:"candidates,omitempty"` + Accounts []Account `json:"accounts,omitempty"` + Coins []Coin `json:"coins,omitempty"` + FrozenFunds []FrozenFund `json:"frozen_funds,omitempty"` + UsedChecks []UsedCheck `json:"used_checks,omitempty"` + MaxGas uint64 `json:"max_gas"` +} + +type Validator struct { + RewardAddress Address `json:"reward_address"` + TotalBipStake *big.Int `json:"total_bip_stake"` + PubKey Pubkey `json:"pub_key"` + Commission uint `json:"commission"` + AccumReward *big.Int `json:"accum_reward"` + AbsentTimes *BitArray `json:"absent_times"` +} + +type Candidate struct { + RewardAddress Address `json:"reward_address"` + OwnerAddress Address `json:"owner_address"` + TotalBipStake *big.Int `json:"total_bip_stake"` + PubKey Pubkey `json:"pub_key"` + Commission uint `json:"commission"` + Stakes []Stake `json:"stakes"` + CreatedAtBlock uint `json:"created_at_block"` + Status byte `json:"status"` +} + +type Stake struct { + Owner Address `json:"owner"` + Coin CoinSymbol `json:"coin"` + Value *big.Int `json:"value"` + BipValue *big.Int `json:"bip_value"` +} + +type Coin struct { + Name string `json:"name"` + Symbol CoinSymbol `json:"symbol"` + Volume *big.Int `json:"volume"` + Crr uint `json:"crr"` + ReserveBalance *big.Int `json:"reserve_balance"` +} + +type FrozenFund struct { + Height uint64 `json:"height"` + Address Address `json:"address"` + CandidateKey Pubkey `json:"candidate_key"` + Coin CoinSymbol `json:"coin"` + Value *big.Int `json:"value"` +} + +type UsedCheck string + +type Account struct { + Address Address `json:"address"` + Balance []Balance `json:"balance"` + Nonce uint64 `json:"nonce"` + MultisigData *Multisig `json:"multisig_data,omitempty"` +} + +type Balance struct { + Coin CoinSymbol `json:"coin"` + Value *big.Int `json:"value"` +} + +type Multisig struct { + Weights []uint `json:"weights"` + Threshold uint `json:"threshold"` + Addresses []Address `json:"addresses"` +} diff --git a/core/state/bitarray.go b/core/types/bitarray.go similarity index 99% rename from core/state/bitarray.go rename to core/types/bitarray.go index 79b35e266..5cc54cc32 100644 --- a/core/state/bitarray.go +++ b/core/types/bitarray.go @@ -1,4 +1,4 @@ -package state +package types import ( "encoding/binary" diff --git a/core/types/types.go b/core/types/types.go index 656b07b28..34d7f8e78 100644 --- a/core/types/types.go +++ b/core/types/types.go @@ -155,6 +155,11 @@ func (c CoinSymbol) MarshalJSON() ([]byte, error) { return buffer.Bytes(), nil } +func (c *CoinSymbol) UnmarshalJSON(input []byte) error { + *c = StrToCoinSymbol(string(input[1 : len(input)-1])) + return nil +} + func (c CoinSymbol) Compare(c2 CoinSymbol) int { return bytes.Compare(c.Bytes(), c2.Bytes()) } @@ -286,6 +291,13 @@ func (p Pubkey) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", p.String())), nil } +func (p *Pubkey) UnmarshalJSON(input []byte) error { + b, err := hex.DecodeString(string(input)[3 : len(input)-1]) + *p = Pubkey(b) + + return err +} + func (p Pubkey) Compare(p2 Pubkey) int { return bytes.Compare(p, p2) } diff --git a/core/types/types_test.go b/core/types/types_test.go index 202d7ef09..ddc5f0261 100644 --- a/core/types/types_test.go +++ b/core/types/types_test.go @@ -17,8 +17,9 @@ package types import ( + "bytes" "encoding/json" - + "github.com/MinterTeam/go-amino" "math/big" "strings" "testing" @@ -126,3 +127,103 @@ func BenchmarkAddressHex(b *testing.B) { testAddr.Hex() } } + +func TestAppState(t *testing.T) { + testAddr := HexToAddress("Mx5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + pubkey := Pubkey{1, 2, 3} + ba := NewBitArray(24) + ba.SetIndex(3, true) + + appState := AppState{ + Validators: []Validator{ + { + RewardAddress: testAddr, + TotalBipStake: big.NewInt(1), + PubKey: pubkey, + Commission: 1, + AccumReward: big.NewInt(1), + AbsentTimes: ba, + }, + }, + Candidates: []Candidate{ + { + RewardAddress: testAddr, + OwnerAddress: testAddr, + TotalBipStake: big.NewInt(1), + PubKey: pubkey, + Commission: 1, + Stakes: []Stake{ + { + Owner: testAddr, + Coin: GetBaseCoin(), + Value: big.NewInt(1), + BipValue: big.NewInt(1), + }, + }, + CreatedAtBlock: 1, + Status: 1, + }, + }, + Accounts: []Account{ + { + Address: testAddr, + Balance: []Balance{ + { + Coin: GetBaseCoin(), + Value: big.NewInt(1), + }, + }, + Nonce: 1, + MultisigData: &Multisig{ + Weights: []uint{1, 2, 3}, + Threshold: 1, + Addresses: []Address{testAddr, testAddr}, + }, + }, + }, + Coins: []Coin{ + { + Name: "ASD", + Symbol: GetBaseCoin(), + Volume: big.NewInt(1), + Crr: 1, + ReserveBalance: big.NewInt(1), + }, + }, + FrozenFunds: []FrozenFund{ + { + Height: 1, + Address: testAddr, + CandidateKey: pubkey, + Coin: GetBaseCoin(), + Value: big.NewInt(1), + }, + }, + UsedChecks: []UsedCheck{ + "123", + }, + MaxGas: 10, + } + + cdc := amino.NewCodec() + + b1, err := cdc.MarshalJSON(appState) + if err != nil { + panic(err) + } + + newAppState := AppState{} + err = cdc.UnmarshalJSON(b1, &newAppState) + if err != nil { + panic(err) + } + + b2, err := cdc.MarshalJSON(newAppState) + if err != nil { + panic(err) + } + + if bytes.Compare(b1, b2) != 0 { + t.Errorf("Bytes are not the same") + } +} diff --git a/core/validators/validators.go b/core/validators/validators.go index 837af6e37..1b57dcbb9 100644 --- a/core/validators/validators.go +++ b/core/validators/validators.go @@ -1,6 +1,6 @@ package validators -func GetValidatorsCountForBlock(block int64) int { +func GetValidatorsCountForBlock(block uint64) int { count := 16 + (block/518400)*4 if count > 256 { @@ -10,6 +10,6 @@ func GetValidatorsCountForBlock(block int64) int { return int(count) } -func GetCandidatesCountForBlock(block int64) int { +func GetCandidatesCountForBlock(block uint64) int { return GetValidatorsCountForBlock(block) * 3 } diff --git a/core/validators/validators_test.go b/core/validators/validators_test.go index 9c7bdf657..1e2f8a68a 100644 --- a/core/validators/validators_test.go +++ b/core/validators/validators_test.go @@ -5,7 +5,7 @@ import ( ) type Results struct { - Block int64 + Block uint64 Result int } diff --git a/eventsdb/amino.go b/eventsdb/amino.go index 3a1add759..213b7cbc6 100644 --- a/eventsdb/amino.go +++ b/eventsdb/amino.go @@ -10,4 +10,6 @@ func RegisterAminoEvents(codec *amino.Codec) { "minter/SlashEvent", nil) codec.RegisterConcrete(UnbondEvent{}, "minter/UnbondEvent", nil) + codec.RegisterConcrete(CoinLiquidationEvent{}, + "minter/CoinLiquidationEvent", nil) } diff --git a/eventsdb/events.go b/eventsdb/events.go index 70136f021..5132d6bf0 100644 --- a/eventsdb/events.go +++ b/eventsdb/events.go @@ -95,3 +95,15 @@ func (e UnbondEvent) MarshalJSON() ([]byte, error) { ValidatorPubKey: e.ValidatorPubKey, }) } + +type CoinLiquidationEvent struct { + Coin types.CoinSymbol +} + +func (e CoinLiquidationEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Coin string `json:"coin"` + }{ + Coin: e.Coin.String(), + }) +} diff --git a/eventsdb/eventsdb.go b/eventsdb/eventsdb.go index 5549059c7..038c4e4ce 100644 --- a/eventsdb/eventsdb.go +++ b/eventsdb/eventsdb.go @@ -54,13 +54,13 @@ type EventsDB struct { } type eventsCache struct { - height int64 + height uint64 events Events lock sync.RWMutex } -func (c *eventsCache) set(height int64, events Events) { +func (c *eventsCache) set(height uint64, events Events) { c.lock.Lock() defer c.lock.Unlock() @@ -94,7 +94,7 @@ func NewEventsDB(db *db.GoLevelDB) *EventsDB { } } -func (db *EventsDB) AddEvent(height int64, event Event) { +func (db *EventsDB) AddEvent(height uint64, event Event) { if !eventsEnabled { return } @@ -103,11 +103,13 @@ func (db *EventsDB) AddEvent(height int64, event Event) { db.setEvents(height, append(events, event)) } -func (db *EventsDB) FlushEvents(height int64) error { +func (db *EventsDB) FlushEvents() error { if !eventsEnabled { return nil } + height := db.cache.height + events := db.getEvents(height) bytes, err := cdc.MarshalBinaryBare(events) @@ -121,11 +123,11 @@ func (db *EventsDB) FlushEvents(height int64) error { return nil } -func (db *EventsDB) setEvents(height int64, events Events) { +func (db *EventsDB) setEvents(height uint64, events Events) { db.cache.set(height, events) } -func (db *EventsDB) LoadEvents(height int64) Events { +func (db *EventsDB) LoadEvents(height uint64) Events { db.lock.RLock() data := db.db.Get(getKeyForHeight(height)) db.lock.RUnlock() @@ -144,7 +146,7 @@ func (db *EventsDB) LoadEvents(height int64) Events { return decoded } -func (db *EventsDB) getEvents(height int64) Events { +func (db *EventsDB) getEvents(height uint64) Events { if db.cache.height == height { return db.cache.get() } @@ -155,9 +157,9 @@ func (db *EventsDB) getEvents(height int64) Events { return events } -func getKeyForHeight(height int64) []byte { +func getKeyForHeight(height uint64) []byte { var h = make([]byte, 8) - binary.BigEndian.PutUint64(h, uint64(height)) + binary.BigEndian.PutUint64(h, height) return h } diff --git a/genesis/genesis.go b/genesis/genesis.go index 2047dd7e1..19364f6ee 100644 --- a/genesis/genesis.go +++ b/genesis/genesis.go @@ -3,32 +3,24 @@ package genesis import ( "encoding/base64" "encoding/json" + "github.com/MinterTeam/go-amino" "github.com/MinterTeam/minter-go-node/core/developers" + "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/helpers" - "github.com/tendermint/tendermint/crypto/ed25519" tmtypes "github.com/tendermint/tendermint/types" "math/big" "time" ) var ( - Network = "minter-test-network-33" - genesisTime = time.Date(2019, 2, 18, 9, 0, 0, 0, time.UTC) + Network = "minter-test-network-34" + genesisTime = time.Date(2019, time.March, 27, 12, 0, 0, 0, time.UTC) - totalValidatorsPower = 100000000 + BlockMaxBytes int64 = 10000000 + DefaultMaxGas int64 = 100000 ) -type AppState struct { - FirstValidatorAddress types.Address `json:"first_validator_address"` - InitialBalances []Account `json:"initial_balances"` -} - -type Account struct { - Address types.Address `json:"address"` - Balance map[string]string `json:"balance"` -} - func GetTestnetGenesis() (*tmtypes.GenesisDoc, error) { validatorsPubKeys := []string{ "SuHuc+YTbIWwypM6mhNHdYozSIXxCzI4OYpnrC6xU7g=", @@ -54,11 +46,17 @@ func GetTestnetGenesis() (*tmtypes.GenesisDoc, error) { "Mx35c40563ee5181899d0d605839edb9e940b0d8e5": 33869, // SolidMinter } + validators, candidates := MakeValidatorsAndCandidates(validatorsPubKeys, big.NewInt(1)) + + cdc := amino.NewCodec() + // Prepare initial AppState - appStateJSON, err := json.Marshal(AppState{ - FirstValidatorAddress: developers.Address, - InitialBalances: makeBalances(balances), - }) + appStateJSON, err := cdc.MarshalJSONIndent(types.AppState{ + Validators: validators, + Candidates: candidates, + Accounts: makeBalances(balances), + MaxGas: 100000, + }, "", " ") if err != nil { return nil, err } @@ -67,11 +65,23 @@ func GetTestnetGenesis() (*tmtypes.GenesisDoc, error) { // Compose Genesis genesis := tmtypes.GenesisDoc{ - ChainID: Network, GenesisTime: genesisTime, - Validators: makeValidators(validatorsPubKeys), - AppHash: appHash[:], - AppState: json.RawMessage(appStateJSON), + ChainID: Network, + ConsensusParams: &tmtypes.ConsensusParams{ + Block: tmtypes.BlockParams{ + MaxBytes: BlockMaxBytes, + MaxGas: DefaultMaxGas, + TimeIotaMs: 1000, + }, + Evidence: tmtypes.EvidenceParams{ + MaxAge: 1000, + }, + Validator: tmtypes.ValidatorParams{ + PubKeyTypes: []string{tmtypes.ABCIPubKeyTypeEd25519}, + }, + }, + AppHash: appHash[:], + AppState: json.RawMessage(appStateJSON), } err = genesis.ValidateAndComplete() @@ -82,31 +92,49 @@ func GetTestnetGenesis() (*tmtypes.GenesisDoc, error) { return &genesis, nil } -func decodeValidatorPubkey(pubkey string) ed25519.PubKeyEd25519 { - validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(pubkey) - if err != nil { - panic(err) - } +func MakeValidatorsAndCandidates(pubkeys []string, stake *big.Int) ([]types.Validator, []types.Candidate) { + validators := make([]types.Validator, len(pubkeys)) + candidates := make([]types.Candidate, len(pubkeys)) + addr := developers.Address - var validatorPubKey ed25519.PubKeyEd25519 - copy(validatorPubKey[:], validatorPubKeyBytes) + for i, val := range pubkeys { + pkey, err := base64.StdEncoding.DecodeString(val) + if err != nil { + panic(err) + } - return validatorPubKey -} + validators[i] = types.Validator{ + RewardAddress: addr, + TotalBipStake: stake, + PubKey: pkey, + Commission: 100, + AccumReward: big.NewInt(0), + AbsentTimes: types.NewBitArray(24), + } -func makeValidators(pubkeys []string) []tmtypes.GenesisValidator { - validators := make([]tmtypes.GenesisValidator, len(pubkeys)) - for i, val := range pubkeys { - validators[i] = tmtypes.GenesisValidator{ - PubKey: decodeValidatorPubkey(val), - Power: int64(totalValidatorsPower / len(pubkeys)), + candidates[i] = types.Candidate{ + RewardAddress: addr, + OwnerAddress: addr, + TotalBipStake: big.NewInt(1), + PubKey: pkey, + Commission: 100, + Stakes: []types.Stake{ + { + Owner: addr, + Coin: types.GetBaseCoin(), + Value: stake, + BipValue: stake, + }, + }, + CreatedAtBlock: 1, + Status: state.CandidateStatusOnline, } } - return validators + return validators, candidates } -func makeBalances(balances map[string]int64) []Account { +func makeBalances(balances map[string]int64) []types.Account { var totalBalances int64 for _, val := range balances { totalBalances += val @@ -114,13 +142,16 @@ func makeBalances(balances map[string]int64) []Account { balances[developers.Address.String()] = 200000000 - totalBalances // Developers account - result := make([]Account, len(balances)) + result := make([]types.Account, len(balances)) i := 0 for address, balance := range balances { - result[i] = Account{ + result[i] = types.Account{ Address: types.HexToAddress(address), - Balance: map[string]string{ - types.GetBaseCoin().String(): helpers.BipToPip(big.NewInt(balance)).String(), + Balance: []types.Balance{ + { + Coin: types.GetBaseCoin(), + Value: helpers.BipToPip(big.NewInt(balance)), + }, }, } i++ diff --git a/gui/a_gui-packr.go b/gui/a_gui-packr.go index 2c2f2e0cb..12506c4fb 100644 --- a/gui/a_gui-packr.go +++ b/gui/a_gui-packr.go @@ -7,5 +7,5 @@ import "github.com/gobuffalo/packr" // You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { - packr.PackJSONBytes("./html", "index.html", "\"PGh0bWw+CjxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiCiAgICAgICAgICBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIHVzZXItc2NhbGFibGU9bm8sIGluaXRpYWwtc2NhbGU9MS4wLCBtYXhpbXVtLXNjYWxlPTEuMCwgbWluaW11bS1zY2FsZT0xLjAiPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJpZT1lZGdlIj4KICAgIDx0aXRsZT5NaW50ZXIgTm9kZSBHVUk8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL3N0YWNrcGF0aC5ib290c3RyYXBjZG4uY29tL2Jvb3RzdHJhcC80LjEuMC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiCiAgICAgICAgICBpbnRlZ3JpdHk9InNoYTM4NC05Z1ZRNGRZRnd3V1NqSURabkxFV254Q2plU1dGcGhKaXdHUFhyMWpkZEloT2VnaXUxRndPNXFSR3ZGWE9kSlo0IiBjcm9zc29yaWdpbj0iYW5vbnltb3VzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly91c2UuZm9udGF3ZXNvbWUuY29tL3JlbGVhc2VzL3Y1LjEuMS9jc3MvYWxsLmNzcyIgaW50ZWdyaXR5PSJzaGEzODQtTzh3aFMzZmhHMk9uQTVLYXMwWTlsM2NmcG1ZamFwakkwRTR0aGVINGl1TUQrcExoYmY2SkkwaklNZlljSzN5WiIgY3Jvc3NvcmlnaW49ImFub255bW91cyI+CiAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS92dWUiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2F4aW9zLzAuMTguMC9heGlvcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2NyeXB0by1qcy8zLjEuOS0xL2NyeXB0by1qcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHN0eWxlPgoKICAgICAgICAuY2FyZCB7CiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDIwcHg7CiAgICAgICAgfQoKICAgICAgICBodG1sLGJvZHksLmJvZHksI2FwcCB7CiAgICAgICAgICAgIG1pbi1oZWlnaHQ6IDEwMCU7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIC5ib2R5IHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDE1cHg7CiAgICAgICAgfQoKICAgICAgICAudGFibGUgewogICAgICAgICAgICBtYXJnaW4tYm90dG9tOiAwOwogICAgICAgICAgICB0YWJsZS1sYXlvdXQ6IGZpeGVkOwogICAgICAgIH0KCiAgICAgICAgLmNhcmQtaGVhZGVyIHsKICAgICAgICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICAgICAgfQoKICAgICAgICAuY2FyZC1oZWFkZXIgewogICAgICAgICAgICBwYWRkaW5nLWxlZnQ6IDEycHg7CiAgICAgICAgfQoKICAgICAgICAuaCB7CiAgICAgICAgICAgIHdpZHRoOiAyMDBweDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YzZjNmMzsKICAgICAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2NjYzsKICAgICAgICB9CgogICAgICAgIC5iZy1zdWNjZXNzLCAuYmctZGFuZ2VyIHsKICAgICAgICAgICAgY29sb3I6IHdoaXRlOwogICAgICAgIH0KCiAgICAgICAgLmJnLWRhbmdlciB7CiAgICAgICAgICAgIGJvcmRlci1jb2xvcjogI2RjMzU0NSAhaW1wb3J0YW50OwogICAgICAgIH0KCiAgICAgICAgLmJnLXN1Y2Nlc3MgewogICAgICAgICAgICBib3JkZXItY29sb3I6ICMyOGE3NDUgIWltcG9ydGFudDsKICAgICAgICB9CgogICAgICAgIC5mYS1jaGVjayB7CiAgICAgICAgICAgIGNvbG9yOiBncmVlbjsKICAgICAgICB9CgogICAgICAgIC5mYS1leGNsYW1hdGlvbi1jaXJjbGUgewogICAgICAgICAgICBjb2xvcjogcmVkOwogICAgICAgIH0KICAgIDwvc3R5bGU+CjwvaGVhZD4KPGJvZHkgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6ICMzNDNhNDAxYSI+CjxkaXYgaWQ9ImFwcCI+CiAgICA8bmF2IGNsYXNzPSJuYXZiYXIgbmF2YmFyLWV4cGFuZC1sZyBuYXZiYXItZGFyayBiZy1kYXJrIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICA8c3BhbiBjbGFzcz0ibmF2YmFyLWJyYW5kIG1iLTAgaDEiPjxpIGNsYXNzPSJmYXMgZmEtdGVybWluYWwiPjwvaT4gJm5ic3A7IE1pbnRlciBGdWxsIE5vZGUgU3RhdHVzPC9zcGFuPgogICAgICAgIDwvZGl2PgogICAgPC9uYXY+CiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIgYm9keSIgdi1pZj0iZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LWRhbmdlciIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICA8aDQgY2xhc3M9ImFsZXJ0LWhlYWRpbmciPkVycm9yIHdoaWxlIGNvbm5lY3RpbmcgdG8gbG9jYWwgbm9kZTwvaDQ+CiAgICAgICAgICAgIDxwIGNsYXNzPSJtYi0wIj57eyBlcnJvciB9fTwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIGJvZHkgYmctd2hpdGUiIHYtaWY9InN0YXR1cyAmJiAhZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9InJvdyI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5vZGUgSW5mbwogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDx0YWJsZSBjbGFzcz0idGFibGUgY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPk1vbmlrZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YXR1cy5ub2RlX2luZm8ubW9uaWtlciB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+Tm9kZSBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5pZCB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TmV0d29yayBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5uZXR3b3JrIH19PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5NaW50ZXIgVmVyc2lvbjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgdmVyc2lvbiB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+VGVuZGVybWludCBWZXJzaW9uPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyBzdGF0dXMubm9kZV9pbmZvLnZlcnNpb24gfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiIHYtaWY9Im5ldF9pbmZvIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5ldCBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgTGlzdGVuaW5nPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD48aSA6Y2xhc3M9InsnZmEtY2hlY2snOiBuZXRfaW5mby5saXN0ZW5pbmd9IiBjbGFzcz0iZmFzIj48L2k+PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5Db25uZWN0ZWQgUGVlcnM8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5ldF9pbmZvLm5fcGVlcnMgfX0gPGkgOmNsYXNzPSJ7J2ZhLWV4Y2xhbWF0aW9uLWNpcmNsZSc6IG5ldF9pbmZvLm5fcGVlcnMgPCAxfSIgY2xhc3M9ImZhcyI+PC9pPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgU3luY2luZyBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgU3luY2VkPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSJzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwIj5Obzwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSIhc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cCI+WWVzPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpIDpjbGFzcz0ieydmYS1jaGVjayc6ICFzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwLCAnZmEtZXhjbGFtYXRpb24tY2lyY2xlJzogc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cH0iIGNsYXNzPSJmYXMiPjwvaT48L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPkxhdGVzdCBCbG9jayBIZWlnaHQ8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICN7eyBzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQgfX0gPHNwYW4gdi1pZj0ibWFzdGVyU3RhdHVzICYmIE51bWJlcihzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQpIDw9IE51bWJlcihtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCkiIGNsYXNzPSJ0ZXh0LW11dGVkIj5vZiB7eyBtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCB9fTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TGF0ZXN0IEJsb2NrIFRpbWU8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAge3sgc3RhdHVzLnN5bmNfaW5mby5sYXRlc3RfYmxvY2tfdGltZSB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIFZhbGlkYXRvciBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5QdWJsaWMgS2V5PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JQdWJLZXkgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+U3RhdHVzPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JTdGF0dXMgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+VG90YWwgU3Rha2U8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YWtlIH19IE1OVDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5Wb3RpbmcgUG93ZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5pY2VOdW0oc3RhdHVzLnZhbGlkYXRvcl9pbmZvLnZvdGluZ19wb3dlcikgfX0gPHNwYW4gY2xhc3M9InRleHQtbXV0ZWQiPm9mIDEwMCwwMDAsMDAwPC9zcGFuPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L2Rpdj4KPHNjcmlwdD4KICAgIG5ldyBWdWUoewogICAgICAgIGVsOiAnI2FwcCcsCiAgICAgICAgZGF0YTogewogICAgICAgICAgICBtYXN0ZXJTdGF0dXM6IG51bGwsCiAgICAgICAgICAgIHN0YXR1czogbnVsbCwKICAgICAgICAgICAgdmVyc2lvbjogbnVsbCwKICAgICAgICAgICAgbmV0X2luZm86IG51bGwsCiAgICAgICAgICAgIGVycm9yOiBudWxsLAogICAgICAgICAgICB2YWxpZGF0b3JQdWJLZXk6ICcuLi4nLAogICAgICAgICAgICB2YWxpZGF0b3JTdGF0dXM6ICcuLi4nLAogICAgICAgICAgICBzdGFrZTogJy4uLicKICAgICAgICB9LAogICAgICAgIG1vdW50ZWQoKSB7CiAgICAgICAgICAgIHRoaXMucmVmcmVzaCgpCiAgICAgICAgfSwKICAgICAgICBtZXRob2RzOiB7CiAgICAgICAgICAgIG5pY2VOdW0obnVtKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gbnVtLnRvU3RyaW5nKCkucmVwbGFjZSgvXEIoPz0oXGR7M30pKyg/IVxkKSkvZywgIiwiKQogICAgICAgICAgICB9LAogICAgICAgICAgICBiYXNlNjRUb0hleChiYXNlNjQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBDcnlwdG9KUy5lbmMuQmFzZTY0LnBhcnNlKGJhc2U2NCkudG9TdHJpbmcoKQogICAgICAgICAgICB9LAogICAgICAgICAgICByZWZyZXNoKCkgewogICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9zdGF0dXMnKSwKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9uZXRfaW5mbycpLAogICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHN0YXR1cywgbmV0X2luZm8pIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLmVycm9yID0gbnVsbAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnN0YXR1cyA9IHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMKICAgICAgICAgICAgICAgICAgICB0aGlzLnZlcnNpb24gPSBzdGF0dXMuZGF0YS5yZXN1bHQudmVyc2lvbgogICAgICAgICAgICAgICAgICAgIHRoaXMubmV0X2luZm8gPSBuZXRfaW5mby5kYXRhLnJlc3VsdAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclB1YktleSA9ICdNcCcgKyB0aGlzLmJhc2U2NFRvSGV4KHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMudmFsaWRhdG9yX2luZm8ucHViX2tleS52YWx1ZSkKCiAgICAgICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCIvLyIgKyB3aW5kb3cubG9jYXRpb24uaG9zdG5hbWUgKyAnOjg4NDEvdmFsaWRhdG9ycycpLAogICAgICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9jYW5kaWRhdGU/cHVia2V5PScgKyB0aGlzLnZhbGlkYXRvclB1YktleSksCiAgICAgICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHZhbGlkYXRvcnMsIGNhbmRpZGF0ZSkgewoKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5zdGFrZSA9IE1hdGgucm91bmQoY2FuZGlkYXRlLmRhdGEucmVzdWx0LnRvdGFsX3N0YWtlIC8gTWF0aC5wb3coMTAsIDE3KSkgLyAxMAoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKHZhbGlkYXRvcnMuZGF0YS5yZXN1bHQuZmluZChmdW5jdGlvbih2YWwpIHsgcmV0dXJuIHZhbC5wdWJrZXkgPT09IHRoaXMudmFsaWRhdG9yUHViS2V5IH0uYmluZCh0aGlzKSkpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ1ZhbGlkYXRpbmcnOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgICAgICAgICAgICAgICAgIH0KCiAgICAgICAgICAgICAgICAgICAgICAgIGlmIChjYW5kaWRhdGUuZGF0YS5yZXN1bHQuc3RhdHVzID09PSAyKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclN0YXR1cyA9ICdDYW5kaWRhdGUnCiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4KICAgICAgICAgICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy52YWxpZGF0b3JTdGF0dXMgPSAnRG93bic7CiAgICAgICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKSkuY2F0Y2goZnVuY3Rpb24oKSAgewogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclN0YXR1cyA9ICdOb3QgZGVjbGFyZWQnOwogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnN0YWtlID0gMDsKICAgICAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpOwoKICAgICAgICAgICAgICAgICAgICBzZXRUaW1lb3V0KHRoaXMucmVmcmVzaCwgNTAwMCkKICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSkpLmNhdGNoKGZ1bmN0aW9uIChyZWFzb24pIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLmVycm9yID0gcmVhc29uLnRvU3RyaW5nKCk7CiAgICAgICAgICAgICAgICAgICAgc2V0VGltZW91dCh0aGlzLnJlZnJlc2gsIDUwMDApCiAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpCgogICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCJodHRwczovL21pbnRlci1ub2RlLTEudGVzdG5ldC5taW50ZXIubmV0d29yay9zdGF0dXMiKS50aGVuKGZ1bmN0aW9uIChtYXN0ZXJTdGF0dXMpIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLm1hc3RlclN0YXR1cyA9IG1hc3RlclN0YXR1cy5kYXRhLnJlc3VsdAogICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSkKPC9zY3JpcHQ+CjwvYm9keT4KPC9odG1sPg==\"") + packr.PackJSONBytes("./html", "index.html", "\"PGh0bWw+CjxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiCiAgICAgICAgICBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIHVzZXItc2NhbGFibGU9bm8sIGluaXRpYWwtc2NhbGU9MS4wLCBtYXhpbXVtLXNjYWxlPTEuMCwgbWluaW11bS1zY2FsZT0xLjAiPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJpZT1lZGdlIj4KICAgIDx0aXRsZT5NaW50ZXIgTm9kZSBHVUk8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL3N0YWNrcGF0aC5ib290c3RyYXBjZG4uY29tL2Jvb3RzdHJhcC80LjEuMC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiCiAgICAgICAgICBpbnRlZ3JpdHk9InNoYTM4NC05Z1ZRNGRZRnd3V1NqSURabkxFV254Q2plU1dGcGhKaXdHUFhyMWpkZEloT2VnaXUxRndPNXFSR3ZGWE9kSlo0IiBjcm9zc29yaWdpbj0iYW5vbnltb3VzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly91c2UuZm9udGF3ZXNvbWUuY29tL3JlbGVhc2VzL3Y1LjEuMS9jc3MvYWxsLmNzcyIgaW50ZWdyaXR5PSJzaGEzODQtTzh3aFMzZmhHMk9uQTVLYXMwWTlsM2NmcG1ZamFwakkwRTR0aGVINGl1TUQrcExoYmY2SkkwaklNZlljSzN5WiIgY3Jvc3NvcmlnaW49ImFub255bW91cyI+CiAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS92dWUiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2F4aW9zLzAuMTguMC9heGlvcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2NyeXB0by1qcy8zLjEuOS0xL2NyeXB0by1qcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHN0eWxlPgoKICAgICAgICAuY2FyZCB7CiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDIwcHg7CiAgICAgICAgfQoKICAgICAgICBodG1sLGJvZHksLmJvZHksI2FwcCB7CiAgICAgICAgICAgIG1pbi1oZWlnaHQ6IDEwMCU7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIC5ib2R5IHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDE1cHg7CiAgICAgICAgfQoKICAgICAgICAudGFibGUgewogICAgICAgICAgICBtYXJnaW4tYm90dG9tOiAwOwogICAgICAgICAgICB0YWJsZS1sYXlvdXQ6IGZpeGVkOwogICAgICAgIH0KCiAgICAgICAgLmNhcmQtaGVhZGVyIHsKICAgICAgICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICAgICAgfQoKICAgICAgICAuY2FyZC1oZWFkZXIgewogICAgICAgICAgICBwYWRkaW5nLWxlZnQ6IDEycHg7CiAgICAgICAgfQoKICAgICAgICAuaCB7CiAgICAgICAgICAgIHdpZHRoOiAyMDBweDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YzZjNmMzsKICAgICAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2NjYzsKICAgICAgICB9CgogICAgICAgIC5iZy1zdWNjZXNzLCAuYmctZGFuZ2VyIHsKICAgICAgICAgICAgY29sb3I6IHdoaXRlOwogICAgICAgIH0KCiAgICAgICAgLmJnLWRhbmdlciB7CiAgICAgICAgICAgIGJvcmRlci1jb2xvcjogI2RjMzU0NSAhaW1wb3J0YW50OwogICAgICAgIH0KCiAgICAgICAgLmJnLXN1Y2Nlc3MgewogICAgICAgICAgICBib3JkZXItY29sb3I6ICMyOGE3NDUgIWltcG9ydGFudDsKICAgICAgICB9CgogICAgICAgIC5mYS1jaGVjayB7CiAgICAgICAgICAgIGNvbG9yOiBncmVlbjsKICAgICAgICB9CgogICAgICAgIC5mYS1leGNsYW1hdGlvbi1jaXJjbGUgewogICAgICAgICAgICBjb2xvcjogcmVkOwogICAgICAgIH0KICAgIDwvc3R5bGU+CjwvaGVhZD4KPGJvZHkgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6ICMzNDNhNDAxYSI+CjxkaXYgaWQ9ImFwcCI+CiAgICA8bmF2IGNsYXNzPSJuYXZiYXIgbmF2YmFyLWV4cGFuZC1sZyBuYXZiYXItZGFyayBiZy1kYXJrIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICA8c3BhbiBjbGFzcz0ibmF2YmFyLWJyYW5kIG1iLTAgaDEiPjxpIGNsYXNzPSJmYXMgZmEtdGVybWluYWwiPjwvaT4gJm5ic3A7IE1pbnRlciBGdWxsIE5vZGUgU3RhdHVzPC9zcGFuPgogICAgICAgIDwvZGl2PgogICAgPC9uYXY+CiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIgYm9keSIgdi1pZj0iZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LWRhbmdlciIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICA8aDQgY2xhc3M9ImFsZXJ0LWhlYWRpbmciPkVycm9yIHdoaWxlIGNvbm5lY3RpbmcgdG8gbG9jYWwgbm9kZTwvaDQ+CiAgICAgICAgICAgIDxwIGNsYXNzPSJtYi0wIj57eyBlcnJvciB9fTwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIGJvZHkgYmctd2hpdGUiIHYtaWY9InN0YXR1cyAmJiAhZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9InJvdyI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5vZGUgSW5mbwogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDx0YWJsZSBjbGFzcz0idGFibGUgY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPk1vbmlrZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YXR1cy5ub2RlX2luZm8ubW9uaWtlciB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+Tm9kZSBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5pZCB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TmV0d29yayBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5uZXR3b3JrIH19PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5NaW50ZXIgVmVyc2lvbjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgdmVyc2lvbiB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+VGVuZGVybWludCBWZXJzaW9uPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyBzdGF0dXMubm9kZV9pbmZvLnZlcnNpb24gfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiIHYtaWY9Im5ldF9pbmZvIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5ldCBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgTGlzdGVuaW5nPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD48aSA6Y2xhc3M9InsnZmEtY2hlY2snOiBuZXRfaW5mby5saXN0ZW5pbmd9IiBjbGFzcz0iZmFzIj48L2k+PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5Db25uZWN0ZWQgUGVlcnM8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5ldF9pbmZvLm5fcGVlcnMgfX0gPGkgOmNsYXNzPSJ7J2ZhLWV4Y2xhbWF0aW9uLWNpcmNsZSc6IG5ldF9pbmZvLm5fcGVlcnMgPCAxfSIgY2xhc3M9ImZhcyI+PC9pPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgU3luY2luZyBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgU3luY2VkPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSJzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwIj5Obzwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSIhc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cCI+WWVzPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpIDpjbGFzcz0ieydmYS1jaGVjayc6ICFzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwLCAnZmEtZXhjbGFtYXRpb24tY2lyY2xlJzogc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cH0iIGNsYXNzPSJmYXMiPjwvaT48L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPkxhdGVzdCBCbG9jayBIZWlnaHQ8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICN7eyBzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQgfX0gPHNwYW4gdi1pZj0ibWFzdGVyU3RhdHVzICYmIHN0YXR1cy5ub2RlX2luZm8ubmV0d29yayA9PSBtYXN0ZXJTdGF0dXMudG1fc3RhdHVzLm5vZGVfaW5mby5uZXR3b3JrICYmIE51bWJlcihzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQpIDw9IE51bWJlcihtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCkiIGNsYXNzPSJ0ZXh0LW11dGVkIj5vZiB7eyBtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCB9fTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TGF0ZXN0IEJsb2NrIFRpbWU8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAge3sgc3RhdHVzLnN5bmNfaW5mby5sYXRlc3RfYmxvY2tfdGltZSB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIFZhbGlkYXRvciBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5QdWJsaWMgS2V5PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JQdWJLZXkgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+U3RhdHVzPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JTdGF0dXMgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+VG90YWwgU3Rha2U8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YWtlIH19IE1OVDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5Wb3RpbmcgUG93ZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5pY2VOdW0oc3RhdHVzLnZhbGlkYXRvcl9pbmZvLnZvdGluZ19wb3dlcikgfX0gPHNwYW4gY2xhc3M9InRleHQtbXV0ZWQiPm9mIDEwMCwwMDAsMDAwPC9zcGFuPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L2Rpdj4KPHNjcmlwdD4KICAgIG5ldyBWdWUoewogICAgICAgIGVsOiAnI2FwcCcsCiAgICAgICAgZGF0YTogewogICAgICAgICAgICBtYXN0ZXJTdGF0dXM6IG51bGwsCiAgICAgICAgICAgIHN0YXR1czogbnVsbCwKICAgICAgICAgICAgdmVyc2lvbjogbnVsbCwKICAgICAgICAgICAgbmV0X2luZm86IG51bGwsCiAgICAgICAgICAgIGVycm9yOiBudWxsLAogICAgICAgICAgICB2YWxpZGF0b3JQdWJLZXk6ICcuLi4nLAogICAgICAgICAgICB2YWxpZGF0b3JTdGF0dXM6ICcuLi4nLAogICAgICAgICAgICBzdGFrZTogJy4uLicKICAgICAgICB9LAogICAgICAgIG1vdW50ZWQoKSB7CiAgICAgICAgICAgIHRoaXMucmVmcmVzaCgpCiAgICAgICAgfSwKICAgICAgICBtZXRob2RzOiB7CiAgICAgICAgICAgIG5pY2VOdW0obnVtKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gbnVtLnRvU3RyaW5nKCkucmVwbGFjZSgvXEIoPz0oXGR7M30pKyg/IVxkKSkvZywgIiwiKQogICAgICAgICAgICB9LAogICAgICAgICAgICBiYXNlNjRUb0hleChiYXNlNjQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBDcnlwdG9KUy5lbmMuQmFzZTY0LnBhcnNlKGJhc2U2NCkudG9TdHJpbmcoKQogICAgICAgICAgICB9LAogICAgICAgICAgICByZWZyZXNoKCkgewogICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9zdGF0dXMnKSwKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9uZXRfaW5mbycpLAogICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHN0YXR1cywgbmV0X2luZm8pIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLmVycm9yID0gbnVsbAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnN0YXR1cyA9IHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMKICAgICAgICAgICAgICAgICAgICB0aGlzLnZlcnNpb24gPSBzdGF0dXMuZGF0YS5yZXN1bHQudmVyc2lvbgogICAgICAgICAgICAgICAgICAgIHRoaXMubmV0X2luZm8gPSBuZXRfaW5mby5kYXRhLnJlc3VsdAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclB1YktleSA9ICdNcCcgKyB0aGlzLmJhc2U2NFRvSGV4KHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMudmFsaWRhdG9yX2luZm8ucHViX2tleS52YWx1ZSkKCiAgICAgICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCIvLyIgKyB3aW5kb3cubG9jYXRpb24uaG9zdG5hbWUgKyAnOjg4NDEvdmFsaWRhdG9ycycpLAogICAgICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9jYW5kaWRhdGU/cHViX2tleT0nICsgdGhpcy52YWxpZGF0b3JQdWJLZXkpLAogICAgICAgICAgICAgICAgICAgIF0pLnRoZW4oYXhpb3Muc3ByZWFkKGZ1bmN0aW9uICh2YWxpZGF0b3JzLCBjYW5kaWRhdGUpIHsKCiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc3Rha2UgPSBNYXRoLnJvdW5kKGNhbmRpZGF0ZS5kYXRhLnJlc3VsdC50b3RhbF9zdGFrZSAvIE1hdGgucG93KDEwLCAxNykpIC8gMTAKCiAgICAgICAgICAgICAgICAgICAgICAgIGlmICh2YWxpZGF0b3JzLmRhdGEucmVzdWx0LmZpbmQoZnVuY3Rpb24odmFsKSB7IHJldHVybiB2YWwucHViX2tleSA9PT0gdGhpcy52YWxpZGF0b3JQdWJLZXkgfS5iaW5kKHRoaXMpKSkgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy52YWxpZGF0b3JTdGF0dXMgPSAnVmFsaWRhdGluZyc7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4KICAgICAgICAgICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKGNhbmRpZGF0ZS5kYXRhLnJlc3VsdC5zdGF0dXMgPT09IDIpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ0NhbmRpZGF0ZScKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybgogICAgICAgICAgICAgICAgICAgICAgICB9CgogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclN0YXR1cyA9ICdEb3duJzsKICAgICAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpKS5jYXRjaChmdW5jdGlvbigpICB7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ05vdCBkZWNsYXJlZCc7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc3Rha2UgPSAwOwogICAgICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSk7CgogICAgICAgICAgICAgICAgICAgIHNldFRpbWVvdXQodGhpcy5yZWZyZXNoLCA1MDAwKQogICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKSkuY2F0Y2goZnVuY3Rpb24gKHJlYXNvbikgewogICAgICAgICAgICAgICAgICAgIHRoaXMuZXJyb3IgPSByZWFzb24udG9TdHJpbmcoKTsKICAgICAgICAgICAgICAgICAgICBzZXRUaW1lb3V0KHRoaXMucmVmcmVzaCwgNTAwMCkKICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSkKCiAgICAgICAgICAgICAgICBheGlvcy5nZXQoImh0dHBzOi8vbWludGVyLW5vZGUtMS50ZXN0bmV0Lm1pbnRlci5uZXR3b3JrL3N0YXR1cyIpLnRoZW4oZnVuY3Rpb24gKG1hc3RlclN0YXR1cykgewogICAgICAgICAgICAgICAgICAgIHRoaXMubWFzdGVyU3RhdHVzID0gbWFzdGVyU3RhdHVzLmRhdGEucmVzdWx0CiAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9KQo8L3NjcmlwdD4KPC9ib2R5Pgo8L2h0bWw+\"") } diff --git a/gui/html/index.html b/gui/html/index.html index fba2a31bc..c96621028 100644 --- a/gui/html/index.html +++ b/gui/html/index.html @@ -145,7 +145,7 @@