From 27f4a425b5fcd7a9d9aefe34d17ffa6164f5275b Mon Sep 17 00:00:00 2001 From: fallen-icarus Date: Wed, 1 Oct 2025 09:47:53 -0400 Subject: [PATCH 1/5] docs: More `BuiltinValue` Functions CIP --- CIP-????/README.md | 149 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 CIP-????/README.md diff --git a/CIP-????/README.md b/CIP-????/README.md new file mode 100644 index 0000000000..c8f751bfc9 --- /dev/null +++ b/CIP-????/README.md @@ -0,0 +1,149 @@ +--- +CIP: ?? +Title: More `BuiltinValue` Functions +Category: Plutus +Status: Proposed +Authors: + - fallen-icarus +Implementors: [] +Discussions: [] +Created: 2025-10-01 +License: CC-BY-4.0 +--- + +## Abstract + +[CIP-0153][1] added a new `BuiltinValue` type to make working with on-chain values more efficient. +However, it added only a few builtin functions for working with this new `BuiltinValue` type which +limits its real world usability. Adding and maintaining builtin functions is costly, but the +importance of validating values in the eUTxO model justifies a wider range of builtin functions for +this purpose. With this in mind, this CIP proposes a few extra builtin functions to improve the +usability of this new `BuiltinValue` type while still trying to minimize the overall maintenance +burden. + +## Motivation: why is this CIP necessary? + +> UPLC is not a general-purpose programming language. It is a language specifically designed to +> write validation logic to set constraints for the creation and transfer of Value and Data across +> the Cardano blockchain. **Given that half of the entire purpose of this language is to express +> constraints over the creation and transfer of** `Value`, why is Value treated as a standard +> library concept rather than a first-class language primitive? + +This was the motivation for [CIP-0153][1]. Given the overwhelming importance for validating `Value`, +it makes sense for `BuiltinValue` to have a reasonably disproportionate number of builtin operations +as compared to `BuiltinList` or `BuiltinArray`. The current builtins added in CIP-0153 do not cover +many of the use cases for `Value`. As a reference, see the Aiken stdlib's [functions on `Value`][2]. +Functions like `tokens`, `negate`, and `policies` are not possible to implement using the CIP-0153 +builtin functions which means they will not benefit from the new `BuiltinValue` type. Plutus needs +enough builtins to cover the main operations on `BuiltinValue`, either through generalizable +builtins or dedicated ones. + +## Specification + +This CIP proposes adding the following new builtins functions: + +```haskell +type BuiltinCurrencySymbol = BuiltinByteString + +-- | Negate all values in a `BuiltinValue`. +negate :: BuiltinValue -> BuiltinValue + +-- | Intersection of two `BuiltinValue`s. Returns data in the first value for the (policy ids, token +-- names) existing in both values. +intersection :: BuiltinValue -> BuiltinValue -> BuiltinValue + +-- | Returns all policy ids found in the value. +policies :: BuiltinValue -> List [BuiltinCurrencySymbol] + +-- | Return all tokens and their quantities for a given policy id. It returns a `BuiltinValue` so +-- that the result can make use of the `lookupCoin` builtin added in CIP-0153. It can always be +-- converted to a `List` for pattern-matching using the `valueData` builtin added in CIP-0153. +-- See the Rationale section for why `intersection` isn't used instead. +lookupTokens :: BuiltinCurrencySymbol -> BuiltinValue -> BuiltinValue +``` + +## Rationale: how does this CIP achieve its goals? + +After this CIP, the total set of `BuiltinValue` functions will be: + +```haskell +-- CIP-0153 functions +insertCoin :: BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinInteger -> BuiltinValue -> BuiltinValue +lookupCoin :: BuiltinValue -> BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinInteger +valueContains :: BuiltinValue -> BuiltinValue -> Bool +unionValue :: BuiltinValue -> BuiltinValue -> BuiltinValue +valueData :: BuiltinValue -> BuiltinData +unValueData :: BuiltinData -> BuiltinValue + +-- New functions +negate :: BuiltinValue -> BuiltinValue +intersection :: BuiltinValue -> BuiltinValue -> BuiltinValue +policies :: BuiltinValue -> List [BuiltinCurrencySymbol] +lookupTokens :: BuiltinCurrencySymbol -> BuiltinValue -> BuiltinValue +``` + +From these builtin functions, most of Aiken's stdlib `Value` functions can now make use of the +improved efficiency of CIP-0153's `BuiltinValue`. The single `intersection` function enables all +kinds of higher-level filtering functions using only a single builtin function. While `intersection` +is not a trivial function, it is very similar to `unionValue` which has already been costed for +CIP-0153. + +> [!NOTE] +> While `lookupTokens` could also be implemented using `intersection`, `lookupTokens` is just a +> simple outer map value lookup. It should be a very simple builtin to cost and maintain so +> accepting the performance hit from `intersection` instead likely isn't justified. For reference, +> Aiken's `tokens` function is used many times in a single contract execution (once per tx output) +> for protocols designed based on [CIP-89][9] which means the overhead from using `intersection` +> would quickly add up. + +### Does this create a "Slippery Slope"? + +Aside from `intersection`, all other proposed builtins are simple operations commonly found on `Map` +data structures. So the author does not believe this CIP sets a precedence for "builtin bloat". + +## Path to Active + +### Acceptance Criteria + +- [ ] The feature is implemented according to the implementation plan and merged into +the master branch of the [plutus][3] repository. +- [ ] [cardano-ledger][4] is updated to include new protocol parameters to control costing of +the new builtins. +- [ ] The feature is integrated into [cardano-node][5] and released as part of a hard fork. + +### Implementation Plan + +The implementation of this CIP should not proceed without an empirical assessment of the +effectiveness of the new primitives, as per the following plan: + +1. Implement the new primitives according to the specification. +2. Assign a preliminary cost to the new builtin functions. Consider similar operations and their + current costs. +3. Create variants of the [existing benchmarks][6] and potentially add some more. +4. Check that the builtin operations over `BuiltinValue` are indeed significantly faster. +5. Check that `lookupTokens` is significantly faster and/or uses significantly less execution memory + than a comparable implementation using `intersection`. + +If the preliminary performance investigation was not successful, this CIP should be revised +according to the findings of the experiment. Otherwise, the implementation can proceed: + +6. Determine the most appropriate costing functions for modelling the builtin's performance +and assign costs accordingly. +7. Add the new builtin type and functions to the appropriate sections in the [Plutus Core +Specification][7]. +8. Formalize the new builtin type and functions in the [plutus-metatheory][8]. +9. The final version of the feature is ready to be merged into [plutus][3] and accepted by +the Plutus Core team. + +## Copyright +This CIP is licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/legalcode). + +[1]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0153/README.md "CIP-0153" +[2]: https://aiken-lang.github.io/stdlib/cardano/assets.html "Aiken `Value` functions" +[3]: https://github.com/IntersectMBO/plutus/ "plutus" +[4]: https://github.com/IntersectMBO/cardano-ledger "cardano-ledger" +[5]: https://github.com/IntersectMBO/cardano-node "cardano-node" +[6]: https://github.com/IntersectMBO/plutus/tree/master/plutus-benchmark/script-contexts "script-context-benchmarks" +[7]: https://plutus.cardano.intersectmbo.org/resources/plutus-core-spec.pdf "Formal Specification of the Plutus Core Language" +[8]: https://github.com/IntersectMBO/plutus/tree/master/plutus-metatheory "plutus-metatheory" +[9]: https://github.com/cardano-foundation/CIPs/blob/master/CIP-0089/README.md "Beacon Tokens CIP" From a64128ecbd2295002bf3732ebab064b233276072 Mon Sep 17 00:00:00 2001 From: Robert Phair Date: Wed, 15 Oct 2025 01:32:28 +0545 Subject: [PATCH 2/5] include `this` PR as a Discussion Co-authored-by: Ryan --- CIP-????/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CIP-????/README.md b/CIP-????/README.md index c8f751bfc9..3e116c9e35 100644 --- a/CIP-????/README.md +++ b/CIP-????/README.md @@ -6,7 +6,8 @@ Status: Proposed Authors: - fallen-icarus Implementors: [] -Discussions: [] +Discussions: + - https://github.com/cardano-foundation/CIPs/pull/1090 Created: 2025-10-01 License: CC-BY-4.0 --- From a32e0b0619909faca62b32e991fdddd1f5e7b854 Mon Sep 17 00:00:00 2001 From: Robert Phair Date: Wed, 15 Oct 2025 01:33:51 +0545 Subject: [PATCH 3/5] assign CIP number 168 --- CIP-????/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CIP-????/README.md b/CIP-????/README.md index 3e116c9e35..0e6c2f4d1d 100644 --- a/CIP-????/README.md +++ b/CIP-????/README.md @@ -1,5 +1,5 @@ --- -CIP: ?? +CIP: 168 Title: More `BuiltinValue` Functions Category: Plutus Status: Proposed From 560158dfc7d85d0e432909ece2802e5282ce8100 Mon Sep 17 00:00:00 2001 From: fallen-icarus Date: Mon, 20 Oct 2025 08:47:47 -0400 Subject: [PATCH 4/5] docs: renamed CIP-0168 directory --- {CIP-???? => CIP-0168}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {CIP-???? => CIP-0168}/README.md (100%) diff --git a/CIP-????/README.md b/CIP-0168/README.md similarity index 100% rename from CIP-????/README.md rename to CIP-0168/README.md From 6707d003fcf09f6f67ec326d6895101a04b54fbc Mon Sep 17 00:00:00 2001 From: fallen-icarus Date: Mon, 24 Nov 2025 12:44:51 -0500 Subject: [PATCH 5/5] docs: updated README based on feedback --- CIP-0168/README.md | 196 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 158 insertions(+), 38 deletions(-) diff --git a/CIP-0168/README.md b/CIP-0168/README.md index 0e6c2f4d1d..cd48289caf 100644 --- a/CIP-0168/README.md +++ b/CIP-0168/README.md @@ -27,17 +27,29 @@ burden. > UPLC is not a general-purpose programming language. It is a language specifically designed to > write validation logic to set constraints for the creation and transfer of Value and Data across > the Cardano blockchain. **Given that half of the entire purpose of this language is to express -> constraints over the creation and transfer of** `Value`, why is Value treated as a standard +> constraints over the creation and transfer of Value**, why is Value treated as a standard > library concept rather than a first-class language primitive? This was the motivation for [CIP-0153][1]. Given the overwhelming importance for validating `Value`, it makes sense for `BuiltinValue` to have a reasonably disproportionate number of builtin operations as compared to `BuiltinList` or `BuiltinArray`. The current builtins added in CIP-0153 do not cover many of the use cases for `Value`. As a reference, see the Aiken stdlib's [functions on `Value`][2]. -Functions like `tokens`, `negate`, and `policies` are not possible to implement using the CIP-0153 -builtin functions which means they will not benefit from the new `BuiltinValue` type. Plutus needs -enough builtins to cover the main operations on `BuiltinValue`, either through generalizable -builtins or dedicated ones. +Functions like `tokens` and `policies` are not possible to implement using the CIP-0153 builtin +functions which means they will not benefit from the new `BuiltinValue` type. Plutus needs enough +builtins to cover the main operations on `BuiltinValue`, either through generalizable builtins or +dedicated ones. + +As an example use case, [CIP-89-based distributed dApps][9] rely on special native assets (aka +Beacon Tokens) to tag UTxOs. These Beacon Token tags can then be used by off-chain indexers to +filter the UTxO set to see only the UTxOs relevant to that dApp. This functionality is critical for +supporting self-sovereign DeFi since it enables all users to get their own DeFi addresses - a +requirement for maintaining delegation control of DeFi assets. But as a consequence, these +distributed dApp protocols need to be very careful about how and when these Beacon Tokens are +minted/spent. [Cardano-Swaps][10] checks every transaction output to see if they contain beacon +tokens by calling `tokens` ([here][11]). Since `tokens` is just traversing the `Value` as a `List`, +this is significantly less efficient than the `O(1)` lookup possible by treating `Value` as a `Map`. +This inefficiency compounds when the Cardano-Swaps protocol is executed as part of a larger +transaction cart that involve other outputs and dApps. ## Specification @@ -46,21 +58,37 @@ This CIP proposes adding the following new builtins functions: ```haskell type BuiltinCurrencySymbol = BuiltinByteString --- | Negate all values in a `BuiltinValue`. -negate :: BuiltinValue -> BuiltinValue +-- | A constant used for an empty value. +emptyValue :: BuiltinValue --- | Intersection of two `BuiltinValue`s. Returns data in the first value for the (policy ids, token --- names) existing in both values. -intersection :: BuiltinValue -> BuiltinValue -> BuiltinValue +-- | Convert a `BuiltinValue` to a `BuiltinList`. This is useful for pattern-matching. +valueToList :: BuiltinValue -> [(BuiltinCurrencySymbol, (BuiltinTokenName, BuiltinInteger)] -- | Returns all policy ids found in the value. -policies :: BuiltinValue -> List [BuiltinCurrencySymbol] +policies :: BuiltinValue -> [BuiltinCurrencySymbol] + +-- | Return all tokens and their quantities for the specified policy ids. It returns a +-- `BuiltinValue` so that the result can make use of the `lookupCoin` builtin added in CIP-0153. +-- It can always be converted to a `List` for pattern-matching using the `valueData` builtin added +-- in CIP-0153. +restrictValueTo :: [BuiltinCurrencySymbol] -> BuiltinValue -> BuiltinValue + +-- | Filters out the policies from the `BuiltinValue`. +filterOutPolicies :: [BuiltinCurrencySymbol] -> BuiltinValue -> BuiltinValue + +-- | The number of policies present. If `BuiltinValue` is tracking its size, this should be a very +-- simple and efficient builtin. +policyCount :: BuiltinValue -> BuiltinInteger --- | Return all tokens and their quantities for a given policy id. It returns a `BuiltinValue` so --- that the result can make use of the `lookupCoin` builtin added in CIP-0153. It can always be --- converted to a `List` for pattern-matching using the `valueData` builtin added in CIP-0153. --- See the Rationale section for why `intersection` isn't used instead. -lookupTokens :: BuiltinCurrencySymbol -> BuiltinValue -> BuiltinValue +-- | The number of asset names present across all of the policy ids. This could be useful after +-- first restricting the `BuiltinValue` with `restrictValueTo`. If `BuiltinValue` is tracking +-- its size, this should be a very simple and efficient builtin. Is this more efficient than +-- pattern-matching on the result of `restrictValueTo`??? +assetCount :: BuiltinValue -> BuiltinInteger + +-- | Value equality. I'm assuming this is `==`, but just in case it isn't actually supported on +-- `BuiltinValue` I'm adding it here. +valueEquals :: BuiltinValue -> BuiltinValue -> BuiltinBool ``` ## Rationale: how does this CIP achieve its goals? @@ -75,32 +103,124 @@ valueContains :: BuiltinValue -> BuiltinValue -> Bool unionValue :: BuiltinValue -> BuiltinValue -> BuiltinValue valueData :: BuiltinValue -> BuiltinData unValueData :: BuiltinData -> BuiltinValue +scaleValue :: BuiltinInteger -> BuiltinValue -> BuiltinValue -- New functions -negate :: BuiltinValue -> BuiltinValue -intersection :: BuiltinValue -> BuiltinValue -> BuiltinValue -policies :: BuiltinValue -> List [BuiltinCurrencySymbol] -lookupTokens :: BuiltinCurrencySymbol -> BuiltinValue -> BuiltinValue +emptyValue :: BuiltinValue +valueToList :: BuiltinValue -> BuiltinList +policies :: BuiltinValue -> [BuiltinCurrencySymbol] +restrictValueTo :: [BuiltinCurrencySymbol] -> BuiltinValue -> BuiltinValue +filterOutPolicies :: [BuiltinCurrencySymbol] -> BuiltinValue -> BuiltinValue +policyCount :: BuiltinValue -> BuiltinInteger +assetCount :: BuiltinValue -> BuiltinInteger +valueEquals :: BuiltinValue -> BuiltinValue -> BuiltinBool ``` From these builtin functions, most of Aiken's stdlib `Value` functions can now make use of the -improved efficiency of CIP-0153's `BuiltinValue`. The single `intersection` function enables all -kinds of higher-level filtering functions using only a single builtin function. While `intersection` -is not a trivial function, it is very similar to `unionValue` which has already been costed for -CIP-0153. - -> [!NOTE] -> While `lookupTokens` could also be implemented using `intersection`, `lookupTokens` is just a -> simple outer map value lookup. It should be a very simple builtin to cost and maintain so -> accepting the performance hit from `intersection` instead likely isn't justified. For reference, -> Aiken's `tokens` function is used many times in a single contract execution (once per tx output) -> for protocols designed based on [CIP-89][9] which means the overhead from using `intersection` -> would quickly add up. +improved efficiency of CIP-0153's `BuiltinValue`. + +### Supported Higher-Level Functions + +These are all taken from the [aiken `Value` functions][2]. + +```haskell +fromAsset :: BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinInteger -> BuiltinValue +fromAsset pol name quantity = insertCoin pol name quantity emptyValue + +fromAssetList :: [(BuiltinCurrencySymbol, (BuiltinTokenName, BuiltinInteger))] -> BuiltinValue +fromAssetList xs = + foldr (\(pol, (name, quantity)) acc -> insertCoin pol name quantity acc) emptyValue xs + +fromLovelace :: BuiltinInteger -> BuiltinValue +fromLovelace quantity = insertCoin "" "" quantity emptyValue + +hasAnyNft :: BuiltinValue -> BuiltinCurrencySymbol -> BuiltinBool +hasAnyNft val pol = not $ null $ valueToList $ restrictValueTo [pol] val + +hasAnyNftStrict :: BuiltinValue -> BuiltinCurrencySymbol -> BuiltinBool +hasAnyNftStrict val pol = restrictValueTo ["", pol] val == val + +hasNft :: BuiltinValue -> BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinBool +hasNft val pol name = lookupCoin val pol name > 0 + +hasNftStrict :: BuiltinValue -> BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinBool +hasNftStrict val pol name = lookupCoin val pol name > 0 && restrictedResult == val + where restrictedResult = restrictValueTo ["", pol] val + +isZero :: BuiltinValue -> BuiltinBool +isZero = null . valueToList + +match + :: BuiltinValue + -> BuiltinData + -> (BuiltinInteger -> BuiltinInteger -> BuiltinBool) + -> BuiltinBool +match left right_data assert_lovelace = assets_are_equal && lovelace_check_passes + where + left_lovelace = lovelaceOf left + left_assets = withoutLovelace left + + right_value = unValueData right_data + right_lovelace = lovelaceOf right_value + right_assets = withoutLovelace right_value + + assets_are_equal = valueData left_assets == valueData right_assets + lovelace_check_passes = assert_lovelace left_lovelace right_lovelace + +lovelaceOf :: BuiltinValue -> BuiltinInteger +lovelaceOf val = lookupCoin val "" "" + +policies :: BuiltinValue -> [BuiltinCurrencySymbol] +policies = policies + +quantityOf :: BuiltinValue -> BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinInteger +quantityOf = lookupCoin + +tokens :: BuiltinValue -> BuiltinCurrencySymbol -> [(TokenName, Integer)] +tokens val pol = case valueToList $ restrictValueTo [pol] val of + [] -> [] + (_, ts):_ -> ts + +add :: BuiltinValue -> BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinInteger -> BuiltinValue +add val pol name newQuantity = insertCoin pol name (current + newQuantity) val + where current = lookupCoin val pol name + +merge :: BuiltinValue -> BuiltinValue -> BuiltinValue +merge = unionValue + +negate :: BuiltinValue -> BuiltinValue +negate = scaleValue (-1) + +restrictedTo :: BuiltinValue -> [BuiltinCurrencySymbol] -> BuiltinValue +restrictedTo val ps = restrictValueTo ps val + +withoutLovelace :: BuiltinValue -> BuiltinValue +withoutLovelace = filterOutPolicies [""] + +subtractValue :: BuiltinValue -> BuiltinValue -> BuiltinValue +subtractValue val1 val2 = unionValue val1 $ negate val2 + +flatten :: BuiltinValue -> [(BuiltinCurrencySymbol , (BuiltinTokenName, BuiltinInteger))] +flatten = valueToList + +flattenWith + :: BuiltinValue + -> (BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinInteger -> a) + -> [a] +flattenWith val f = map (\(pol, (name, q)) -> f pol name q) $ valueToList val + +reduce + :: BuiltinValue + -> a + -> (a -> BuiltinCurrencySymbol -> BuiltinTokenName -> BuiltinInteger -> a) + -> a +reduce val initial f = foldr (\(pol, (name, q)) acc -> f acc pol name q) initial $ valueToList val +``` ### Does this create a "Slippery Slope"? -Aside from `intersection`, all other proposed builtins are simple operations commonly found on `Map` -data structures. So the author does not believe this CIP sets a precedence for "builtin bloat". +All of the proposed builtins are simple operations commonly found on `Map` data structures. So the +author does not believe this CIP sets a precedence for "builtin bloat". ## Path to Active @@ -122,17 +242,15 @@ effectiveness of the new primitives, as per the following plan: current costs. 3. Create variants of the [existing benchmarks][6] and potentially add some more. 4. Check that the builtin operations over `BuiltinValue` are indeed significantly faster. -5. Check that `lookupTokens` is significantly faster and/or uses significantly less execution memory - than a comparable implementation using `intersection`. If the preliminary performance investigation was not successful, this CIP should be revised according to the findings of the experiment. Otherwise, the implementation can proceed: 6. Determine the most appropriate costing functions for modelling the builtin's performance and assign costs accordingly. -7. Add the new builtin type and functions to the appropriate sections in the [Plutus Core +7. Add the new builtin functions to the appropriate sections in the [Plutus Core Specification][7]. -8. Formalize the new builtin type and functions in the [plutus-metatheory][8]. +8. Formalize the new builtin functions in the [plutus-metatheory][8]. 9. The final version of the feature is ready to be merged into [plutus][3] and accepted by the Plutus Core team. @@ -148,3 +266,5 @@ This CIP is licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4 [7]: https://plutus.cardano.intersectmbo.org/resources/plutus-core-spec.pdf "Formal Specification of the Plutus Core Language" [8]: https://github.com/IntersectMBO/plutus/tree/master/plutus-metatheory "plutus-metatheory" [9]: https://github.com/cardano-foundation/CIPs/blob/master/CIP-0089/README.md "Beacon Tokens CIP" +[10]: https://github.com/fallen-icarus/cardano-swaps "Cardano-Swaps" +[11]: https://github.com/fallen-icarus/cardano-swaps/blob/9ec41e7619f5ba9d3dd46dd194e2146098093721/aiken/lib/cardano_swaps/one_way_swap/utils.ak#L328 "`tokens` usage"