diff --git a/DOCKER/Dockerfile b/DOCKER/Dockerfile index 878a8b8f4e..75ea89ad6e 100644 --- a/DOCKER/Dockerfile +++ b/DOCKER/Dockerfile @@ -93,7 +93,7 @@ COPY --from=compile /src/tenderdash/build/tenderdash /src/tenderdash/build/abcid # You can overwrite these before the first run to influence # config.json and genesis.json. Additionally, you can override # CMD to add parameters to `tenderdash node`. -ENV PROXY_APP=kvstore MONIKER=dockernode CHAIN_ID=dockerchain +ENV PROXY_APP=kvstore MONIKER=dockernode CHAIN_ID=dockerchain ABCI="" COPY ./DOCKER/docker-entrypoint.sh /usr/local/bin/ diff --git a/DOCKER/docker-entrypoint.sh b/DOCKER/docker-entrypoint.sh index bd080c46f5..0863cfd022 100755 --- a/DOCKER/docker-entrypoint.sh +++ b/DOCKER/docker-entrypoint.sh @@ -15,8 +15,14 @@ if [ ! -d "$TMHOME/config" ]; then -e 's/^prometheus\s*=.*/prometheus = true/' \ "$TMHOME/config/config.toml" + if [ -n "$ABCI" ]; then + sed -i \ + -e "s/^abci\s*=.*/abci = \"$ABCI\"/" \ + "$TMHOME/config/config.toml" + fi + jq ".chain_id = \"$CHAIN_ID\" | .consensus_params.block.time_iota_ms = \"500\"" \ - "$TMHOME/config/genesis.json" > "$TMHOME/config/genesis.json.new" + "$TMHOME/config/genesis.json" >"$TMHOME/config/genesis.json.new" mv "$TMHOME/config/genesis.json.new" "$TMHOME/config/genesis.json" fi diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index b7504090ba..ab04ac3c71 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -3,9 +3,9 @@ package kvstore import ( "bytes" "context" - "encoding/json" "errors" "fmt" + "io" "path" "strconv" "time" @@ -228,7 +228,12 @@ func newApplication(stateStore StoreFactory, opts ...OptFunc) (*Application, err defer in.Close() if err := app.LastCommittedState.Load(in); err != nil { - return nil, fmt.Errorf("load state: %w", err) + // EOF means we most likely don't have any state yet + if !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("load state: %w", err) + } else { + app.logger.Debug("no state found, using initial state") + } } app.snapshots, err = NewSnapshotStore(path.Join(app.cfg.Dir, "snapshots")) @@ -264,9 +269,9 @@ func (app *Application) InitChain(_ context.Context, req *abci.RequestInitChain) } // Overwrite state based on AppStateBytes + // Note this is not optimal from memory perspective; use chunked state sync instead if len(req.AppStateBytes) > 0 { - err := json.Unmarshal(req.AppStateBytes, &app.LastCommittedState) - if err != nil { + if err := app.LastCommittedState.Load(bytes.NewBuffer(req.AppStateBytes)); err != nil { return &abci.ResponseInitChain{}, err } } @@ -398,7 +403,8 @@ func (app *Application) FinalizeBlock(_ context.Context, req *abci.RequestFinali appHash := tmbytes.HexBytes(req.Block.Header.AppHash) roundState, ok := app.roundStates[roundKey(appHash, req.Height, req.Round)] if !ok { - return &abci.ResponseFinalizeBlock{}, fmt.Errorf("state with apphash %s not found", appHash) + return &abci.ResponseFinalizeBlock{}, fmt.Errorf("state with apphash %s at height %d round %d not found", + appHash, req.Height, req.Round) } if roundState.GetHeight() != req.Height { return &abci.ResponseFinalizeBlock{}, @@ -530,14 +536,15 @@ func (app *Application) ApplySnapshotChunk(_ context.Context, req *abci.RequestA } if app.offerSnapshot.isFull() { - chunks := app.offerSnapshot.bytes() - err := json.Unmarshal(chunks, &app.LastCommittedState) - if err != nil { + chunks := app.offerSnapshot.reader() + defer chunks.Close() + + if err := app.LastCommittedState.Load(chunks); err != nil { return &abci.ResponseApplySnapshotChunk{}, fmt.Errorf("cannot unmarshal state: %w", err) } + app.logger.Info("restored state snapshot", "height", app.LastCommittedState.GetHeight(), - "json", string(chunks), "apphash", app.LastCommittedState.GetAppHash(), "snapshot_height", app.offerSnapshot.snapshot.Height, "snapshot_apphash", app.offerSnapshot.appHash, diff --git a/abci/example/kvstore/kvstore_test.go b/abci/example/kvstore/kvstore_test.go index 0041c0e4ff..66274fe414 100644 --- a/abci/example/kvstore/kvstore_test.go +++ b/abci/example/kvstore/kvstore_test.go @@ -136,7 +136,8 @@ func TestPersistentKVStoreKV(t *testing.T) { data, err := os.ReadFile(path.Join(dir, "state.json")) require.NoError(t, err) - assert.Contains(t, string(data), fmt.Sprintf(`"%s":"%s"`, key, value)) + + assert.Contains(t, string(data), fmt.Sprintf(`"key":"%s","value":"%s"`, key, value)) } func TestPersistentKVStoreInfo(t *testing.T) { diff --git a/abci/example/kvstore/snapshots.go b/abci/example/kvstore/snapshots.go index e3d7db80ef..271d5cf021 100644 --- a/abci/example/kvstore/snapshots.go +++ b/abci/example/kvstore/snapshots.go @@ -1,12 +1,13 @@ -//nolint:gosec package kvstore import ( "bytes" + "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" @@ -97,20 +98,31 @@ func (s *SnapshotStore) Create(state State) (abci.Snapshot, error) { s.Lock() defer s.Unlock() - bz, err := json.Marshal(state) + height := state.GetHeight() + + filename := filepath.Join(s.dir, fmt.Sprintf("%v.json", height)) + f, err := os.Create(filename) if err != nil { return abci.Snapshot{}, err } - height := state.GetHeight() + defer f.Close() + + hasher := sha256.New() + writer := io.MultiWriter(f, hasher) + + if err := state.Save(writer); err != nil { + f.Close() + // Cleanup incomplete file; ignore errors during cleanup + _ = os.Remove(filename) + return abci.Snapshot{}, err + } + snapshot := abci.Snapshot{ Height: uint64(height), Version: 1, - Hash: crypto.Checksum(bz), - } - err = os.WriteFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", height)), bz, 0644) - if err != nil { - return abci.Snapshot{}, err + Hash: hasher.Sum(nil), } + s.metadata = append(s.metadata, snapshot) err = s.saveMetadata() if err != nil { @@ -216,6 +228,44 @@ func (s *offerSnapshot) bytes() []byte { return buf.Bytes() } +// reader returns a reader for the snapshot data. +func (s *offerSnapshot) reader() io.ReadCloser { + chunks := s.chunks.Values() + reader := &chunkedReader{chunks: chunks} + + return reader +} + +type chunkedReader struct { + chunks [][]byte + index int + offset int +} + +func (r *chunkedReader) Read(p []byte) (n int, err error) { + if r.chunks == nil { + return 0, io.EOF + } + for n < len(p) && r.index < len(r.chunks) { + copyCount := copy(p[n:], r.chunks[r.index][r.offset:]) + n += copyCount + r.offset += copyCount + if r.offset >= len(r.chunks[r.index]) { + r.index++ + r.offset = 0 + } + } + if r.index >= len(r.chunks) { + err = io.EOF + } + return +} + +func (r *chunkedReader) Close() error { + r.chunks = nil + return nil +} + // makeChunkItem returns the chunk at a given index from the full byte slice. func makeChunkItem(chunks *ds.OrderedMap[string, []byte], chunkID []byte) chunkItem { chunkIDStr := hex.EncodeToString(chunkID) diff --git a/abci/example/kvstore/state.go b/abci/example/kvstore/state.go index 0bf2b47a83..8a9e47602c 100644 --- a/abci/example/kvstore/state.go +++ b/abci/example/kvstore/state.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "net/url" dbm "github.com/tendermint/tm-db" @@ -19,8 +20,6 @@ import ( // Caller of State methods should do proper concurrency locking (eg. mutexes) type State interface { dbm.DB - json.Marshaler - json.Unmarshaler // Save writes full content of this state to some output Save(output io.Writer) error @@ -50,7 +49,7 @@ type State interface { } type kvState struct { - dbm.DB + dbm.DB `json:"-"` // Height of the state. Special value of 0 means zero state. Height int64 `json:"height"` InitialHeight int64 `json:"initial_height,omitempty"` @@ -182,98 +181,99 @@ func (state *kvState) UpdateAppHash(lastCommittedState State, _txs types1.Txs, t return nil } +// Load state from the reader. +// It expects json-encoded kvState, followed by all items from the state. +// +// As a special case, io.EOF when reading the header means that the state is empty. func (state *kvState) Load(from io.Reader) error { if state == nil || state.DB == nil { return errors.New("cannot load into nil state") } - stateBytes, err := io.ReadAll(from) - if err != nil { - return fmt.Errorf("kvState read: %w", err) + // We reuse DB as we can use atomic batches to load items. + newState := NewKvState(state.DB, state.InitialHeight).(*kvState) + + decoder := json.NewDecoder(from) + if err := decoder.Decode(&newState); err != nil { + return fmt.Errorf("error reading state header: %w", err) } - if len(stateBytes) == 0 { - return nil // NOOP + + // Load items to state DB + batch := newState.DB.NewBatch() + defer batch.Close() + + if err := resetDB(newState.DB, batch); err != nil { + return err } - err = json.Unmarshal(stateBytes, &state) - if err != nil { - return fmt.Errorf("kvState unmarshal: %w", err) + item := exportItem{} + var err error + for err = decoder.Decode(&item); err == nil; err = decoder.Decode(&item) { + key, err := url.QueryUnescape(item.Key) + if err != nil { + return fmt.Errorf("error restoring state item key %+v: %w", item, err) + } + value, err := url.QueryUnescape(item.Value) + if err != nil { + return fmt.Errorf("error restoring state item value %+v: %w", item, err) + } + + if err := batch.Set([]byte(key), []byte(value)); err != nil { + return fmt.Errorf("error restoring state item %+v: %w", item, err) + } } - return nil -} + if !errors.Is(err, io.EOF) { + return err + } -func (state kvState) Save(to io.Writer) error { - stateBytes, err := json.Marshal(state) - if err != nil { - return fmt.Errorf("kvState marshal: %w", err) + // commit changes + if err := batch.Write(); err != nil { + return fmt.Errorf("error finalizing restore batch: %w", err) } - _, err = to.Write(stateBytes) - if err != nil { - return fmt.Errorf("kvState write: %w", err) + // copy loaded values to the state + state.InitialHeight = newState.InitialHeight + state.Height = newState.Height + state.Round = newState.Round + state.AppHash = newState.AppHash + // apphash cannot be nil,zero-length + if len(state.AppHash) == 0 { + state.AppHash = make(tmbytes.HexBytes, crypto.DefaultAppHashSize) } return nil } -type StateExport struct { - Height *int64 `json:"height,omitempty"` - InitialHeight *int64 `json:"initial_height,omitempty"` - AppHash tmbytes.HexBytes `json:"app_hash,omitempty"` - Items map[string]string `json:"items,omitempty"` // we store items as string-encoded values -} +// Save saves state to the writer. +// First it puts json-encoded kvState, followed by all items from the state. +func (state kvState) Save(to io.Writer) error { + encoder := json.NewEncoder(to) + if err := encoder.Encode(state); err != nil { + return fmt.Errorf("kvState marshal: %w", err) + } -// MarshalJSON implements json.Marshaler -func (state kvState) MarshalJSON() ([]byte, error) { iter, err := state.DB.Iterator(nil, nil) if err != nil { - return nil, err + return fmt.Errorf("error creating state iterator: %w", err) } defer iter.Close() - height := state.Height - initialHeight := state.InitialHeight - apphash := state.GetAppHash() - - export := StateExport{ - Height: &height, - InitialHeight: &initialHeight, - AppHash: apphash, - Items: nil, - } - for ; iter.Valid(); iter.Next() { - if export.Items == nil { - export.Items = map[string]string{} + key := url.QueryEscape(string(iter.Key())) + value := url.QueryEscape(string(iter.Value())) + item := exportItem{Key: key, Value: value} + if err := encoder.Encode(item); err != nil { + return fmt.Errorf("error encoding state item %+v: %w", item, err) } - export.Items[string(iter.Key())] = string(iter.Value()) } - return json.Marshal(&export) + return nil } -// UnmarshalJSON implements json.Unmarshaler. -// Note that it unmarshals only existing (non-nil) values. -// If unmarshaled data contains a nil value (eg. is not present in json), these will stay intact. -func (state *kvState) UnmarshalJSON(data []byte) error { - - export := StateExport{} - if err := json.Unmarshal(data, &export); err != nil { - return err - } - - if export.Height != nil { - state.Height = *export.Height - } - if export.InitialHeight != nil { - state.InitialHeight = *export.InitialHeight - } - if export.AppHash != nil { - state.AppHash = export.AppHash - } - - return state.persistItems(export.Items) +type exportItem struct { + Key string `json:"key"` + Value string `json:"value"` } func (state *kvState) Close() error { @@ -282,23 +282,3 @@ func (state *kvState) Close() error { } return nil } - -func (state *kvState) persistItems(items map[string]string) error { - if items == nil { - return nil - } - batch := state.DB.NewBatch() - defer batch.Close() - - if len(items) > 0 { - if err := resetDB(state.DB, batch); err != nil { - return err - } - for key, value := range items { - if err := batch.Set([]byte(key), []byte(value)); err != nil { - return err - } - } - } - return batch.Write() -} diff --git a/abci/example/kvstore/state_test.go b/abci/example/kvstore/state_test.go index e2ba12102d..e65f14950e 100644 --- a/abci/example/kvstore/state_test.go +++ b/abci/example/kvstore/state_test.go @@ -1,7 +1,7 @@ package kvstore import ( - "encoding/json" + "bytes" "testing" "github.com/stretchr/testify/assert" @@ -27,14 +27,17 @@ func TestStateMarshalUnmarshal(t *testing.T) { assert.NoError(t, state.UpdateAppHash(state, nil, nil)) apphash := state.GetAppHash() - encoded, err := json.MarshalIndent(state, "", " ") + encoded := bytes.NewBuffer(nil) + err := state.Save(encoded) require.NoError(t, err) assert.NotEmpty(t, encoded) - t.Log(string(encoded)) + + t.Log("encoded:", encoded.String()) decoded := NewKvState(dbm.NewMemDB(), 1) - err = json.Unmarshal(encoded, &decoded) + err = decoded.Load(encoded) require.NoError(t, err) + decoded.Print() v1, err := decoded.Get([]byte("key1")) require.NoError(t, err) @@ -44,14 +47,14 @@ func TestStateMarshalUnmarshal(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, []byte("value2"), v2) - v3, err := decoded.Get([]byte("key2")) + v3, err := decoded.Get(key3) require.NoError(t, err) - assert.EqualValues(t, []byte("value2"), v3) + assert.EqualValues(t, value3, v3) assert.EqualValues(t, apphash, decoded.GetAppHash()) } -func TestStateUnmarshal(t *testing.T) { +func TestStateLoad(t *testing.T) { const initialHeight = 12345678 zeroAppHash := make(tmbytes.HexBytes, crypto.DefaultAppHashSize) type keyVals struct { @@ -89,11 +92,10 @@ func TestStateUnmarshal(t *testing.T) { name: "full", encoded: []byte(`{ "height": 6531, - "app_hash": "1C9ECEC90E28D2461650418635878A5C91E49F47586ECF75F2B0CBB94E897112", - "items": { - "key1": "value1", - "key2": "value2" - }}`), + "app_hash": "1C9ECEC90E28D2461650418635878A5C91E49F47586ECF75F2B0CBB94E897112" + } + {"key":"key1","value":"value1"} + {"key":"key2","value":"value2"}`), expectHeight: 6531, expectAppHash: tmbytes.MustHexDecode("1C9ECEC90E28D2461650418635878A5C91E49F47586ECF75F2B0CBB94E897112"), expectKeyVals: []keyVals{ @@ -106,7 +108,7 @@ func TestStateUnmarshal(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { decoded := NewKvState(dbm.NewMemDB(), initialHeight) - err := json.Unmarshal(tc.encoded, &decoded) + err := decoded.Load(bytes.NewBuffer(tc.encoded)) if tc.expectDecodeError { require.Error(t, err, "decode error expected") } else { diff --git a/abci/example/kvstore/tx.go b/abci/example/kvstore/tx.go index 2c10d6e952..18330adad9 100644 --- a/abci/example/kvstore/tx.go +++ b/abci/example/kvstore/tx.go @@ -119,7 +119,7 @@ func execTx(tx types.Tx, roundState State) (abci.ExecTxResult, error) { // execPrepareTx is noop. tx data is considered as placeholder // and is substitute at the PrepareProposal. -func execPrepareTx(_tx []byte) (abci.ExecTxResult, error) { +func execPrepareTx(_ []byte) (abci.ExecTxResult, error) { // noop return abci.ExecTxResult{}, nil } diff --git a/test/e2e/generator/generate.go b/test/e2e/generator/generate.go index c6877afc42..2fbc995cd3 100644 --- a/test/e2e/generator/generate.go +++ b/test/e2e/generator/generate.go @@ -1,14 +1,12 @@ package main import ( - "encoding/json" "fmt" "math/rand" "sort" "strings" "time" - "github.com/dashpay/tenderdash/abci/example/kvstore" e2e "github.com/dashpay/tenderdash/test/e2e/pkg" "github.com/dashpay/tenderdash/types" ) @@ -20,7 +18,11 @@ var ( "topology": {"single", "quad", "large"}, "initialState": { "{}", - `{"items": {"initial01": "a", "initial02": "b", "initial03": "c"}}`, + `{} + {"key":"initial01","value":"a"} + {"key":"initial02","value":"b"} + {"key":"initial03","value":"c"} + `, }, "validators": {"genesis", "initchain"}, @@ -113,13 +115,8 @@ type Options struct { // generateTestnet generates a single testnet with the given options. func generateTestnet(r *rand.Rand, opt map[string]interface{}) (e2e.Manifest, error) { - initialState := kvstore.StateExport{} - if opt["initialState"] != nil { - data := opt["initialState"].(string) - if err := json.Unmarshal([]byte(data), &initialState); err != nil { - return e2e.Manifest{}, fmt.Errorf("unmarshal initialState: %w", err) - } - } + initialState := opt["initialState"].(string) + manifest := e2e.Manifest{ IPv6: ipv6.Choose(r).(bool), InitialState: initialState, diff --git a/test/e2e/networks/dashcore.toml b/test/e2e/networks/dashcore.toml index 41032e045c..aa70916364 100644 --- a/test/e2e/networks/dashcore.toml +++ b/test/e2e/networks/dashcore.toml @@ -2,14 +2,14 @@ # functionality with a single network. initial_height = 1000 -initial_state = { items = { initial01 = "a", initial02 = "b", initial03 = "c" } } +initial_state = '{} { "key":"initial01","value":"a"}{"key":"initial02","value":"b"}{"key":"initial03","value":"c"}' initial_core_chain_locked_height = 3400 queue_type = "priority" log_level = "debug" - -max_block_size = 1048576 # 1 MB -max_evidence_size = 102400 # 100 kB +# Tune block size for TestApp_TxTooBig +max_block_size = 262144 # 0.25 MB +max_evidence_size = 52428 # 50 kB [chainlock_updates] 1000 = 3450 diff --git a/test/e2e/networks/rotate.toml b/test/e2e/networks/rotate.toml index c29b5c4be9..736e56765c 100644 --- a/test/e2e/networks/rotate.toml +++ b/test/e2e/networks/rotate.toml @@ -2,14 +2,15 @@ # functionality with a single network. initial_height = 1000 -initial_state = { items = { initial01 = "a", initial02 = "b", initial03 = "c" } } +initial_state = '{}{ "key": "initial01","value":"a"} {"key":"initial02","value":"b"} {"key":"initial03" ,"value":"c" }' initial_core_chain_locked_height = 3400 -init_app_core_chain_locked_height = 2308 # should override initial_core_chain_locked_height +init_app_core_chain_locked_height = 2308 # should override initial_core_chain_locked_height queue_type = "priority" log_level = "debug" -max_block_size = 1048576 # 1 MB -max_evidence_size = 102400 # 100 kB +# Tune block size for TestApp_TxTooBig +max_block_size = 262144 # 0.25 MB +max_evidence_size = 52428 # 50 kB [chainlock_updates] 1000 = 3450 diff --git a/test/e2e/pkg/manifest.go b/test/e2e/pkg/manifest.go index 7e9a8d52bd..a070b2b398 100644 --- a/test/e2e/pkg/manifest.go +++ b/test/e2e/pkg/manifest.go @@ -7,8 +7,6 @@ import ( "time" "github.com/BurntSushi/toml" - - "github.com/dashpay/tenderdash/abci/example/kvstore" ) // Manifest represents a TOML testnet manifest. @@ -31,7 +29,7 @@ type Manifest struct { // InitialState is an initial set of key/value pairs for the application, // set in genesis. Defaults to nothing. - InitialState kvstore.StateExport `toml:"initial_state"` + InitialState string `toml:"initial_state"` // Validators is the initial validator set in genesis, given as node names // and power (for Dash power must all be set to default power): diff --git a/test/e2e/pkg/testnet.go b/test/e2e/pkg/testnet.go index 29f76c6f1a..44ca245d28 100644 --- a/test/e2e/pkg/testnet.go +++ b/test/e2e/pkg/testnet.go @@ -15,7 +15,6 @@ import ( "github.com/dashpay/dashd-go/btcjson" - "github.com/dashpay/tenderdash/abci/example/kvstore" abci "github.com/dashpay/tenderdash/abci/types" "github.com/dashpay/tenderdash/crypto" "github.com/dashpay/tenderdash/crypto/bls12381" @@ -77,7 +76,7 @@ type Testnet struct { Dir string IP *net.IPNet InitialHeight int64 - InitialState kvstore.StateExport + InitialState string Validators ValidatorsMap ValidatorUpdates map[int64]ValidatorsMap Nodes []*Node diff --git a/test/e2e/runner/setup.go b/test/e2e/runner/setup.go index ac90b00123..b46d101857 100644 --- a/test/e2e/runner/setup.go +++ b/test/e2e/runner/setup.go @@ -5,7 +5,6 @@ import ( "context" "encoding/base64" "encoding/hex" - "encoding/json" "errors" "fmt" "os" @@ -202,13 +201,9 @@ func MakeGenesis(testnet *e2e.Testnet, genesisTime time.Time) (types.GenesisDoc, sort.Slice(genesis.Validators, func(i, j int) bool { return strings.Compare(genesis.Validators[i].Name, genesis.Validators[j].Name) == -1 }) - if len(testnet.InitialState.Items) > 0 { - appState, err := json.Marshal(testnet.InitialState) - if err != nil { - return genesis, err - } - genesis.AppState = appState - } + + genesis.AppState = []byte(testnet.InitialState) + return genesis, genesis.ValidateAndComplete() } diff --git a/test/e2e/tests/app_test.go b/test/e2e/tests/app_test.go index d302242c1f..470c523bfe 100644 --- a/test/e2e/tests/app_test.go +++ b/test/e2e/tests/app_test.go @@ -6,13 +6,18 @@ import ( "fmt" "math/big" "math/rand" + "os" + "sort" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + db "github.com/tendermint/tm-db" "github.com/dashpay/tenderdash/abci/example/code" + "github.com/dashpay/tenderdash/abci/example/kvstore" + tmbytes "github.com/dashpay/tenderdash/libs/bytes" tmrand "github.com/dashpay/tenderdash/libs/rand" "github.com/dashpay/tenderdash/rpc/client/http" e2e "github.com/dashpay/tenderdash/test/e2e/pkg" @@ -26,17 +31,24 @@ const ( // Tests that any initial state given in genesis has made it into the app. func TestApp_InitialState(t *testing.T) { testNode(t, func(ctx context.Context, t *testing.T, node e2e.Node) { - if len(node.Testnet.InitialState.Items) == 0 { - return - } client, err := node.Client() require.NoError(t, err) - for k, v := range node.Testnet.InitialState.Items { - resp, err := client.ABCIQuery(ctx, "", []byte(k)) + state := kvstore.NewKvState(db.NewMemDB(), 0) + err = state.Load(bytes.NewBufferString(node.Testnet.InitialState)) + require.NoError(t, err) + iter, err := state.Iterator(nil, nil) + require.NoError(t, err) + + for iter.Valid() { + k := iter.Key() + v := iter.Value() + resp, err := client.ABCIQuery(ctx, "", k) require.NoError(t, err) - assert.Equal(t, k, string(resp.Response.Key)) - assert.Equal(t, v, string(resp.Response.Value)) + assert.Equal(t, k, resp.Response.Key) + assert.Equal(t, v, resp.Response.Value) + + iter.Next() } }) } @@ -195,12 +207,61 @@ func TestApp_Tx(t *testing.T) { // when I submit them to the node, // then the first transaction should be committed before the last one. func TestApp_TxTooBig(t *testing.T) { - const timeout = 60 * time.Second + // Pair of txs, last must be in block later than first + type txPair struct { + firstTxHash tmbytes.HexBytes + lastTxHash tmbytes.HexBytes + } + + /// timeout for broadcast to single node + const broadcastTimeout = 5 * time.Second + /// Timeout to read response from single node + const readTimeout = 1 * time.Second + /// Time to process whole mempool + const includeInBlockTimeout = 75 * time.Second + + mainCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + testnet := loadTestnet(t) + nodes := testnet.Nodes + + if name := os.Getenv("E2E_NODE"); name != "" { + node := testnet.LookupNode(name) + require.NotNil(t, node, "node %q not found in testnet %q", name, testnet.Name) + nodes = []*e2e.Node{node} + } else { + sort.Slice(nodes, func(i, j int) bool { + return nodes[i].Name < nodes[j].Name + }) + } + + // we will use last client to check if txs were included in block, so we + // define it outside the loop + var client *http.HTTP + outcome := make([]txPair, 0, len(nodes)) + + start := time.Now() + /// Send to each node more txs than we can fit into block + for _, node := range nodes { + ctx, cancel := context.WithTimeout(mainCtx, broadcastTimeout) + defer cancel() + + if ctx.Err() != nil { + t.Fatalf("context canceled before broadcasting to all nodes") + } + node := *node + + if node.Stateless() { + continue + } + + t.Logf("broadcasting to %s", node.Name) - testNode(t, func(ctx context.Context, t *testing.T, node e2e.Node) { session := rand.Int63() - client, err := node.Client() + var err error + client, err = node.Client() require.NoError(t, err) // FIXME: ConsensusParams is broken for last height, this is just workaround @@ -237,9 +298,26 @@ func TestApp_TxTooBig(t *testing.T) { assert.NoError(t, err, "failed to broadcast tx %06x", i) } - lastTxHash := tx.Hash() + outcome = append(outcome, txPair{ + firstTxHash: firstTxHash, + lastTxHash: tx.Hash(), + }) + } + + t.Logf("submitted txs in %s", time.Since(start).String()) + + successful := 0 + // now we check if these txs were committed within timeout + require.Eventuallyf(t, func() bool { + failed := false + successful = 0 + for _, item := range outcome { + ctx, cancel := context.WithTimeout(mainCtx, readTimeout) + defer cancel() + + firstTxHash := item.firstTxHash + lastTxHash := item.lastTxHash - require.Eventuallyf(t, func() bool { // last tx should be committed later than first lastTxResp, err := client.Tx(ctx, lastTxHash, false) if err == nil { @@ -262,22 +340,17 @@ func TestApp_TxTooBig(t *testing.T) { ) assert.Less(t, firstTxResp.Height, lastTxResp.Height, "first tx should in block before last tx") - return true + successful++ + } else { + failed = true } + } - return false - }, - timeout, // timeout - time.Second, // interval - "submitted tx %X wasn't committed after %v", - lastTxHash, timeout, - ) - - // abciResp, err := client.ABCIQuery(ctx, "", []byte(lastTxKey)) - // require.NoError(t, err) - // assert.Equal(t, code.CodeTypeOK, abciResp.Response.Code) - // assert.Equal(t, lastTxKey, string(abciResp.Response.Key)) - // assert.Equal(t, lastTxHash, types.Tx(abciResp.Response.Value).Hash()) - }) - + return !failed + }, + includeInBlockTimeout, // timeout + time.Second, // interval + "submitted transactions were not committed after %s", + includeInBlockTimeout.String(), + ) } diff --git a/types/genesis.go b/types/genesis.go index c98872fdd9..3af46b108e 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -75,7 +75,7 @@ type GenesisDoc struct { ConsensusParams *ConsensusParams Validators []GenesisValidator AppHash tmbytes.HexBytes - AppState json.RawMessage + AppState []byte // dash fields InitialCoreChainLockedHeight uint32 `json:"initial_core_chain_locked_height"` @@ -92,7 +92,7 @@ type genesisDocJSON struct { ConsensusParams *ConsensusParams `json:"consensus_params,omitempty"` Validators []GenesisValidator `json:"validators,omitempty"` AppHash tmbytes.HexBytes `json:"app_hash,omitempty"` - AppState json.RawMessage `json:"app_state,omitempty"` + AppState []byte `json:"app_state,omitempty"` // dash fields InitialCoreChainLockedHeight uint32 `json:"initial_core_chain_locked_height,omitempty"` diff --git a/types/genesis_test.go b/types/genesis_test.go index 92a918f01d..344c189fb8 100644 --- a/types/genesis_test.go +++ b/types/genesis_test.go @@ -2,6 +2,7 @@ package types import ( + "encoding/base64" "encoding/json" "fmt" "os" @@ -202,7 +203,8 @@ func TestGenesisCorrect(t *testing.T) { func TestBasicGenesisDoc(t *testing.T) { // test a good one by raw json - genDocBytes := []byte( + appState := base64.StdEncoding.AppendEncode(nil, []byte(`{"account_owner": "Bob"}`)) + genDocBytes := []byte(fmt.Sprintf( `{ "genesis_time": "0001-01-01T00:00:00Z", "chain_id": "test-chain-QDKdJr", @@ -222,7 +224,7 @@ func TestBasicGenesisDoc(t *testing.T) { "validator_quorum_hash":"43FF39CC1F41B9FC63DFA5B1EDF3F0CA3AD5CAFAE4B12B4FE9263B08BB50C4CC", "validator_quorum_type":100, "app_hash":"", - "app_state":{"account_owner": "Bob"}, + "app_state":"%s", "consensus_params": { "synchrony": {"precision": "1", "message_delay": "10"}, "timeout": { @@ -237,8 +239,8 @@ func TestBasicGenesisDoc(t *testing.T) { "block": {"max_bytes": "100"}, "evidence": {"max_age_num_blocks": "100", "max_age_duration": "10"} } - }`, - ) + }`, appState, + )) _, err := GenesisDocFromJSON(genDocBytes) assert.NoError(t, err, "expected no error for good genDoc json") diff --git a/types/vote_extension.go b/types/vote_extension.go index 784da379a5..c141df34aa 100644 --- a/types/vote_extension.go +++ b/types/vote_extension.go @@ -482,10 +482,7 @@ func (e ThresholdRawVoteExtension) SignItem(_ string, height int64, round int32, // that reversal. msgHash := tmbytes.Reverse(ext.Extension) - signItem, err := NewSignItemFromHash(quorumType, quorumHash, signRequestID, msgHash), nil - if err != nil { - return SignItem{}, err - } + signItem := NewSignItemFromHash(quorumType, quorumHash, signRequestID, msgHash) // signItem.Msg left empty by purpose, as we don't want hash to be checked in Verify() return signItem, nil