Here we focus on software-agnostic protocol versions.
The Software Version is covered by SemVer and described elsewhere. It is not relevant to the protocol description, suffice to say that if any protocol version changes, the software version changes, but not necessarily vice versa.
Software version should be included in NodeInfo for convenience/diagnostics.
We are also interested in versioning across different blockchains in a meaningful way, for instance to differentiate branches of a contentious hard-fork. We leave that for a later ADR.
We need to version components of the blockchain that may be independently upgraded. We need to do it in a way that is scalable and maintainable - we can't just litter the code with conditionals.
We can consider the complete version of the protocol to contain the following sub-versions: BlockVersion, P2PVersion, AppVersion. These versions reflect the major sub-components of the software that are likely to evolve together, at different rates, and in different ways, as described below.
The BlockVersion defines the core of the blockchain data structures and should change infrequently.
The P2PVersion defines how peers connect and communicate with eachother - it's not part of the blockchain data structures, but defines the protocols used to build the blockchain. It may change gradually.
The AppVersion determines how we compute app specific information, like the AppHash and the Results.
All of these versions may change over the life of a blockchain, and we need to be able to help new nodes sync up across version changes. This means we must be willing to connect to peers with older version.
Each component of the software is independently versioned in a modular way and its easy to mix and match and upgrade.
Each of BlockVersion, AppVersion, P2PVersion, is a monotonically increasing uint64.
To use these versions, we need to update the block Header, the p2p NodeInfo, and the ABCI.
Block Header should include a Version
struct as its first field like:
type Version struct {
Block uint64
App uint64
}
Here, Version.Block
defines the rules for the current block, while
Version.App
defines the app version that processed the last block and computed
the AppHash
in the current block. Together they provide a complete description
of the consensus-critical protocol.
Since we have settled on a proto3 header, the ability to read the BlockVersion out of the serialized header is unanimous.
Using a Version struct gives us more flexibility to add fields without breaking the header.
The ProtocolVersion struct includes both the Block and App versions - it should serve as a complete description of the consensus-critical protocol.
NodeInfo should include a Version struct as its first field like:
type Version struct {
P2P uint64
Block uint64
App uint64
Other []string
}
Note this effectively makes Version.P2P
the first field in the NodeInfo, so it
should be easy to read this out of the serialized header if need be to facilitate an upgrade.
The Version.Other
here should include additional information like the name of the software client and
it's SemVer version - this is for convenience only. Eg.
tendermint-core/v0.22.8
. It's a []string
so it can include information about
the version of Tendermint, of the app, of Tendermint libraries, etc.
Since the ABCI is responsible for keeping Tendermint and the App in sync, we need to communicate version information through it.
On startup, we use Info to perform a basic handshake. It should include all the version information.
We also need to be able to update versions in the life of a blockchain. The natural place to do this is EndBlock.
Note that currently the result of the Handshake isn't exposed anywhere, as the
handshaking happens inside the proxy.AppConns
abstraction. We will need to
remove the handshaking from the proxy
package so we can call it independently
and get the result, which should contain the application version.
RequestInfo should add support for protocol versions like:
message RequestInfo {
string version
uint64 block_version
uint64 p2p_version
}
Similarly, ResponseInfo should return the versions:
message ResponseInfo {
string data
string version
uint64 app_version
int64 last_block_height
bytes last_block_app_hash
}
The existing version
fields should be called software_version
but we leave
them for now to reduce the number of breaking changes.
Updating the version could be done either with new fields or by using the
existing tags
. Since we're trying to communicate information that will be
included in Tendermint block Headers, it should be native to the ABCI, and not
something embedded through some scheme in the tags. Thus, version updates should
be communicated through EndBlock.
EndBlock already contains ConsensusParams
. We can add version information to
the ConsensusParams as well:
message ConsensusParams {
BlockSize block_size
EvidenceParams evidence_params
VersionParams version
}
message VersionParams {
uint64 block_version
uint64 app_version
}
For now, the block_version
will be ignored, as we do not allow block version
to be updated live. If the app_version
is set, it signals that the app's
protocol version has changed, and the new app_version
will be included in the
Block.Header.Version.App
for the next block.
BlockVersion is included in both the Header and the NodeInfo.
Changing BlockVersion should happen quite infrequently and ideally only for critical upgrades. For now, it is not encoded in ABCI, though it's always possible to use tags to signal an external process to co-ordinate an upgrade.
Note Ethereum has not had to make an upgrade like this (everything has been at state machine level, AFAIK).
P2PVersion is not included in the block Header, just the NodeInfo.
P2PVersion is the first field in the NodeInfo. NodeInfo is also proto3 so this is easy to read out.
Note we need the peer/reactor protocols to take the versions of peers into account when sending messages:
Doing this will be specific to the upgrades being made.
Note we also include the list of reactor channels in the NodeInfo and already don't send messages for channels the peer doesn't understand. If upgrades always use new channels, this simplifies the development cost of backwards compatibility.
Note NodeInfo is only exchanged after the authenticated encryption handshake to ensure that it's private. Doing any version exchange before encrypting could be considered information leakage, though I'm not sure how much that matters compared to being able to upgrade the protocol.
XXX: if needed, can we change the meaning of the first byte of the first message to encode a handshake version? this is the first byte of a 32-byte ed25519 pubkey.
AppVersion is also included in the block Header and the NodeInfo.
AppVersion essentially defines how the AppHash and LastResults are computed.
Restricting peer compatibility based on version is complicated by the need to help old peers, possibly on older versions, sync the blockchain.
We might be tempted to say that we only connect to peers with the same AppVersion and BlockVersion (since these define the consensus critical computations), and a select list of P2PVersions (ie. those compatible with ours), but then we'd need to make accomodations for connecting to peers with the right Block/AppVersion for the height they're on.
For now, we will connect to peers with any version and restrict compatibility solely based on the ChainID. We leave more restrictive rules on peer compatibiltiy to a future proposal.
It may be valuable to support an /unsafe_stop?height=_
endpoint to tell Tendermint to shutdown at a given height.
This could be use by an external manager process that oversees upgrades by
checking out and installing new software versions and restarting the process. It
would subscribe to the relevant upgrade event (needs to be implemented) and call /unsafe_stop
at
the correct height (of course only after getting approval from its user!)