The overall purpose of a "Constitution Script" is to define in Plutus, that part of the Cardano Constitution which is possible to be automated as a smart contract.
Currently, in this repository, we are focusing on defining a constitution
script to check that a ParameterChange proposal or TreasuryWithdrawals proposal
is "constitutional". In the future, we may
enhance the script to automate more parts of the Cardano Constitution.
The script is written in the high-level PlutusTx language, which
is subsequently compiled to Untyped Plutus Core and executed
on then chain upon every new Governance Proposal.
FIXME: any governance proposal?
Being a smart contract, the constitution script is a validator function of the form:
const_script :: BuiltinData -> BuiltinUnitThe sole argument to this function is the BuiltinData-encoded V3.ScriptContext.
Note the absence of the 2 extra arguments, previously known as Datum argument and the Redeemer argument.
Since V3 and CIP-69, the Datum and Redeemer values are not passed anymore as separate function arguments,
but embedded inside the V3.ScriptContext argument.
The "proposal under investigation" is also embedded inside the ScriptContext.
Datum is not provided for the "constitution script", since it is not a spending validator.
Redeemer will be provided to the "constitution script", but the current script
implementations ignore any value given to it (see Clause D).
When the script is fully applied, one of the 2 cases can happen:
-
The script executes successfully by returning
BuiltinUnit, which means that the Proposal under investigation is constitutional (the next step of the process would be to vote on this proposal, but this is out-of-scope of this repository). -
The script fails with an error. There can be many different reasons for a script error:
- logical error in the constitution script (in config and/or engine, see next section)
- Proposal is malformed
- Proposal violates a constitution rule.
- bug in the CEK evaluator
Irregardless of the specific error, the outcome is the same: the proposal would be un-constitutional (and no further steps will be taken for this proposal).
The constitution rules (a.k.a. guardrails) may change in the future, which may require that the constitution script be also accordingly updated, and re-submitted on the chain so as to be "enacted".
To minimise the chances of introducing bugs when the constititution script has to be updated,
we decided to separate the fixed "logic part" of the script from the
possibly evolving part, i.e. the "constitution rules".
For this reason, the constitution rules are separately given as a PlutusTx ADT
(with type Config), which when applied to the fixed-logic part (named engine for short)
yields the actual constitution script:
data Config = ... -- see Config/Types.hs
const_engine :: Config -> V3.ScriptContext -> BuiltinUnit
const_engine = ...fixed logic...
const_script :: V3.ScriptContext -> BuiltinUnit
const_script = const_engine my_configIn other words, the Config is "eliminated" statically at compile-time, by partially applying it to
the constitution engine.
These constitution rules can be thought of as predicates (PlutusTx functions that return Bool)
over the proposed values. We currently have 3 such predicates:
minValue jsonValue proposedValue = jsonValue Tx.=< proposedValue
maxValue jsonValue proposedValue = jsonValue Tx.>= proposedValue
notEqual jsonValue proposedValue = jsonValue Tx./= proposedValueAn alternative & preferred method than constructing a Config value, is to
edit a configuration file that contains the "constitution rules" laid out in JSON.
Its default location is at data/defaultConstitution.json,
with its expected JSON schema specified at data/defaultConstitution.schema.json.
After editing this JSON configuration file and re-compiling the cabal package, the JSON will be statically translated
to a Config PlutusTx-value and applied to the engine to yield a new script.
This is the preferred method because, first, it does not require any prior PlutusTx/Haskell knowledge
and second, there can be extra sanity checks applied: e.g. when parsing/translating the JSON to Config
or when using an external JSON schema validator.
In case of a ParameterChange governance action, the ledger will construct out of the proposed parameters, a ChangedParameters value,
encode it as BuiltinData, then pass it onto us (the Constitution script) inside the V3.ScriptContext.
The ChangedParameters valueis decoded as aTx.AssocMap`:
ChangedParameters => Tx.AssocMap ChangedParamId ChangedParamValue
ChangedParamId => I Integer
ChangedParamValue => I Integer
, or => (I Integer, Cons(I Integer, Nil)) -- a Rational numerator, denominator, a.k.a unit_interval
, or => List(ChangedParamValue) -- an arbitrary-length, heterogeneous list of (integer, unit_interval) values
Integer is the usual arbitrary-precision Integer of Haskell/PlutusTx.
There is no other type of a changed parameter (e.g. nested-list parameter), so the script implementations will fail on any other format.
There can be many different implementations of the logic (engine) made for the constitution script. A particular script implementation behaves correctly (is valid), when it complies with all the following clauses:
- S01. If
thisGovActionisTreasuryWithdrawals _ _, thenPASSand no checks left. - S02. If
thisGovActionis(ParameterChange _ proposedParams _)and the decodedproposedParamsis an empty list (a.k.a. Tx.AssocMap), thenUNSPECIFIED. - S03. If
thisGovActionis(ParameterChange _ proposedParams _)and decodedproposedParamsis a non-empty list, start checking each proposed parameter in the list against theConfig. - S04. The Redeemer
BuiltinDatavalue isUNSPECIFIED. - S05. In all other cases of decoded
ScriptContextreturnFAIL. - S06. Lookup in the
Configthe rules associated to the currentproposedParam's id and test these rules against theproposedParam's value. If one or more tests fail =>FAIL. Otherwise, set the nextproposedParamin the list as the current one and continue to (F). - S07. If no
proposedParamto check is left in theproposedParamslist,PASSand no more checks left. - S08. If a
proposedParam's id is not found in theConfig=>FAIL. This can happen if the parameter is unknown, or is known but wrongfully omitten from the config file. - S09. If the
Configsays{type: any}under a givenproposedParam, then do not try to decode the value of theproposedParam, but simplyPASSand continue to next check. - S10. In all other cases of
{type: integer/unit_interval/list}, decode theproposedParamvalue according to the expected type (see "ChangedParameters Format"). If the encoding of theproposedParamvalue does not match the expected encoding of that type,FAIL. - S11. In case of expected type
list, if more or less than the expected length of the list elements are proposed,FAIL.
thisGovAction:(fromBuiltinData(v3_context) -> scriptcontextScriptInfo -> (ProposingScript _ (ProposalProcedure _ _ thisGovAction)))PASS: An implementation accepts the check, and continues with the rest of the checks. If there are no checks left, the script returnsBuiltinUnit, thus deeming this proposal constitutional.FAIL: An implementation must make the overall script fail with an error (explicitly by callingTx.error ()or implicitly e.g.1/0), thus deeming the proposal un-constitutional.UNSPECIFIED: The behavior is explicitly left unspecified, meaning that implementations may decide toPASSorFAILor loop indefinitely --- note that in reality, looping indefinitely behaves the same asFAILsince plutus scripts are "guarded" by certain resource limits.
Although not part of the specification, the ledger provides us extra guarantees, which a valid implementation may optionally rely upon (i.e. take it as an assumption):
- G01. The underlying AssocMap does not contain duplicate-key entries.
- G02. The underlying AssocMap is sorted on the keys (the usual Ordering Integer).
- G03. The underlying AssocMap is not empty.
- G04. Unit_Interval's denominator is strictly positive.
- G05. Unit_Interval's numerator and denominator are co-prime.
- G06. Unit_Interval's value range is [0,1] (i.e. both sides inclusive).
- G07. Protocol parameter IDs are positive.
- G08. Redeemer is encoded as
(). FIXME: any governance proposal?
There are 2 engine implementations:
UnsortedSorted
Unsorted and Sorted must be valid implementations, so they must comply to all clauses [S01..].
Unsorted does not rely on any ledger guarantees.
Sorted as the name implies relies on sortedness to work and thus assumes the G01,G02 guarantees.
Sorted further requires that the Config is also sorted, which must be guaranteed by-construction when using the JSON Config format
(this is not currently guaranteed when manually constructing a Config ADT value).
Note that, although all implementations could theoretically work without problem with negative proposedParam ids,
the Config JSON format (not the ADT) and the Ledger are limited only to positive ids (see G07).
The Sorted implementation will most likely be the implementation to be used on the mainnet chain.
The testing infrastructure generates artificial proposals and unit/random tests them against the 2 valid implementations, unsorted and sorted.
The artificial proposals are built such as to satisfy all specification clauses [S01..] (required for validity).
Since this repository's testing infrastructure cannot be aware or test the ledger's behavior, we have to make explicit the ledger guarantees that the test code needs to rely upon. To keep things simple and uniform, we decided that the (random) testing infrastructure has to rely on the union (Sum) of the ledger guarantees required by all our current implementations, i.e. G01,G02.
src/Cardano/Constitution/Config/*: types and instances for theConfigADTsrc/Cardano/Constitution/Config.hs: "predicate meanings" and umbrella module for theConfigsrc/Cardano/Constitution/Validator/Sorted.hs: sorted enginesrc/Cardano/Constitution/Validator/Unsorted.hs: unsorted enginesrc/Cardano/Constitution/Validator/Common.hs: common code between the 2 enginesdata/*: contains the JSON configuration filestest/*: testing code