Introducing Global Constants

By Daniel Hines on 6 September, 2021

One of the chief complaints of smart contract developers on Tezos is running out of bytes - you have some awesome new application you want to write, but there's no way to write the code such that it fits within Tezos' size limit for contracts. In protocol Hangzhou, we introduce a small but powerful feature called global constants to address this issue.

The "global" in global constants refers to the fact that any Tezos user can register a Micheline value in a new table built into the protocol by submitting a new register_global_constant operation and paying the cost of storage. The values are indexed by their hash, using the same algorithm used to hash big_map keys. This forms a kind of content-addressable global "library" of Micheline expressions that can be referenced and used in smart contracts. Note I say Micheline, the data format, not Michelson the programming language (check out the respective Micheline and Michelson docs if the distinction isn't clear). Values stored in the table are not first type-checked, so you can store whatever you want, but the responsibility of using the values correctly is on the user.

Once registered, a constant can be used in a contract with a new constant primitive that takes a single valid hash as a string - something like constant "expruQN5r2umbZVHy6WynYM8f71F8zS4AERz9bugF8UkPBEqrHLuU8" (where expruQQN5... is the hash of the Micheline integer value 999). Global constants work just like macros - an expression like consant "<exprhash>" is the macro, and it expands to the value stored in table. Any Micheline node in the source code of a Michelson contract can be replaced with a constant. The macro is expanded before execution, but the protocol only stores store the unexpanded form, allowing you to originate much larger contracts - about five times larger in Hangzhou's configuration (more on that below).

A Brief Example

Let's work through an example. Suppose we're writing a contract where the type of the parameter is very large, and repeated often in the code (a common case, especially in contracts compiled from a higher-level language). For the sake of simplicity, we'll use the type unit, represented by a single Micheline primitive - but in practice it could be much larger expression.

Here's an example of a contract:

parameter unit; storage unit ; code { CDR; LAMBDA unit unit { }; SWAP; APPLY; NIL operation; PAIR }

We could register the Micheline expression unit for reference from anywhere in the contract:

$ tezos-client register global constant unit from bootstrap1 --burn-cap 0.01675 Node is bootstrapped. Estimated gas: 1440 units (will add 100 for safety) Estimated storage: 67 bytes added (will add 20 for safety) Operation successfully injected in the node. Operation hash is 'ooKXF2SkqBeKJtpFgoUuLLYXiqC3K2Khatz13XTecDBqwfufmFy' NOT waiting for the operation to be included. Use command tezos-client wait for ooKXF2SkqBeKJtpFgoUuLLYXiqC3K2Khatz13XTecDBqwfufmFy to be included --confirmations 5 --branch BLockGenesisGenesisGenesisGenesisGenesisCCCCCeZiLHU and/or an external block explorer to make sure that it has been included. This sequence of operations was run: Manager signed operations: From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx Fee to the baker: ꜩ0.000384 Expected counter: 1 Gas limit: 1540 Storage limit: 87 bytes Balance updates: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ................ -ꜩ0.000384 fees(the baker who will include this operation,0) ... +ꜩ0.000384 Register Global: Value: unit This global constant registration was successfully applied Balance updates: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ0.01675 Consumed gas: 1440 Storage size: 67 bytes Global address: exprvKFFbc7SnPjkPZgyhaHewQhmrouNjNae3DpsQ8KuADn9i2WuJ8

You can see in the operation receipt the global address for unit is exprvKFFbc7SnPjkPZgyhaHewQhmrouNjNae3DpsQ8KuADn9i2WuJ8. and replace each instant with a constant, compressing the size of the contract.

parameter (constant "exprvKFFbc7SnPjkPZgyhaHewQhmrouNjNae3DpsQ8KuADn9i2WuJ8"); storage (constant "exprvKFFbc7SnPjkPZgyhaHewQhmrouNjNae3DpsQ8KuADn9i2WuJ8") ; code { CDR; LAMBDA (constant "exprvKFFbc7SnPjkPZgyhaHewQhmrouNjNae3DpsQ8KuADn9i2WuJ8") (constant "exprvKFFbc7SnPjkPZgyhaHewQhmrouNjNae3DpsQ8KuADn9i2WuJ8") { }; SWAP; APPLY; NIL operation; PAIR }

(In this example the expression unit is actually smaller than constant representing it, but you get the idea).

Constants can refer to other constants, and they'll be expanded recursively at type checking time. However, constant expansion is gas metered, so there is a protocol-dependent limit on the total size of a fully-expanded constant. The depth of a fully-expanded constant is limited to 10,000. Additionally, to protect the chain in non-gas-metered situations, there is a hard limit on both the total number of nodes and the number of bytes in a fully expanded constant. These are controlled by protocol constants, and in Hangzhou are set to 50,000 each.

As the first release of the feature, there are no RPC endpoints for querying which constants have been registered. However, you can always calculate the hash of an expression, and we suggest indexers use the operation receipts to map registered expressions to their hashes as a stop gap until proper RPC endpoints are implemented.

Other Use Cases

While originating bigger contracts is the primary use case, we hope to see other applications as well. One idea is fully audited "trusted hashes" for critical Michelson code that can easily be integrated into a wide array of contracts - think FA2 library code. If you have ideas for how cool applications or thoughts on how the global constant feature could be improved or expanded, let us know!


Interested readers can find out more about the usage of constants in this docs MR, or start at the beginning with the TZIP and MR

Happy hashing!

If you want to know more about Marigold, please follow us on social media (Twitter, Reddit, Linkedin)!