Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ unavailable to the typed DSL until a later reviewed adapter milestone.
omitted. The DSL must name it accordingly and must not imply that it models a
complete on-ledger constitution.

The pinned model also omits the wrapper plutus-ledger-api puts around it:
Haskell encodes `Constitution` as `Constr 0 [Maybe ScriptHash]`, while the model
places the bare `Option ScriptHash` in `NewConstitution`'s second field. JuLC
encodes the wrapper since julc #223, so a verified contract that reads
`constitution().script()` is evaluated against the wrong data shape until the pin
moves to a model revision with the wrapper (to be reported upstream).

The pinned `isKnownProposal` is exact indexed equality over the full proposal,
including raw governance-action `Data`. Negative indexes and out-of-range
indexes return false. This dedicated helper may be exposed without admitting
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/internals/compiler-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ Tier 3: Composite records → Address, TxOut, TxInInfo, Interval
Tier 4: Top-level → TxInfo (16 fields), ScriptContext
```

Governance types (Conway era): `Vote`, `DRep`, `Voter`, `StakingCredential`, `Delegatee`, `TxCert`, `GovernanceAction`, `ProposalProcedure`, `Committee`, `ScriptPurpose`.
Governance types (Conway era): `Vote`, `DRep`, `Voter`, `StakingCredential`, `Delegatee`, `TxCert`, `GovernanceAction`, `ProposalProcedure`, `Committee`, `Constitution`, `ScriptPurpose`.

**Stage 2: User Types** (`TypeRegistrar`)

Expand Down
31 changes: 31 additions & 0 deletions docs/src/content/docs/reference/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@ title: "Release Notes"
description: "JuLC release notes and migration guidance"
---

## Upcoming preview: `NewConstitution` carries the `Constitution` wrapper (#223)

`GovernanceAction.NewConstitution` now has the Plutus Data layout of plutus-ledger-api V3,
`Constr 5 [Maybe GovernanceActionId, Constitution]` with
`Constitution = Constr 0 [Maybe ScriptHash]`. JuLC encoded and read the second field as a
bare `Maybe ScriptHash`, without the `Constr 0` that the ledger builds (`transConstitution`
in cardano-ledger `Conway/TxInfo.hs`). A compiled V3 script that read a `NewConstitution`
proposal from a real script context therefore read the wrong field: `constitution()` gave
the `Constitution` value where the script expected the optional hash.

The ledger API has a new record `Constitution(Optional<ScriptHash> script)`, and the
second component of `NewConstitution` changes from `Optional<ScriptHash> constitution` to
`Constitution constitution`. Read the guardrails script hash with
`nc.constitution().script()`.

**Contracts that read `constitution()` must be updated and recompiled.** Their script
bytes and hashes change. Other contracts, including those that only match the
`NewConstitution` case, compile to the same bytes as before. Off-chain code that
constructs `NewConstitution` must pass a `Constitution`, and the script contexts the
cardano-client-lib evaluator builds now contain the wrapper. The other six governance
actions are unchanged; tests now compare each of them with hex written by hand from
Haskell's `ToData`.

Formal verification of this field is not yet sound: the pinned Lean model
(CardanoLedgerApiBlaster `5dab3c43`) still omits the `Constitution` wrapper, so a verified
contract that reads `constitution().script()` is checked against the old data shape.

As with other ledger type names such as `Committee`, a type named `Constitution` declared
inside a validator class now resolves to the ledger type. Rename such a type, or declare
it at top level in the contract's package.

## Upcoming preview: instanceof pattern variables may not reuse a field name (JULC0059, #207)

JuLC binds an `instanceof` pattern variable only when the pattern is the whole
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ private static GovernanceAction govAction(List<DataItem> a) {
new Rational(quorum[0], quorum[1]));
}
case 5 -> new GovernanceAction.NewConstitution(prevActionId(a.get(1)),
scriptHash(items(a.get(2)).get(1)));
new Constitution(scriptHash(items(a.get(2)).get(1))));
case 6 -> new GovernanceAction.InfoAction();
default -> throw new IllegalArgumentException("Unknown governance action tag: " + tag);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.julclang.clientlib.JulcScriptAdapter;
import org.julclang.core.Constant;
import org.julclang.core.PlutusData;
import org.julclang.core.cbor.PlutusDataCborEncoder;
import org.julclang.core.Program;
import org.julclang.core.Term;
import org.julclang.ledger.*;
Expand Down Expand Up @@ -264,29 +265,34 @@ void governanceActionsAreTranslated() throws Exception {
proposals.append("841a000f4240").append(returnAccount).append(action).append(ANCHOR);
}
TxInfo info = converter(tx(4, INPUTS + NO_OUTPUTS + FEE + proposals, NO_WITNESSES), 10).buildTxInfo();
var got = toList(info.proposalProcedures()).stream().map(ProposalProcedure::governanceAction).toList();

var params = new PlutusData.MapData(List.of(
new PlutusData.Pair(new PlutusData.IntData(0), new PlutusData.IntData(44)),
new PlutusData.Pair(new PlutusData.IntData(9), new PlutusData.ListData(List.of(
new PlutusData.IntData(3), new PlutusData.IntData(10))))));
var prev = Optional.of(new GovernanceActionId(TxId.of(b("cc".repeat(32))), BigInteger.ONE));
var guardrails = Optional.of(ScriptHash.of(b(policy)));
assertEquals(new GovernanceAction.ParameterChange(Optional.empty(), params, guardrails).toPlutusData(),
got.get(0).toPlutusData());
assertEquals(new GovernanceAction.HardForkInitiation(prev,
new ProtocolVersion(BigInteger.TEN, BigInteger.ZERO)).toPlutusData(), got.get(1).toPlutusData());
var treasury = (GovernanceAction.TreasuryWithdrawals) got.get(2);
assertEquals(List.of(new Credential.ScriptCredential(ScriptHash.of(b("11".repeat(28)))),
new Credential.PubKeyCredential(PubKeyHash.of(b("40".repeat(28))))),
toList(treasury.withdrawals().keys()), "ledger AccountAddress order");
assertInstanceOf(GovernanceAction.NoConfidence.class, got.get(3));
var committee = (GovernanceAction.UpdateCommittee) got.get(4);
assertEquals(new Rational(BigInteger.ONE, BigInteger.TWO), committee.newQuorum(), "in lowest terms");
assertEquals(BigInteger.TEN, committee.addedMembers().get(
new Credential.ScriptCredential(ScriptHash.of(b("22".repeat(28))))));
assertEquals(new GovernanceAction.NewConstitution(prev, guardrails), got.get(5));
assertInstanceOf(GovernanceAction.InfoAction.class, got.get(6));
var got = toList(info.proposalProcedures()).stream()
.map(p -> HexFormat.of().formatHex(PlutusDataCborEncoder.encode(p.governanceAction().toPlutusData())))
.toList();

// Written by hand from Haskell's ToData (plutus-ledger-api 1.65 V3 Contexts.hs makeIsDataSchemaIndexed;
// Maybe: Just = Constr 0, Nothing = Constr 1; Rational = Constr 0 [n, d]) of transGovAction's output.
String nothing = "d87a80";
String justPrev = "d8799f" + "d8799f5820" + "cc".repeat(32) + "01ff" + "ff";
String justPolicy = "d8799f581c" + policy + "ff";
String[] expected = {
// Constr 0 [Nothing, Map [(0, 44), (9, List [3, 10])], Just policy]: keys ascending, ratio reduced
"d8799f" + nothing + "a200182c099f030aff" + justPolicy + "ff",
// Constr 1 [Just prev, ProtocolVersion 10 0]
"d87a9f" + justPrev + "d8799f0a00ff" + "ff",
// Constr 2 [Map in AccountAddress order (script credential first), Nothing]
"d87b9f" + "a2" + "d87a9f581c" + "11".repeat(28) + "ff06" + "d8799f581c" + "40".repeat(28) + "ff05"
+ nothing + "ff",
// Constr 3 [Nothing]
"d87c9f" + nothing + "ff",
// Constr 4 [Nothing, [key 00], Map [(script 22, 10)], Rational 1 2]
"d87d9f" + nothing + "9fd8799f581c" + "00".repeat(28) + "ffff" + "a1d87a9f581c" + "22".repeat(28)
+ "ff0a" + "d8799f0102ff" + "ff",
// Constr 5 [Just prev, Constitution (Just policy)]: the anchor is dropped, the Constr 0 kept
"d87e9f" + justPrev + "d8799f" + justPolicy + "ff" + "ff",
// Constr 6 []
"d87f80"
};
assertEquals(List.of(expected), got);
assertEquals(new Credential.PubKeyCredential(PubKeyHash.of(b("00".repeat(28)))),
toList(info.proposalProcedures()).get(0).returnAddress());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public final class LedgerCatalog {
* also adding it here is a deliberate choice the maintainer makes.
*/
private static final List<String> TYPES = List.of(
"Address", "Committee", "Credential", "DRep", "DatumHash",
"Address", "Committee", "Constitution", "Credential", "DRep", "DatumHash",
"Delegatee", "GovernanceAction", "GovernanceActionId", "Interval",
"IntervalBound", "IntervalBoundType", "OutputDatum", "PolicyId",
"ProposalProcedure", "ProtocolVersion", "PubKeyHash", "Rational",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
{"type": "org.julclang.stdlib.lib.ValuesLib", "allDeclaredMethods": true},
{"type": "org.julclang.ledger.Address", "allDeclaredMethods": true, "allDeclaredFields": true},
{"type": "org.julclang.ledger.Committee", "allDeclaredMethods": true, "allDeclaredFields": true},
{"type": "org.julclang.ledger.Constitution", "allDeclaredMethods": true, "allDeclaredFields": true},
{"type": "org.julclang.ledger.Credential", "allDeclaredMethods": true},
{"type": "org.julclang.ledger.Credential$PubKeyCredential", "allDeclaredMethods": true, "allDeclaredFields": true},
{"type": "org.julclang.ledger.Credential$ScriptCredential", "allDeclaredMethods": true, "allDeclaredFields": true},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.julclang.compiler;

import org.julclang.core.PlutusData;
import org.julclang.core.Program;
import org.julclang.stdlib.StdlibRegistry;
import org.julclang.vm.JulcVm;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -574,6 +576,51 @@ static boolean validate(GovernanceAction ga, ScriptContext ctx) {
var result = vm.evaluateWithArgs(program, List.of(ctx));
assertTrue(result.isSuccess(), "HardForkInitiation major=10 should match. Got: " + result);
}

@Test
void newConstitutionGuardrailsScriptFieldAccess() {
// plutus-ledger-api V3: NewConstitution = Constr(5, [Maybe GovernanceActionId, Constitution]),
// Constitution = Constr(0, [Maybe ScriptHash])
var withScript = PlutusData.constr(5, optionalNone(),
PlutusData.constr(0, optionalSome(PlutusData.bytes(new byte[28]))));
var withoutScript = PlutusData.constr(5, optionalNone(), PlutusData.constr(0, optionalNone()));

var present = compileNewConstitutionCheck(
"c.script().isPresent() && Builtins.lengthOfByteString(c.script().get()) == 28");
var result = vm.evaluateWithArgs(present, List.of(buildCtx(withScript)));
assertTrue(result.isSuccess(), "Guardrails script should be present. Got: " + result);

// Succeeds, so the absent case is read as Nothing rather than failing on the layout
var absent = compileNewConstitutionCheck("!c.script().isPresent()");
result = vm.evaluateWithArgs(absent, List.of(buildCtx(withoutScript)));
assertTrue(result.isSuccess(), "Guardrails script should be absent. Got: " + result);
}

private Program compileNewConstitutionCheck(String check) {
var source = """
import org.julclang.stdlib.Builtins;

@MintingValidator
class TestValidator {
@Entrypoint
static boolean validate(GovernanceAction ga, ScriptContext ctx) {
return switch (ga) {
case ParameterChange pc -> false;
case HardForkInitiation hf -> false;
case TreasuryWithdrawals tw -> false;
case NoConfidence nc -> false;
case UpdateCommittee uc -> false;
case NewConstitution nc2 -> {
Constitution c = nc2.constitution();
yield %s;
}
case InfoAction ia -> false;
};
}
}
""".formatted(check);
return new JulcCompiler(StdlibRegistry.defaultRegistry()).compile(source).program();
}
}

// =======================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.julclang.ledger;

import org.julclang.core.PlutusData;

import java.util.List;
import java.util.Optional;

/**
* A constitution: the optional guardrails script hash. The anchor is omitted, as in plutus-ledger-api V3.
* Encoded as {@code Constr 0 [Maybe ScriptHash]}.
*/
public record Constitution(Optional<ScriptHash> script) implements PlutusDataConvertible {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve source-declared Constitution binding before bundling this type

The newly registered ledger type captures a previously valid nested source record. Reproduced with @MintingValidator class ReviewBinding { record Constitution(BigInteger value) {} @Entrypoint static boolean validate(Constitution c, ScriptContext ctx) { return true; } } (ordinary imports for BigInteger, annotations and ScriptContext). Both versions compile with no diagnostics. Evaluating the generated programs with the Java VM configured for PV11 and the same V3 context gives:

Redeemer Before this PR This PR
Constr 0 [I 42] success UnConstrData: not ConstrData
Constr 0 [Constr 1 []] UnIData: not IntData success

The compiler/ledger source used for the baseline is identical to this PR's parent. This is not merely a source-compatibility warning: an unused typed parameter silently validates against the ledger record instead of the declaration Java binds to, changing which datum/redeemer representations are accepted. Referencing c.value() also newly fails member resolution. The release-note workaround to rename the user's record does not prevent the silent case. Please fix source-type resolution/registration precedence as part of adding this bundled name, and add both compile/evaluate regressions so local declarations retain their own schema. This violates the repository's source-name binding and typed-boundary invariants.

@Override
public PlutusData.ConstrData toPlutusData() {
return new PlutusData.ConstrData(0, List.of(
PlutusDataHelper.encodeOptional(script, ScriptHash::toPlutusData)));
}

public static Constitution fromPlutusData(PlutusData data) {
var fields = PlutusDataHelper.expectConstr(data, 0);
return new Constitution(PlutusDataHelper.decodeOptional(fields.get(0), ScriptHash::fromPlutusData));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ public PlutusData.ConstrData toPlutusData() {
}

record NewConstitution(Optional<GovernanceActionId> id,
Optional<ScriptHash> constitution) implements GovernanceAction {
Constitution constitution) implements GovernanceAction {
@Override
public PlutusData.ConstrData toPlutusData() {
return new PlutusData.ConstrData(5, List.of(
PlutusDataHelper.encodeOptional(id, GovernanceActionId::toPlutusData),
PlutusDataHelper.encodeOptional(constitution, ScriptHash::toPlutusData)));
constitution.toPlutusData()));
}
}

Expand Down Expand Up @@ -106,7 +106,7 @@ static GovernanceAction fromPlutusData(PlutusData data) {
Rational.fromPlutusData(f.get(3)));
case 5 -> new NewConstitution(
PlutusDataHelper.decodeOptional(f.get(0), GovernanceActionId::fromPlutusData),
PlutusDataHelper.decodeOptional(f.get(1), ScriptHash::fromPlutusData));
Constitution.fromPlutusData(f.get(1)));
case 6 -> new InfoAction();
default -> throw new IllegalArgumentException("Invalid GovernanceAction tag: " + c.tag());
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ void updateCommitteeRoundTrip() {

@Test
void newConstitutionRoundTrip() {
var ga = new GovernanceAction.NewConstitution(Optional.empty(), Optional.of(sh()));
var ga = new GovernanceAction.NewConstitution(Optional.empty(), new Constitution(Optional.of(sh())));
assertEquals(ga, GovernanceAction.fromPlutusData(ga.toPlutusData()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,81 @@ class GovernanceTypes {
}
}

/**
* Every {@link GovernanceAction} constructor against hex written by hand from Haskell's {@code ToData}:
* {@code makeIsDataSchemaIndexed} in plutus-ledger-api 1.65 {@code PlutusLedgerApi/V3/Contexts.hs}
* ({@code GovernanceAction} tags 0-6, {@code GovernanceActionId}, {@code ProtocolVersion} and
* {@code Constitution} are {@code Constr 0}), {@code Maybe} ({@code Just} = {@code Constr 0},
* {@code Nothing} = {@code Constr 1}), V3 {@code TxId} a bare {@code B}, {@code Rational} the pair
* {@code Constr 0 [n, d]} (PlutusTx.Ratio), {@code Map} a CBOR map and a list an indefinite array.
* cardano-ledger's {@code transGovAction} (Conway/TxInfo.hs) fills these fields.
*/
@Nested
class GovernanceActionHaskellGoldens {
static final String TX_ID = "02".repeat(32);
static final String HASH = "01".repeat(28);
static final String NOTHING = "d87a80";
static final String JUST_ACTION_ID = "d8799f" + "d8799f5820" + TX_ID + "01ff" + "ff";
static final String JUST_HASH = "d8799f581c" + HASH + "ff";
static final String KEY = "d8799f581c" + HASH + "ff";
static final String SCRIPT = "d87a9f581c" + HASH + "ff";

static final Optional<GovernanceActionId> ACTION_ID =
Optional.of(new GovernanceActionId(new TxId(bytes32()), BigInteger.ONE));
static final Optional<ScriptHash> GUARDRAILS = Optional.of(new ScriptHash(bytes28()));

private void assertGolden(String hex, GovernanceAction action) {
assertEquals(hex, HexFormat.of().formatHex(PlutusDataCborEncoder.encode(action.toPlutusData())));
assertEquals(action, GovernanceAction.fromPlutusData(
PlutusDataCborDecoder.decode(HexFormat.of().parseHex(hex))));
}

@Test void parameterChange() {
var params = new PlutusData.MapData(List.of(
new PlutusData.Pair(new PlutusData.IntData(0), new PlutusData.IntData(44))));
assertGolden("d8799f" + JUST_ACTION_ID + "a100182c" + NOTHING + "ff",
new GovernanceAction.ParameterChange(ACTION_ID, params, Optional.empty()));
}

@Test void hardForkInitiation() {
assertGolden("d87a9f" + NOTHING + "d8799f0a00ff" + "ff",
new GovernanceAction.HardForkInitiation(Optional.empty(),
new ProtocolVersion(BigInteger.TEN, BigInteger.ZERO)));
}

@Test void treasuryWithdrawals() {
assertGolden("d87b9f" + "a1" + SCRIPT + "1a000f4240" + JUST_HASH + "ff",
new GovernanceAction.TreasuryWithdrawals(JulcMap.of(
new Credential.ScriptCredential(new ScriptHash(bytes28())),
BigInteger.valueOf(1_000_000)), GUARDRAILS));
}

@Test void noConfidence() {
assertGolden("d87c9f" + JUST_ACTION_ID + "ff", new GovernanceAction.NoConfidence(ACTION_ID));
}

@Test void updateCommittee() {
assertGolden("d87d9f" + NOTHING + "9f" + KEY + "ff" + "a1" + SCRIPT + "1901f4" + "d8799f0203ff" + "ff",
new GovernanceAction.UpdateCommittee(Optional.empty(),
JulcList.of(new Credential.PubKeyCredential(new PubKeyHash(bytes28()))),
JulcMap.of(new Credential.ScriptCredential(new ScriptHash(bytes28())),
BigInteger.valueOf(500)),
new Rational(BigInteger.TWO, BigInteger.valueOf(3))));
}

@Test void newConstitution() {
// Constr 5 [Maybe GovernanceActionId, Constitution], Constitution = Constr 0 [Maybe ScriptHash]
assertGolden("d87e9f" + JUST_ACTION_ID + "d8799f" + JUST_HASH + "ff" + "ff",
new GovernanceAction.NewConstitution(ACTION_ID, new Constitution(GUARDRAILS)));
assertGolden("d87e9f" + NOTHING + "d8799f" + NOTHING + "ff" + "ff",
new GovernanceAction.NewConstitution(Optional.empty(), new Constitution(Optional.empty())));
}

@Test void infoAction() {
assertGolden("d87f80", new GovernanceAction.InfoAction());
}
}

@Nested
class ScriptContextCbor {
@Test
Expand Down
Loading
Loading