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.
Being a smart contract, the constitution script is a validator function of the form:
const_script :: BuiltinData -> BuiltinUnit
The 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_config
In 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 configValue proposedValue = configValue Tx.=< proposedValue
maxValue configValue proposedValue = configValue Tx.>= proposedValue
notEqual configValue proposedValue = configValue Tx./= proposedValue
An alternative & preferred method than constructing a Config
inside Haskell, 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
.
This BuiltinData
object has the following format (in pseudocode):
ChangedParametersData = Map ChangedIdData ChangedManyValueData
ChangedIdData = I Integer
ChangedManyValueData =
ChangedSingleValueData
| List[ChangedSingleValueData...]
-- ^ an arbitrary-length, heterogeneous (integer or ratio) list of values (to support sub-parameters)
ChangedSingleValueData =
I Integer -- a proposed integer value
| List[I Integer, I Integer] -- a proposed numerator,denominator (ratio value)
-- ^ a 2-exact element list; *BE CAREFUL* because this can be alternatively (ambiguously) interpreted
-- as a many-value data (sub-parameter) of two integer single-value data.
, where Map,I,List are the constructors of PlutusCore.Data
and Integer
is the usual arbitrary-precision PlutusTx/Haskell Integer
.
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
thisGovAction
isTreasuryWithdrawals _ _
, thenPASS
and no checks left. - S02. If
thisGovAction
is(ParameterChange _ proposedParams _)
and the decodedproposedParams
is an empty list (a.k.a. Tx.AssocMap), thenUNSPECIFIED
. - S03. If
thisGovAction
is(ParameterChange _ proposedParams _)
and decodedproposedParams
is a non-empty list, start checking each proposed parameter in the list against theConfig
. - S04. The Redeemer
BuiltinData
value isUNSPECIFIED
. - S05. In all other cases of decoded
ScriptContext
returnFAIL
. - S06. Lookup in the
Config
the 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 nextproposedParam
in the list as the current one and continue to (F). - S07. If no
proposedParam
to check is left in theproposedParams
list,PASS
and 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
Config
says{type: any}
under a givenproposedParam
, then do not try to decode the value of theproposedParam
, but simplyPASS
and continue to next check. - S10. In all other cases of
{type: integer/unit_interval/list}
, decode theproposedParam
value according to the expected type (see "ChangedParameters Format"). If the encoding of theproposedParam
value 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 toPASS
orFAIL
or loop indefinitely --- note that in reality, looping indefinitely behaves the same asFAIL
since 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:
Unsorted
Sorted
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 is the selected 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 theConfig
ADTsrc/Cardano/Constitution/Config.hs
: "predicate meanings" and umbrella module for theConfig
src/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 codeapp/create-json-envelope
: an executable to construct constitution script, ready to be submitted to the chain
We have created an executable create-json-envelope
to ease the creation of a constitution script.
This executable is disabled by default; to enable it, uncomment the related lines inside this repo's cabal.project
:
-- Uncomment the following lines to make cardano-constitution:create-json-envelope buildable:True
--
-- package cardano-constitution
-- flags: +force-build
-- allow-newer: *:plutus-ledger-api
-- allow-older: *:nothunks
Then,
cabal run cardano-constitution:create-json-envelope -- out.plutus
will create out.plutus
JSON file that contains the plutus code as-configured by data/defaultConstitution.json
,
ready to submit it to the chain.
Normally any editing of data/defaultConstitution.json
should be picked up by re-running cabal run
;
if not, use also -fforce-recomp
.