Skip to content

perf(linear-vesting): optimize validator with raw BI.* data extraction#7658

Merged
Unisay merged 2 commits intomasterfrom
yura/linear-vesting-optimized
Mar 10, 2026
Merged

perf(linear-vesting): optimize validator with raw BI.* data extraction#7658
Unisay merged 2 commits intomasterfrom
yura/linear-vesting-optimized

Conversation

@Unisay
Copy link
Contributor

@Unisay Unisay commented Mar 10, 2026

Context

  • What: Optimize the LinearVesting validator by replacing high-level Haskell pattern matching with raw BI.* data extraction
  • Why: Reduce on-chain execution cost for the benchmark validator, demonstrating the performance gains achievable through manual data extraction
  • Issue: IntersectMBO/plutus-private#2113

Approach

Replaced the entire validation logic in LinearVesting.Validator with hand-written BI.* operations that bypass the high-level PlutusLedgerApi pattern matching infrastructure. Instead of deserializing ScriptContext, TxInfo, and related types into Haskell ADTs, the optimized code works directly with BuiltinData using BI.unsafeDataAsConstr, BI.head, and BI.tail to extract fields positionally.

Uses standard Haskell multi-way if guards for control flow rather than the builtinIf lambda/unit pattern, based on experiments in PRs #7583 and #7584 which confirmed guards produce slightly better results (~5% CPU / ~7% MEM improvement).

The VestingDatum and VestingRedeemer type definitions and TH splices are preserved unchanged to maintain compatibility with Test.hs, which constructs test data using the typed API.

Changes

Validator optimization

  • Replaced fromBuiltinData-based ScriptContext parsing with direct BI.unsafeDataAsConstr field extraction in untypedValidator
  • Replaced txSignedBy with txSignedBy' using BI.caseList' over raw signatory list
  • Replaced List.find with findInputByOutRef and findOutputByAddress using BI.caseList'
  • Replaced assetClassValueOf with valueOf that walks the BI.unsafeDataAsMap structure directly
  • Replaced getLowerInclusiveTimeRange pattern match with lowerInclusiveTime that reads constructor tags positionally
  • Added getScriptHashFromAddress, getPubKeyHashFromAddress, getSpendingInfo helpers for raw credential extraction (no tag checks — ledger invariants guarantee correct credential types)
  • Used BI.caseInteger for redeemer dispatch instead of fromBuiltinData-based VestingRedeemer parsing

Imports cleanup

  • Removed PlutusTx.Prelude, PlutusLedgerApi.Data.V3 (full), PlutusLedgerApi.V3.Data.Contexts, PlutusTx.Data.List
  • Added targeted imports: PlutusTx.Bool, PlutusTx.Builtins.Internal, PlutusTx.Trace
  • Added explicit module export list

Golden files

  • Updated .golden.eval, .golden.pir, .golden.uplc to reflect optimized output

Results

Metric Before After Reduction
CPU 30,405,131 11,040,647 64%
MEM 128,919 45,828 64%
AST Size 1,854 1,467 21%
Flat Size 2,490 2,326 7%

@Unisay Unisay self-assigned this Mar 10, 2026
Replace high-level pattern matching with raw BI.unsafeDataAsConstr,
BI.head/tail for data extraction, BI.caseList' for list traversal,
and BI.caseInteger for redeemer dispatch. Use standard Haskell
multi-way if guards instead of builtinIf pattern.

CPU: 30.4M → 11.4M (62% reduction)
MEM: 129K → 48K (63% reduction)
@Unisay Unisay force-pushed the yura/linear-vesting-optimized branch from b080a9f to 19eb906 Compare March 10, 2026 12:46
@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

Execution Budget Golden Diff

318c729 (master) vs fc4db8b

output

plutus-benchmark/linear-vesting/test/9.6/main.golden.eval

Metric Old New Δ%
CPU 30_405_131 11_040_647 -63.69%
Memory 128_919 45_828 -64.45%
Flat Size 2_490 2_191 -12.01%

This comment will get updated when changes are made.

@Unisay Unisay requested a review from a team March 10, 2026 12:47
@Unisay Unisay added the No Changelog Required Add this to skip the Changelog Check label Mar 10, 2026
@Unisay Unisay enabled auto-merge (squash) March 10, 2026 12:48
credFields = BI.snd credCon
in if BI.equalsInteger credTag 0
then BI.unsafeDataAsB (BI.head credFields)
else traceError "Expected PubKeyCredential"
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't need the constructor tag check, it doesn't impact the security invariants of the contract as we require the public key hash to be present in txInfoSignatories which guarantees that it is indeed a valid public key hash (the ledger enforces this).

This can be rewritten (more efficiently) as:

BI.unsafeDataAsB $ BI.head $ BI.snd $ BI.unsafeDataAsConstr $ BI.head (BI.snd (BI.unsafeDataAsConstr addr))

Same with the other similar functions (ie. getScriptHashFromAddress). Even more-so for getScriptHashFromAddress because the ledger invariants already guarantee that the argument that we call this on inputAddress must be an address where the first field has the encoding Constr 1 [scriptHash] because inputAddress is the address of the input being validated by this script execution, which guarantees that it is indeed a script input and thus that the payment credential has constructor index 1 (is a ScriptCredential).

This is a micro-optimization probably so feel free to ignore it.

This check would be necessary if this contract had a minting policy for authentication tokens for creating vesting positions (in which case we would impose this check amongst other checks to enforce that the beneficiary address is indeed a valid public key address with the correct constructor index and a single bytestring field that has the length of 28) AND the validator allowed anyone to claim the vested amount provided they send an output with that amount to the beneficiary address. This would be required because in such a scenario, if the constructor index of the beneficiary address was incorrect then the check that txOutAddressAsData satisfyingOutput == beneficiaryAddress would be unsatisfiable if beneficiaryAddress was not strictly enforced to have canonical builtin data encoding that matches the ledger (ie. if it had constructor tag 3, it would be unsatisfiable because there is no way to construct a tx such that the output in the script context has an address with constructor tag 3).

Copy link
Contributor Author

@Unisay Unisay Mar 10, 2026

Choose a reason for hiding this comment

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

Good call: applied in fc4db8b. Both getScriptHashFromAddress and getPubKeyHashFromAddress now skip the tag check entirely, since as you pointed out the ledger guarantees the correct credential type in both cases. Shaved off another ~360K CPU.

Remove constructor tag verification from getScriptHashFromAddress
and getPubKeyHashFromAddress. The ledger guarantees the correct
credential type in both cases: the validated input is always a
script address, and the beneficiary hash is checked against
txInfoSignatories.

Saves ~360K CPU / ~1.9K MEM.
Copy link
Member

@zliu41 zliu41 left a comment

Choose a reason for hiding this comment

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

Please add this to the validation benchmark. We need more better-optimized scripts in the validation benchmark.

@Unisay Unisay merged commit 4811959 into master Mar 10, 2026
9 of 10 checks passed
@Unisay Unisay deleted the yura/linear-vesting-optimized branch March 10, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

No Changelog Required Add this to skip the Changelog Check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants