all, state: unexpose GenesisDoc, ChainID fields make them accessor methods#676
all, state: unexpose GenesisDoc, ChainID fields make them accessor methods#676
Conversation
state/state.go
Outdated
| // parses the GenesisDoc from that memoizing it for later use. | ||
| // If you'd like to change the value of the GenesisDoc, invoke SetGenesisDoc. | ||
| func (s *State) GenesisDoc() (*types.GenesisDoc, error) { | ||
| s.mtx.Lock() |
There was a problem hiding this comment.
Not really, if the genesisDoc wasn't set to begin with, we have to retrieve it from the DB.
state/state.go
Outdated
| } | ||
|
|
||
| func (s *State) ChainID() (string, error) { | ||
| genDoc, err := s.GenesisDoc() |
There was a problem hiding this comment.
We need to load the genesisDoc from the DB if unset hence the need for it.
There was a problem hiding this comment.
The code you linked is in a function whose comment says that it is used in tests
Lines 343 to 346 in 925696c
There was a problem hiding this comment.
I initially had it like that but in the description of the feature request @ebuchman says
We can provide methods on State to fetch them instead, and we can store them under special keys in the state database
which is why I do the fetches in the DB and store them respectively. Perhaps we could discuss the feature more and get a basic spec there, to come to consensus ;)
There was a problem hiding this comment.
https://github.com/tendermint/tendermint/blob/develop/node/node.go#L141
https://github.com/tendermint/tendermint/blob/develop/state/state.go#L346
the comment needs to be deleted IMHO
There was a problem hiding this comment.
Oh, I see. Forget everything I wrote then. I should be more careful about details
There was a problem hiding this comment.
Need to remember not to do any reviews at night
There was a problem hiding this comment.
Not a problem at all, they are great questions and better for simplicity, so no biggie.
state/state.go
Outdated
|
|
||
| // SetGenesisDoc sets the internal genesisDoc, but doesn't | ||
| // serialize it to the database, until Save is invoked. | ||
| func (s *State) SetGenesisDoc(genDoc *types.GenesisDoc) { |
There was a problem hiding this comment.
second attempt :) what is the purpose of this? if we know that we set genesis doc only once during startup
There was a problem hiding this comment.
IMHO that's coupling too much of the functionality into startup time, and as I had mentioned
the requirements of this package warrant us loading and setting the genesisDoc from and into the DB respectively. Because we are unexposing genesisDoc, SetGenesisDoc is the setter. However, I haven't yet seen any place in our code in which we change the GenesisDoc so you have a point here.
state/state.go
Outdated
| ChainID: genDoc.ChainID, | ||
| db: db, | ||
|
|
||
| cachedGenesisDoc: genDoc, |
There was a problem hiding this comment.
Roger that, thanks.
state/state.go
Outdated
| // its database the JSON marshaled bytes keyed by "genesis-doc", and | ||
| // parses the GenesisDoc from that memoizing it for later use. | ||
| // If you'd like to change the value of the GenesisDoc, invoke SetGenesisDoc. | ||
| func (s *State) GenesisDoc() (*types.GenesisDoc, error) { |
There was a problem hiding this comment.
Don't think that GenesisDoc() or ChainID() methods should return an error. If we know that genesis file is mandatory and TM will not work without it, maybe we can just throw a panic. That way we won't be needing to handle all these errors like https://github.com/tendermint/tendermint/pull/676/files#diff-80343e80650fe43a4394fc26afd5cb3bR508. I want to hear other opinions anyway.
There was a problem hiding this comment.
That's a justified concern, it makes sense to simplify the PR more, which is nice. However, the requirements of the PR request that we instead have a way of alternatively loading the genesisDoc and chainID after loading them from a DB is what warrants us returning an error
since:
- Non-existent data
- Invalid data stored
Otherwise I'd have agreed that the PR can just be simplified to a single set and panic as you advise.
There was a problem hiding this comment.
Agree, we should not return an error. This is a panic scenario.
state/state.go
Outdated
| defer s.mtx.Unlock() | ||
|
|
||
| if s.cachedGenesisDoc == nil { | ||
| retrGenesisDocBytes := s.db.Get(genesisDBKey) |
There was a problem hiding this comment.
what retr stands for? can't we just call it bytes or jsonBytes
There was a problem hiding this comment.
retr --> retrieve*
Sure, I'll update that to your suggestion, thanks.
state/state.go
Outdated
| // should not change | ||
| GenesisDoc *types.GenesisDoc | ||
| ChainID string | ||
| // cachedGenesisDoc is the memoized genesisDoc to cut down |
There was a problem hiding this comment.
I think this comment should go into git commit msg
There was a problem hiding this comment.
I'd rather keep it though as a mention for the future and purpose. Without it I feel like we'll be asking ourselves why we didn't just expose it as GenesisDoc, and also it is already mentioned in the git commit message :)
53f0fcc to
ef17728
Compare
|
Thanks odeke. Note this will be a breaking change so not priority until we're ready for develop to be breaking. Also need to consider exactly how we want to do this - the goal with State is for it to be as simple and non-mutable as possible. |
|
Thanks for the review @ebuchman and welcome back! So we'll actually need your input on:
|
|
We only need to set it once, at startup time. It's not something that would ever change. Only question is whether we should load it from DB every time or if we should cache it in the state. Leaning towards loading from DB every time since it's rarely accessed and we want to keep the State struct simple as possible (ie. no private fields). That said, we could also cache it in the rpc package where it's actually asked for |
|
We actually already do set the GenesisDoc in the rpc, so we may not need it in the state at all! We'll have to move the state.GenesisDoc.ConsensusParams so we can still access those from the state (since they might change), but we knew that was coming |
|
I am gonna work on this right now. |
ef17728 to
03d098c
Compare
Codecov Report
@@ Coverage Diff @@
## develop #676 +/- ##
===========================================
- Coverage 57.85% 57.26% -0.59%
===========================================
Files 82 82
Lines 8436 8453 +17
===========================================
- Hits 4881 4841 -40
- Misses 3142 3188 +46
- Partials 413 424 +11 |
20758c8 to
8bc0098
Compare
| dbm "github.com/tendermint/tmlibs/db" | ||
| "github.com/tendermint/go-wire" | ||
| "github.com/tendermint/tendermint/types" | ||
| . "github.com/tendermint/tmlibs/common" |
|
@melekes sorry for the late reply, I was away from all devices for a whole lot of hours, but sure, please feel free to take over it. |
node/node.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| genDoc, err := genesisDocProvider() |
There was a problem hiding this comment.
this assumes the genesis will always be available, even if we've already run the node. it's probably ok, but not sure we want to make that assumption
There was a problem hiding this comment.
If we start a node, and then later the genesis file is changed, we will not be able to restart the node unless we can recover that genesis file. So I think it's worth having the genesis saved in the database to prevent a certain class of user errors. What do you think?
There was a problem hiding this comment.
don't know. I am trying to come up with a scenario where it will be useful.
1. user loses state
then we'll need original genesis file to MakeGenesisState, but since we store all DBs together, a user will most likely lose genesis state too, so no point of storing it
2. user accidentally changes the genesis json (or some hacker changes it)
then TM will either fail to start due to a parsing error / missing genesis file error OR RPC will be serving incorrect genesis file. Is it worth saving it in the DB for the purpose of preventing such errors? I am not sure.
There was a problem hiding this comment.
Date: Fri Oct 13 13:11:16 2017 +0400
save genesis doc in DB to prevent user errors
https://github.com/tendermint/tendermint/pull/676#discussion_r144411458
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
modified: node/node.go
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
@ node.go:5 @ package node
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
@ node.go:137 @ func NewNode(config *cfg.Config,
return nil, err
}
genDoc, err := genesisDocProvider()
// Get genesis doc
genesisDB, err := dbProvider(&DBContext{"genesis", config})
if err != nil {
return nil, err
}
genDoc, err := loadGenesisDoc(genesisDB)
if err != nil {
genDoc, err = genesisDocProvider()
if err != nil {
return nil, err
}
// save genesis doc to prevent a certain class of user errors (e.g. when it
// was changed, accidentally or not)
err = saveGenesisDoc(genesisDB, genDoc)
if err != nil {
return nil, err
}
}
state := sm.LoadState(stateDB)
if state == nil {
@ node.go:554 @ func (n *Node) DialSeeds(seeds []string) error {
}
//------------------------------------------------------------------------------
var (
genesisDocKey = []byte("genesisDoc")
)
func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) {
bytes := db.Get(genesisDocKey)
if len(bytes) == 0 {
return nil, errors.New("Genesis doc not found")
} else {
var genDoc *types.GenesisDoc
err := json.Unmarshal(bytes, &genDoc)
return genDoc, err
}
}
func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) error {
bytes, err := json.Marshal(genDoc)
if err != nil {
return err
}
db.SetSync(genesisDocKey, bytes)
return nil
}
I wrote the code and can push it if we agree that we need to save genesis doc.
There was a problem hiding this comment.
I'm not actually sure about passing GenesisDoc via a factory method (i.e. GenesisDocProvider). Almost by definition GenesisDoc creation is point-like and immutable, but a factory method implies it that it may need to created multiple times, lazily initialised, or icreated n different contexts that branch. None of these should be true for genesis. I think it would be better to make NewNode take a plain GenesisDoc as a argument (value type to emphasise the immutability.) Callers will just need to call your default provider rather than passing it. All I do for the provider is the busywork of wrapping my GenesisDoc in a func.
I also think that the GenesisDoc is an interesting part of the audit trail of the chain and you should probbly store it and serve it from state reliably.
Also in the world where we are handing provision of genesis doc and database provider to the caller we shouldn't make assumptions about what state is co-located or not or even stored to disk.
Come to think of it I actually think a hash of the GenesisDoc should be mixed into the initial AppHash. I don't think it's the same chain if it didn't start with the same GenesisDoc and validator set, but I suppose the way you have it now allows for some flexibility for the ABCI app in that respect (in next Burrow the initial AppHash is our GenesisDoc hash which includes validator set and time, ChainID is also derived from that)
There was a problem hiding this comment.
wow, thanks for your input! ok, let's persist genesis doc
There was a problem hiding this comment.
As for NewNode taking GenesisDoc as an argument, don't think this is what we want in the end. We call genesisDocProvider only once, then we save genesis doc to the state. If we rewrite the code to allow to pass it as an argument, this would mean the caller would have to retrieve it from some source on every start and that's not what we want.
There was a problem hiding this comment.
what if we change the DBProvider to an interface:
type DBProvider interface{
DB(*DBContext) (dbm.DB, error)
GenesisDoc() GenesisDoc
}
then we can remove the genesisDocProvider, and folks can wrap up their strategies for genesis files and dbs however they want
There was a problem hiding this comment.
meh, this is kinda weird.
Ok I think what we currently have is fine. Let's let it lie for a bit until something genuinely better emerges.
node/node.go
Outdated
| } | ||
| state.Save() | ||
| } else { | ||
| state.SetChainID(genDoc.ChainID) |
There was a problem hiding this comment.
right - we shouldn't have a need for this (but I see this is why the genesis was moved out of the if block. The chainID should be saved somewhere. The ConsensusParams may also change over the life of the chain (not implemented yet), so we don't want to set them back to the genesis params every time we start
node/node.go
Outdated
|
|
||
| // reload the state (it may have been updated by the handshake) | ||
| state = sm.LoadState(stateDB) | ||
| state.SetChainID(genDoc.ChainID) |
state/state.go
Outdated
| // should not change | ||
| GenesisDoc *types.GenesisDoc | ||
| ChainID string | ||
| // See https://github.com/tendermint/tendermint/issues/671. |
There was a problem hiding this comment.
I know Jae doesn't like private mutable data fields in the State. We may need to seek another way to do this ... perhaps the params need to live in the ConsensusState.
Immutable private chainID should be ok.
There was a problem hiding this comment.
perhaps the params need to live in the ConsensusState.
Will these params always be for consensus only (maybe we'll want to add blockchain params or something?).
There was a problem hiding this comment.
Also, if I am not mistaken, we do not persist ConsensusState (as we recreate it using blockstore & WAL). So, how about we store ChainID & Params under a separate key (genesisParamsKey)?
There was a problem hiding this comment.
Ok, I think we should just expose ChainID and Params in here and let them be written every time too.
I'd ask @jaekwon to confirm.
Also if we will want historical params like we have historical validators once we update ABCI to allow changes there.
It would also be amazing if state encoding was extensible so we dont have a breaking change every time we want to change something here!
There was a problem hiding this comment.
Yeah we should definitely expose ChainID.
ConsensusParams is small enough, we can just write it.
state/state.go
Outdated
| } | ||
| state.Save() | ||
| } else { | ||
| state.SetChainID(genDoc.ChainID) |
state/state.go
Outdated
|
|
||
| // SetChainID sets the chain ID. | ||
| func (s *State) SetChainID(chainID string) { | ||
| s.chainID = chainID |
There was a problem hiding this comment.
eh. Maybe it's fine if ChainID is written to disk every time - it's small, and might be worth avoiding this extra step of needing to set it. I can see folks easily forgetting to call this, and everything will probably break without it.
| // create list of them | ||
| pubkeys := make([]crypto.PubKey, N+1) | ||
| pubkeys[0] = state.GenesisDoc.Validators[0].PubKey | ||
| _, val := state.Validators.GetByIndex(0) |
|
This change probably warrants more discussion. Why is it needed for #564 ? They should probably come together since both breaking changes to state. However #564 is something we can do easily, though it's breaking, while this is a bigger change. Question is do we want to rapid fire a series of breaking changes as we iterate through some refactoring here, or should we try to bundle them at lower frequency ... |
because including |
|
Oh I was thinking to use json.RawMessage to avoid that! It's not like we actually need access to the app_options, just to print them! |
oh, my bad then. will push the code shortly. |
state/state.go
Outdated
| defer s.mtx.Unlock() | ||
| s.saveValidatorsInfo() | ||
| s.db.SetSync(stateKey, s.Bytes()) | ||
| s.mtx.Unlock() |
There was a problem hiding this comment.
I prefer defer s.mtx.Unlock() unless there's something to do after unlocking.
``` jaekwon Yeah we should definitely expose ChainID. ConsensusParams is small enough, we can just write it. ``` #676 (comment)
608d175 to
2eba353
Compare
``` jaekwon Yeah we should definitely expose ChainID. ConsensusParams is small enough, we can just write it. ``` #676 (comment)
ebuchman
left a comment
There was a problem hiding this comment.
Tentative approval but see comments/questions
state/state.go
Outdated
| ChainID string | ||
| // consensus parameters used for validating blocks | ||
| // must not be nil | ||
| Params *types.ConsensusParams |
There was a problem hiding this comment.
GenesisDoc uses a pointer value https://github.com/tendermint/tendermint/pull/676/files/2eba353e2ed3da4c1ae18b4bde3e7f853a7a66a3#diff-f6be34387a141f652301c16cf7a5e36cL272 so that's just transferring the field in here. However, to answer your question, usually I'll go with pointers because then I can find out if something was set or not idiomatically with
if g.Params == nil {
}as opposed to
if g.Params == types.ConsensusParams{} {
}or
if reflect.DeepEqual(g.Params, types.ConsensusParams{}) {
}There was a problem hiding this comment.
I think it depends on how often params will be changing. If not very often, we can make it a raw struct. If the opposite, then making it a pointer makes sense for faster updates.
state/state.go
Outdated
| // should not change | ||
| GenesisDoc *types.GenesisDoc | ||
| ChainID string | ||
| // See https://github.com/tendermint/tendermint/issues/671. |
There was a problem hiding this comment.
we can remove this comment now
node/node.go
Outdated
| return nil, err | ||
| } | ||
|
|
||
| genDoc, err := genesisDocProvider() |
There was a problem hiding this comment.
If we start a node, and then later the genesis file is changed, we will not be able to restart the node unless we can recover that genesis file. So I think it's worth having the genesis saved in the database to prevent a certain class of user errors. What do you think?
…thods Fixes #671 Unexpose GenesisDoc and ChainID fields to avoid them being serialized to the DB on every block write/state.Save() A GenesisDoc can now be alternatively written to the state's database, by serializing its JSON as a value of key "genesis-doc". There are now accessors and a setter for these attributes: - state.GenesisDoc() (*types.GenesisDoc, error) - state.ChainID() (string, error) - state.SetGenesisDoc(*types.GenesisDoc) This is a breaking change since it changes how the state's serialization and requires that if loading the GenesisDoc entirely from the database, you'll need to set its value in the database as the GenesisDoc's JSON marshaled bytes.
- remove state#GenesisDoc() method
``` jaekwon Yeah we should definitely expose ChainID. ConsensusParams is small enough, we can just write it. ``` #676 (comment)
also remove the comment
87c52ba to
6469e2c
Compare
| return nil, errors.New("Genesis doc not found") | ||
| } else { | ||
| var genDoc *types.GenesisDoc | ||
| err := json.Unmarshal(bytes, &genDoc) |
There was a problem hiding this comment.
maybe we panic here actually?
There was a problem hiding this comment.
also the double pointer here can be confusing. Isn't it better to:
genDoc := new(types.GenesisDoc)
Unmarshal(bytes, genDoc)
?
There was a problem hiding this comment.
mm.. using *smth seems more standard. I am not used to using new
…ndermint#676) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.16.0 to 1.17.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](bufbuild/buf-setup-action@v1.16.0...v1.17.0) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Fixes #671
Unexpose GenesisDoc and ChainID fields to avoid them being
serialized to the DB on every block write/state.Save()
A GenesisDoc can now be alternatively written to the state's
database, by serializing its JSON as a value of key "genesis-doc".
There are now accessors and a setter for these attributes:
This is a breaking change since it changes how the state's
serialization and requires that if loading the GenesisDoc entirely
from the database, you'll need to set its value in the database
as the GenesisDoc's JSON marshaled bytes.