SimplicityHL updates: enum types and witness type inference

I just pushed a documentation update to

(and a couple of other related documentation pages) reflecting some significant changes in SimplicityHL 0.7.0: type inference in witness parsing and enum types.

You can see these in action in my prize claim contract where I declare

enum Action {
    Update,
    Claim(u64, u64),
}

and later on do

    match witness::ACTION {
       Action::Update => update(dest_addr_script_hash),
       Action::Claim(p: u64, q: u64) => claim(dest_addr_script_hash, p, q),
    }

… much cleaner than the historical Left() and Right() branches that we would have to use to match on an Either<> type in the witness.

The corresponding .wit files now look like

{
    "DEST_ADDR_SCRIPT_HASH": "0xbcfbe70502021903755bb406a7c4681817be317affc7d1120de2041a9e06cfc5",
    "ACTION": "Action::Claim(17041427052385644731, 18305655359241496139)"
}

and

{
    "DEST_ADDR_SCRIPT_HASH": "0xbcfbe70502021903755bb406a7c4681817be317affc7d1120de2041a9e06cfc5",
    "ACTION": "Action::Update"
}

so they’re also more concise and readable both due to type inference and the enum feature.

The last_will.simf example covenant has also been updated to use these features.

enum Action {
    Inherit(Signature),
    ColdSpend(Signature),
    HotSpend(Signature),
}

fn main() {
    match witness::ACTION {
        Action::Inherit(sig: Signature) => inherit_spend(sig),
        Action::ColdSpend(sig: Signature) => cold_spend(sig),
        Action::HotSpend(sig: Signature) => refresh_spend(sig),
    }
}

with the witness simplified to just

{
    "ACTION": "Action::Inherit(0x755201bb62b0a8b8d18fd12fc02951ea3998ba42bfc6664daaf8a0d2298cad43cdc21358c7c82f37654275dc2fea8c858adbe97bac92828b498a5a237004db6f)"
}

Anyway, hopefully it will be fairly clear from these examples how to use these new features to write much more concise and readable contracts (you currently need SimplicityHL 0.7.0 and need to compile with -Z enums). The exact details are all up on the documentation site.

Just as an example of how much more readable these features can make the main dispatch logic of a contract, I rewrote the nested match for the main() function of the options contract.

The original version:

match witness::PATH {
    Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right {
        Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => {
            let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params;
            funding_path(
                expected_asset_amount,
                input_option_abf, input_option_vbf,
                input_grantor_abf, input_grantor_vbf,
                output_option_abf, output_option_vbf,
                output_grantor_abf, output_grantor_vbf
            );
        },
        Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement {
            Left(params: (bool, u64, u64, u64)) => {
                let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params);
                exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed)
            },
            Right(params: (bool, u64, u64)) => {
                let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params);
                settlement_path(amount_to_burn, asset_amount, is_change_needed)
            },
        },
    },
    Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right {
        Left(params: (bool, u64, u64)) => {
            let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params;
            expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed)
        },
        Right(params: (bool, u64, u64)) => {
            let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params;
            cancellation_path(amount_to_burn, collateral_amount, is_change_needed)
        },
    },
}

The new version using enum:

match witness::PATH {
    Action::Funding(expected_asset_amount: u64, input_option_abf: u256, input_option_vbf: u256, input_grantor_abf: u256, input_grantor_vpf: u256, output_option_abf: u256, output_option_vbf: u256, output_grantor_abf: u256, output_grantor_vbf: u256) => funding_path(expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vpf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf),
    Action::Exercise(is_change_needed: bool, amount_to_burn: u64, collateral_amount: u64, asset_amount: u64) => exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed),
    Action::Settlement(amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) => settlement_path(amount_to_burn, asset_amount, is_change_needed),
    Action::Expiry(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) => expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed),
    Action::Cancellation(amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) => cancellation_path(amount_to_burn, collateral_amount, is_change_needed),
}

A lot tidier and more readable!

This is a huge quality-of-life improvement! Very cool to see SimplicityHL abstracting away more and more details of the underlying Simplicity code. Maybe in the future there could be language syntax for specifying the relative probabilities of different spend paths for optimized branching under the hood, kind of like miniscript.

Any idea when enums will be available in smplx? :grin:

I’ve gotten them to work fine by just manually changing the SimplicityHL dependency version in Simplex’s Cargo.toml file. No problems at all. Of course this makes your Simplex project non-portable to other people’s systems, but if you’re just running it yourself, it should be fine!

Edit: and it also means you have to run with a locally-compiled Simplex instead of just installing the crate or via simplexup, which could be a bit more of a nuisance.

1 Like

@stringhandler I forgot, to what extent does txmanifest currently support enums?

I prefer using nix for defining my dev and CI environments, so actually I sort of already use a locally-compiled version. I just need to point it to a different fork and commit hash. :grin:

I think I was actually overoptimistic about only needing to change Cargo.toml. I had forgotten that I had a couple of other small AI-suggested patches in my local branch that made some SimplicityHL 0.7.1 things work correctly with Simplex. If you try this and you get errors, please let me know and I should be able to share the patches needed to work around those errors.

I actually don’t know at what point Simplex plans to bump the SimplicityHL dependency version. @arvolear?

Txmanifest does support enums from manifest version 0.2.0, using the specified “unstable_features” field.

{
  "simplicity_hl": {
    "unstable_features": ["enums"]
...
}