The Midnight public testnet was recently released. Along with the testnet, version 0.10.1 of the Compact programming language and version 0.18.2 of the compactc compiler have been released.
This testnet release has several changes from language version 0.5.0 on the Midnight devnet. Some of these are breaking changes that will require developers to update their code.
The rest of this document describes the changes to the language and compiler.
There is a new public ledger declaration syntax
We have changed the way that the public ledger is declared. Compact 0.5.0 had at most one ledger block in a contract. The ledger block included declarations of all the ledger fields. It had at most one constructor to initialize the contract’s public state. For example, from the Bulletin Board tutorial:
ledger {
state: Cell[STATE];
message: Cell[Maybe[Opaque["string"]]];
instance: Counter;
poster: Cell[Bytes[32]];
constructor() {
ledger.state = STATE.vacant;
ledger.message = none[Opaque["string"]]();
ledger.instance.increment(1);
}
}
Note that this is the old ledger syntax.
In Compact 0.10.1 the ledger block has been removed and there are separate declarations for each ledger field. These declarations can occur anywhere at the top level of a contract or anywhere in a module body. There is still at most one constructor and, if present, it must occur at the top level of a contract. The Bulletin Board public state declaration now looks like:
export ledger state: Cell[STATE];
export ledger message: Cell[Maybe[Opaque["string"]]];
export ledger instance: Counter;
export ledger poster: Cell[Bytes[32]];
constructor() {
state = STATE.vacant;
message = none[Opaque["string"]]();
instance.increment(1);
}
Ledger fields are declared individually with the keyword ledger. They still have a name and a type as before.
An important change is that ledger fields are now in the same namespace as other declarations, either at the top level of a contract or in a module. This means that a ledger field name can now clash with a name used in some other declaration (module, circuit, struct, etc.). This will be a compile-time error.
Ledger fields can be optionally exported by prefixing the declaration with the keyword export. All of the ledger fields in the example above are exported.
Exporting a ledger field from a module makes the field available to code that imports the module. Non-exported ledger fields from a module are still part of the contract’s public ledger state (if the module is imported), but they cannot be directly accessed by code that imports the module. This is a way for a module writer to control the visibility of the ledger state used by the module. The only way to access ledger fields that are not exported from a module is through the module’s own circuits.
Exporting a ledger field declared at the top level makes that field visible to a DApp’s TypeScript code. An exported ledger field will appear in the Ledger object returned by the JavaScript function ledger in the compiler-generated code. This gives read-only access to the ledger field from the DApp. Note that in Midnight, writing to the public ledger always requires submitting a transaction to the Midnight chain.
Ledger fields can be optionally marked “sealed” by prefixing the declaration with the keyword sealed. A sealed field cannot be set except during contract initialization. That is, its value can be modified only by the contract constructor (if any), either directly within the body of the constructor or via helper circuits called by the constructor. None of the ledger fields in the example above are sealed. The sealed keyword must come after the export keyword (if present) and before the ledger keyword, as in the following example:
sealed ledger field1: Cell[Unsigned Integer[32]];
export sealed ledger field2: Cell[Unsigned Integer[32]];
circuit init(x: Unsigned Integer[32]): Void {
field2 = x;
}
constructor(x: Unsigned Integer[16]) {
field1 = 2 * x;
init(x);
}
To prevent the setting of a sealed ledger field other than by the constructor, the compiler will give you an error if any exported circuit, or any circuit reachable from an exported circuit, attempts to modify the value of a sealed ledger field.
There is at most one constructor for the contract’s public state, and this must appear at the contract’s top level (not in a module). To initialize ledger fields declared in a module, the module can export a circuit that performs the initialization:
module Keys {
sealed ledger publicKey: Cell[Bytes[32]];
witness publicKeyFor(sk: Bytes[32]): Bytes[32];
export circuit initKeys(sk: Bytes[32]): Void {
publicKey = publicKeyFor(sk);
}
}
constructor(sk: Bytes[32]) {
import Keys;
initKeys(sk);
}
Why we made these changes
We wanted to give developers the flexibility to declare ledger fields wherever they felt that it made the most sense in their code. A ledger field can be declared together with the circuits that access it. This in turn gave us a nice way to have ledger fields that were local (or “private”) to a module.
How to fix your code
This is a breaking change. The old ledger declaration syntax will not work. Your code can be changed by removing the ledger keyword from the declaration and removing the curly braces around the declaration’s body. Then, the ledger fields (but not the constructor) should be prefixed with ledger or export ledger. If you’re not sure, the safe default for your existing code is to export all the ledger fields.
When making this change, you have to be careful that ledger field names are distinct from other names declared in the same namespace (i.e., at the top level of the contract or in the same module). The compiler will give you an error if this is not the case. You will have to decide whether to rename the ledger field or the conflicting declaration. Note that both choices will require renaming in your contract’s code and potentially also the TypeScript implementation of your DApp.
The ledger keyword is no longer used for ledger field access
Along with the change to ledger declarations, there is a change to ledger field access. Compact 0.5.0 used the keyword ledger to refer to ledger fields, so a developer would write ledger.fieldName.operation(...). Here, ledger is a keyword. Note that this is the old syntax.
Since ledger fields are now declared at the top-level or in a module, we have made the field names visible as unqualified identifiers in the same namespace as the field declaration. The keyword ledger is no longer used for field access (it’s only used for field declarations). The same code as above would now be written as fieldName.operation(...).
Note that ledger fields can now be shadowed by other declarations, such as a circuit parameter, a const binding statement, a declaration in a module, etc.
Why we made this change
Because ledger fields are now top-level or module-level declarations, there is no strong reason to require their names to be prefixed with a keyword.
How to fix your code
This is a breaking change. The ledger keyword will no longer work in this context. Your code can be changed by removing the keyword and the dot (.) from ledger field accesses.
The ledger field name might be shadowed by some other declaration. This is not a name clash that the compiler will directly report (it’s perfectly valid Compact 0.10.1 code!). You will normally get a type error where you intend to use the ledger field, because whatever is shadowing the ledger field will almost never have a ledger field type. So if this change introduces type errors for a ledger field access, inspect the context where the ledger field access occurs to see if there is a declaration shadowing the ledger field. If so, either the ledger field or the shadowing declaration needs to be renamed. It’s usually simpler to rename the shadowing declaration.
Ledger kernel operations are available on kernel
The midnight ledger has some kernel operations. These are conceptually “top-level” operations on the ledger, without respect to a particular field. In Compact 0.5.0 these use the ledger keyword without a field name, like ledger.kernelOperation(...).
In Compact 0.10.1 these kernel operations are accessed without the ledger keyword. Instead, there is a “kernel” ledger field named kernel (with type Kernel) in the standard library. Note that kernel is a regular Compact identifier and not a keyword. So, provided that the standard library is included in your contract and if the name kernel is not shadowed by some other declaration, you can write kernel.kernelOperation(...).
Why we made this change
This is part of removing the ledger syntax from ledger accesses, in order to streamline the language’s syntax.
How to fix your code
This is a breaking change. The ledger keyword will no longer work in this context. Your code can be fixed by including the standard library if it wasn’t already and replacing the ledger keyword with the identifier kernel. You will have to ensure that it is not shadowed in this context by some other declaration of the name kernel.
Ledger-field update shortcuts are now statements
Both Compact 0.5.0 and Compact 0.10.1 provide shortcuts for ledger write, increment, and decrement operations:
ledgerFieldAccess = expr // equivalent to ledgerFieldAccess.write(expr)
ledgerFieldAccess += expr // equivalent to ledgerFieldAccess.increment(expr)
ledgerFieldAccess -= expr // equivalent to ledgerFieldAccess.decrement(expr)
(Where ledgerFieldAccess differs between the two versions only in that the ledger. prefix used to identify a ledger access in Compact 0.5.0 is not used in Compact 0.10.1, as described earlier.)
In Compact 0.5.0 these shortcuts were expressions.
In Compact 0.10.1 they are statements.
Why we made this change
Because these shortcuts have type Void, they have no useful value and are used solely for effect, like, for example, the statements assert and for, we have made them statements as well.
How to fix your code
This is a breaking change. Code that uses one or more of these shortcuts outside of statement contexts must be rewritten to use them only in statement contexts.
Unsigned Integer sizes can now be generic
In Compact 0.5.0, the types Unsigned Integer[<= k] and Unsigned Integer[k] were valid only if k was a numeric literal.
In Compact 0.10.1, k can either be a numeric literal or a generic parameter name, as illustrated by the following example:
circuit f[n](x: Unsigned Integer[8], y: Unsigned Integer[n]): Unsigned Integer[n] {
return x < 16 ? y : y - x;
}
Why we made this change
The Compact 0.5.0 limitation served no purpose, and the generalization is consistent with the ability to use generics for Bytes and Vector lengths.
How to fix your code
This is strictly a generalization of the old syntax, so no changes should be necessary. Code that was previously replicated merely to handle multiple Unsigned Integer sizes or maximum values might now be generalizable by taking advantage of this change, but doing so is not necessary.
There is better static typing of subtraction
In Compact 0.5.0, if there was subtraction expression of the form a - b where a had type Unsigned Integer[<= m] and b had type Unsigned Integer[<= n], then the result would have type Unsigned Integer[<= k] where k was the maximum of m and n.
In Compact 0.10.1, the same operation will have type Unsigned Integer[<= m]. Clearly, if the subtraction does not underflow (that is, give a negative result, which is a runtime error), then the result will be bounded by m.
Why we made this change
The previous typing was due to the way that we implemented subtraction in the ZK proof. We changed this to provide a more precise bound on the result type.
How to fix your code
It should not be necessary to change anything. Unsigned Integer[<= m] is always a subtype of Unsigned Integer[<= k] where k is the maximum of m and some other bound n. So if you did have variables declared as Unsigned Integer[<= k] the compiler will now insert an upcast to that type (which always succeeds and has no runtime effect). It might be possible to improve the typing of your code to use smaller bounds on some types.
(Our expectation is that programmers will mostly work with “sized” integer types like Unsigned Integer[32] for ones that fit in 32 bits, rather than “bounded” integer types anyway.)
There is new precedence for the relational operators
In Compact 0.5.0, all the relational operators (==, !=, <, <=, >, and >=) had the same precedence and were left associative. An expression like a < b == c > d would parse as ((a < b) == c) > d. This is a type error because the greater than (>) comparison requires a numeric type and the result of the equality comparison (==) is a non-numeric Boolean type.
In Compact 0.10.1, we have made the equals and not-equals comparisons (== and !=) have lower precedence than the other relational operators. All of the relational operators remain left associative. The expression above will now parse as (a < b) == (c > d).
Why we made this change
We now match the precedence of these operators in TypeScript and JavaScript. This allows programmers to omit some parentheses and the behavior will still match their expectations.
How to fix your code
It should not be necessary to change anything. The default unparenthesized parsing of chains of comparisons would be a type error in Compact 0.5.0. It might now be possible to remove some redundant parentheses, but this is not necessary.
Generics can no longer be parameterized over generics
In Compact 0.5.0, you could declare a generic like a struct, circuit, witness, or module that was parameterized over another generic. For instance, you could write:
struct Foo[F] {
x: F[Field];
}
Notice that the generic parameter F is expected to itself be a generic taking a single parameter. So, Foo could be specialized to any generic type taking one parameter.
In Compact 0.10.1, we have removed this feature. Generics can only be parameterized over types and natural number literals.
Why we made this change
This feature has no direct analog in TypeScript and so was not working for exported structs. Instead of limiting its use to unexported structs, we’ve removed it for now.
How to fix your code
This was an undocumented feature, so we don’t expect anyone to have been using it. If you were using it, you can probably already figure out how to work around its removal. You will have to collect the different generic types you were specializing a generic with and create non-generic versions for each of those types, effectively “splitting” the generic into non-generic versions for each specialization. This may in turn cause cascading changes to the uses of those generics.
The identifier contract is now a reserved word
In Compact 0.5.0, you could use the name contract
as a normal identifier (e.g., module name, type name, circuit name, parameter name, etc.).
In Compact 0.10.1, we have made contract
a reserved word.
Why we made this change
We are in the process of adding support for contracts to call other contracts, and the newly reserved word contract
is now used in the (as yet undocumented and not fully functional) syntax for contract-type declarations.
How to fix your code
This is a breaking change. Any other uses of the newly reserved word contract
must be replaced with some other identifier.
The run-compactc.sh shell script is renamed to compactc
On the Midnight devnet there was a shell script to invoke the compiler. We have changed the name of the script for the testnet. It was previously named run-compactc.sh
and it has been renamed to compactc
.
The compiler binary itself was previously named compactc
and it has been renamed as well. The new name is not hard to discover, but we don’t intend for users to invoke the compiler binary directly.
Why we made this change
The shell script wraps the invocation of the compiler in order to correctly set the environment variables the compiler needs. We found that users were directly invoking the compiler binary without setting these environment variables. This leads to puzzling errors for users, such as an inability to find the standard library or an inability to generate the ZK proof output from the compiler.
We’ve renamed things to hopefully make it clear that the shell script is the command-line interface to the compiler, and that the compiler should not be invoked directly.
How to fix your code
If you were invoking run-compactc.sh
, either directly or in a script or shell alias, you will need to invoke compactc
instead.