@lucid-evolution/uplc
v0.5.3
Published
## 1. Enter Development Environment
Readme
Development Environment Setup
1. Enter Development Environment
To start working in the development environment, run the following command:
nix develop2. Build the Rust Project & Generate WASM Files
Before continuing, please note that this step is not part of the CI process. You'll need to run it locally to build the Rust project and generate the required WASM files:
pnpm build-localThe browser builds are made with wasm-pack. The Node.js builds need wasm-bindgen's --experimental-reset-state-function, which wasm-pack cannot pass, so scripts/build-node.sh runs cargo, the wasm-bindgen CLI and wasm-opt itself: both must be on the PATH, with the wasm-bindgen CLI at the version of wasm-bindgen in rust/Cargo.lock (cargo install wasm-bindgen-cli --version <version>).
3. Build the UPLC Package
Once you've completed the previous step, you can build the UPLC package by running:
pnpm buildSpeed and size builds
The package ships two builds of the evaluator:
- speed (
-O3, ~1.93 MB wasm): faster script evaluation. - size (
-Oz, ~1.03 MB wasm): smaller download.
@lucid-evolution/uplc resolves to the speed build on Node, Bun and Deno, and to the size build in browser bundles and edge runtimes (browser, worker, workerd and edge-light export conditions). To pick a variant explicitly, import @lucid-evolution/uplc/speed or @lucid-evolution/uplc/size.
Recovering from a wasm trap
On Node.js (the default export condition), the bindings recover from a wasm trap. A trap or a stack overflow inside wasm leaves the instance unable to run anything after it, so after any binding throws, the next call first replaces the instance with a fresh one, and the error itself is rethrown unchanged. A result returned before the replacement throws when read afterwards, and freeing it, explicitly or by garbage collection, does not touch the new instance.
The browser, worker, workerd and edge-light builds do not recover: a bundler instantiates their wasm once, and wasm-bindgen cannot replace that instance. After a trap there, load the package again (for example in a new worker).
Evaluating a program
eval_program_raw evaluates a UPLC program applied to Data arguments, outside of any transaction. The caller builds whatever arguments the program expects, including its script context.
import { eval_program_raw } from "@lucid-evolution/uplc";
const result = eval_program_raw(
programFlat, // Uint8Array: the program, flat-encoded without a CBOR wrapper
[datum, redeemer, context], // Uint8Array[]: each argument as CBOR-encoded Data
3, // Plutus version: 1, 2 or 3
11, // protocol major version: 5 to 11
costModel, // BigInt64Array: every cost model parameter for this Plutus version
10_000_000_000n, // CPU budget
14_000_000n, // memory budget
true, // charge every step, and stop as soon as the budget runs out
);
result.ok; // whether the program succeeded (for Plutus V3, returned unit)
result.error_kind; // 0 ok, 1 over budget, 2 failed, 3 Plutus V3 result not unit
result.cpu; // bigint
result.mem; // bigint
result.logs; // string[]: the program's tracesWith false as the last argument, steps are charged and the budget is checked every 200 steps as the ledger does. That only changes the cost reported for a program that fails, as the ledger does not charge the steps it took since the budget was last checked; the Haskell evaluator reports the same cost. It throws only when the inputs are malformed or unsupported.
Evaluating a builtin
eval_builtin_raw evaluates one builtin applied to constant arguments as the evaluator does for a Plutus V3 script at a protocol version (9 to 12), for callers that step through UPLC themselves.
import {
BuiltinErrorKind,
BuiltinValueKind,
eval_builtin_raw,
} from "@lucid-evolution/uplc";
const result = eval_builtin_raw(
0, // the builtin's flat tag: 0 is addInteger
[one, two], // Uint8Array[]: each argument, encoded as its kind says
Uint8Array.of(BuiltinValueKind.Flat, BuiltinValueKind.Flat),
11, // protocol major version: 9 to 12
);
result.ok; // whether the builtin returned a value
result.error_kind; // a BuiltinErrorKind: Ok, NotAvailable, RejectedArguments or Failed
result.result_kind; // a BuiltinValueKind
result.result; // Uint8Array: the value, encoded as result_kind says
result.cpu; // bigint
result.mem; // bigint
result.error; // the evaluator's message when the builtin did not return a valueA Flat value is a flat-encoded (program 1.1.0 (con …)). Flat cannot encode BLS12-381 points, so a point crosses as its compressed bytes (G1, G2), and a list or array of points as the compressed points one after the other (G1List, G2List, G1Array, G2Array). The Miller-loop builtins (68 to 70) are not supported. cpu and mem are the builtin's charge under Plutus' default Plutus V3 cost model for the protocol version, excluding the machine's per-step costs; they are 0 when the builtin is not available or rejects its arguments, as the machine fails before costing it. It throws only when the inputs are malformed or unsupported.
uplc_build_info() returns JSON that describes the build: the uplc crate it was built from, the latest protocol version it supports, each Plutus version's cost model length, a digest of what a fixed set of programs evaluate to, and whether those programs give the expected results ("selfTest": "pass").
Tests
pnpm testtest/fixtures/builtins.json holds builtin applications with the outcome and cost the evaluator gives them. It is generated by the crate's tests (packages/uplc/rust/src/builtin/tests.rs), which check each one against the machine; rerun cargo test with UPDATE_BUILTIN_FIXTURE=1 after changing the cases.
test/fixtures/programs.json holds programs with their outcome and cost under Plutus V3 and protocol version 11, as given by the Haskell evaluator (plutus-core 1.71).
