This post expands on an idea proposed by @nearmax here.
Note: This is not yet a full proposal. Please feel free to suggest alternatives.
Background
Currently protocol config parameters are all part of the protocol. These parameters range from the gas cost of various actions and host functions to maximum number of validators allowed by the protocol. Since they are part of the protocol, changing any parameter requires a protocol upgrade, which usually takes a while to be implemented and released. For example, in this PR, we reduced the storage cost by 10x through a protocol change.
Motivation
Protocol changes take time to implement and stabilize. As a result, even changing one parameter in the config on mainnet may take months, which slows us down unnecessarily. In addition, currently there is no way for smart contracts to read those protocol config parameters and some have to be hardcoded in the sdk, which makes changing them a more cumbersome process. Therefore, I propose that we store protocol config in a special smart contract to allow faster iterations and make it easier for smart contracts to read those parameters.
Design
There is a special contract protocolconfig
that holds all the protocol config parameters (essentially what is stored in ProtocolConfig
today). This contract has getter and setter for each parameter. For example,
/// Get the current storage price per byte
fn get_storage_amount_per_byte(&self) -> Balance;
/// Set the storage price to `new_price`.
fn set_storage_amount_per_byte(&mut self, new_price: Balance);
Anyone can call the getters to get the current values. For setters, we follow the same model that Aurora uses: initially the account would have a full access key that allows us to iterate fast. Later on we will implement a voting contract such that a value can only be changed if more than 2/3 of the validators voted yes. For example, let’s say that we have a contract protocolconfig-vote
that allows validators to vote on proposals to change the protocol config parameters. The setters could be implemented as
fn set_storage_amount_per_byte(&mut self, new_price: Balance) {
assert_eq!(env::predecessor_id(), "protocol_config-vote".to_string());
self.storage_amount_per_byte = new_price;
}
This contract (protocolconfig
) lives in a global shard. More concretely, there is a special shard that is tracked by every node (including nodes who otherwise track no shards) and protocolconfig
is the only account (for now) on that shard.
On the protocol side, we store a copy of ProtocolConfig
and update it in finalize_epoch
by reading from the storage of this special contract. Since the shard that protocolconfig
lives on is global, every node can access the latest protocol config. This also allows us to implement different logic based on which fields have changed. For example, if the layout of the shards has changed, then we can trigger the logic responsible for resharding.
Side effects
- Having a global shard has profound consequences and it is something we cannot easily get rid of in the future.
- While contracts can now read the latest protocol config parameters, it still involves a cross-contract call at least once per epoch. While we could provide utility functions to do that, someone needs to ping the contract to trigger the call once every epoch, which may result in suboptimal devx.
- To read the config parameters from within the blockchain client (
nearcore
for example) becomes a bit more involved since it requires reading the storage of a specific contract.
Alternatives
We don’t have to introduce a global shard to store the protocol config contract. For example, we could add a field protocol_config_hash
which is the hash of the struct to block headers to guarantee that validators agree on the protocol config. However, having a global shard is a more general solution that allows us to easily store more such contracts in the future without having to modify the structure of block or block header.