perf: parse chain-id from big genesis file could be slow (backport #18204) (#18267)

Co-authored-by: yihuang <huang@crypto.com>
Co-authored-by: marbar3778 <marbar3778@yahoo.com>
This commit is contained in:
mergify[bot] 2023-10-26 12:46:39 +02:00 committed by GitHub
parent 9c6c648436
commit d8957530a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 207 additions and 2 deletions

View File

@ -50,6 +50,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (x/staking) [#18035](https://github.com/cosmos/cosmos-sdk/pull/18035) Hoisted out of the redelegation loop, the non-changing validator and delegator addresses parsing.
* (keyring) [#17913](https://github.com/cosmos/cosmos-sdk/pull/17913) Add `NewAutoCLIKeyring` for creating an AutoCLI keyring from a SDK keyring.
* (x/consensus) [#18041](https://github.com/cosmos/cosmos-sdk/pull/18041) Let `ToProtoConsensusParams()` return an error.
* [#18204](https://github.com/cosmos/cosmos-sdk/pull/18204) Use streaming json parser to parse chain-id from genesis file.
### Bug Fixes

View File

@ -475,12 +475,16 @@ func DefaultBaseappOptions(appOpts types.AppOptions) []func(*baseapp.BaseApp) {
chainID := cast.ToString(appOpts.Get(flags.FlagChainID))
if chainID == "" {
// fallback to genesis chain-id
appGenesis, err := genutiltypes.AppGenesisFromFile(filepath.Join(homeDir, "config", "genesis.json"))
reader, err := os.Open(filepath.Join(homeDir, "config", "genesis.json"))
if err != nil {
panic(err)
}
defer reader.Close()
chainID = appGenesis.ChainID
chainID, err = genutiltypes.ParseChainIDFromGenesis(reader)
if err != nil {
panic(fmt.Errorf("failed to parse chain-id from genesis file: %w", err))
}
}
snapshotStore, err := GetSnapshotStore(appOpts)

View File

@ -0,0 +1,69 @@
package types
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"github.com/cometbft/cometbft/types"
)
const ChainIDFieldName = "chain_id"
// ParseChainIDFromGenesis parses the `chain_id` from a genesis JSON file, aborting early after finding the `chain_id`.
// For efficiency, it's recommended to place the `chain_id` field before any large entries in the JSON file.
// Returns an error if the `chain_id` field is not found.
func ParseChainIDFromGenesis(r io.Reader) (string, error) {
dec := json.NewDecoder(r)
t, err := dec.Token()
if err != nil {
return "", err
}
if t != json.Delim('{') {
return "", fmt.Errorf("expected {, got %s", t)
}
for dec.More() {
t, err = dec.Token()
if err != nil {
return "", err
}
key, ok := t.(string)
if !ok {
return "", fmt.Errorf("expected string for the key type, got %s", t)
}
if key == ChainIDFieldName {
var chainID string
if err := dec.Decode(&chainID); err != nil {
return "", err
}
if err := validateChainID(chainID); err != nil {
return "", err
}
return chainID, nil
}
// skip the value
var value json.RawMessage
if err := dec.Decode(&value); err != nil {
return "", err
}
}
return "", errors.New("missing chain-id in genesis file")
}
func validateChainID(chainID string) error {
if strings.TrimSpace(chainID) == "" {
return errors.New("genesis doc must include non-empty chain_id")
}
if len(chainID) > types.MaxChainIDLen {
return fmt.Errorf("chain_id in genesis doc is too long (max: %d)", types.MaxChainIDLen)
}
return nil
}

View File

@ -0,0 +1,130 @@
package types_test
import (
_ "embed"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/x/genutil/types"
)
//go:embed testdata/parse_chain_id.json
var BenchmarkGenesis string
func TestParseChainIDFromGenesis(t *testing.T) {
testCases := []struct {
name string
json string
expChainID string
expError string
}{
{
"success",
`{
"state": {
"accounts": {
"a": {}
}
},
"chain_id": "test-chain-id"
}`,
"test-chain-id",
"",
},
{
"nested",
`{
"state": {
"accounts": {
"a": {}
},
"chain_id": "test-chain-id"
}
}`,
"",
"missing chain-id in genesis file",
},
{
"not exist",
`{
"state": {
"accounts": {
"a": {}
}
},
"chain-id": "test-chain-id"
}`,
"",
"missing chain-id in genesis file",
},
{
"invalid type",
`{
"chain-id": 1,
}`,
"",
"invalid character '}' looking for beginning of object key string",
},
{
"invalid json",
`[ " ': }`,
"",
"expected {, got [",
},
{
"empty chain_id",
`{"chain_id": ""}`,
"",
"genesis doc must include non-empty chain_id",
},
{
"whitespace chain_id",
`{"chain_id": " "}`,
"",
"genesis doc must include non-empty chain_id",
},
{
"chain_id too long",
`{"chain_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}`,
"",
"chain_id in genesis doc is too long",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
chainID, err := types.ParseChainIDFromGenesis(strings.NewReader(tc.json))
if tc.expChainID == "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expError)
} else {
require.NoError(t, err)
require.Equal(t, tc.expChainID, chainID)
}
})
}
}
func BenchmarkParseChainID(b *testing.B) {
expChainID := "cronosmainnet_25-1"
b.ReportAllocs()
b.Run("new", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
chainID, err := types.ParseChainIDFromGenesis(strings.NewReader(BenchmarkGenesis))
require.NoError(b, err)
require.Equal(b, expChainID, chainID)
}
})
b.Run("old", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
doc, err := types.AppGenesisFromReader(strings.NewReader(BenchmarkGenesis))
require.NoError(b, err)
require.Equal(b, expChainID, doc.ChainID)
}
})
}

File diff suppressed because one or more lines are too long