Closed mesudip closed 1 month ago
@reeshavacharya @mesudip @nabinpkl , Whenever you encounter anything that needs to be noted for PlutusV3, we add comment here to remind us later.
@reeshavacharya encountered change in fee
field in PlutusV3
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.0.0 #-}
configurableMarketValidator constructor =
$$(PlutusTx.compile [|| mkWrappedConfigurableMarket ||])
`unsafeApplyCode` PlutusTx.liftCode plcVersion100 constructor
When compiling for PlutusV2 with new compiler it is required that the pragma for target-version is included in the file, and when lifting code, plcVersion100 should be provided to PlutusTx.liftCode
Cardano Marketplace is a straightforward DApp for an NFT marketplace utilizing Plutus smart contracts. It supports minting assets, placing them on sell in the marketplace, purchasing them from the marketplace, and withdrawing them if you are the asset owner. Additionally, it can perform these tasks with reference scripts.
Types
The Cardano marketplace has been implemented using Plutus V1, V2, and V3. This article serves as a migration guide from Plutus V2 to Plutus V3.
A significant change with the new Plutus dependency, as introduced by the CIP-0069, affects the arguments given to all types of Plutus scripts:
"This CIP unifies the arguments given to all types of Plutus scripts currently available (spending, certifying, rewarding, minting) by removing the argument of a datum."
Developers must update their DApps if they wish to use the latest dependencies. Previously, all script purposes followed the form Redeemer -> ScriptContext -> a
, except spending scripts which were Datum -> Redeemer -> ScriptContext -> a
. With the changes from CIP-0069, both Redeemer and Datum can be decoded from ScriptContext, making the validator take only one argument: ScriptContext -> a
. This same update applies to Cardano-Marketplace.
In V3, the scriptInfo can identify whether the script is for minting, spending, rewarding, certifying, voting, or proposing. Details about the datum can be obtained if the script is a spending script. This update changes the 3-parameter validator to a 1-parameter validator. The validator still takes an extra argument if the smart contract is to be parameterized. Also, the check function now returns a BuiltinUnit
data type. Thus, a validator for a smart contract with V3 dependency looks like this:
validator :: BuiltinData -> BuiltinUnit
validator context = check $ validatorLogic (unsafeFromBuiltinData context)
where
vaildatorLogic :: ScriptContext -> Bool
vaildatorLogic = ... --decoding of the script-context for datum and redeemer information can be done here
For smart contracts using Plutus V2, the validator still takes 3 arguments, but the return type changes to BuiltinUnit. For example:
validator :: BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit
validator datum redeemer context =
check $
validatorLogic
(unsafeFromBuiltinData datum)
(unsafeFromBuiltinData redeemer)
(unsafeFromBuiltinData context)
where
validatorLogic :: Datum -> Redeemer -> ScriptContext -> Bool
validatorLogic datum redeemer context = ...
If developers want to use PlutusLedgerApi.V2
dependencies, the only change needed is to the return type of the validator. Since the type signature for the check function has changed from Bool -> ()
to Bool -> BuiltinUnit
, the validator implementing this function must also return BuiltinUnit
. This type is compatible with the PlutusTx.compile
function.
This implementation has been tested and verified in the Cardano-Marketplace. Here is a sample of the validator using this upgrade:
{-# INLINABLE mkWrappedMarket #-}
mkWrappedMarket :: BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit
mkWrappedMarket d r c =
check $
mkMarket
(parseData d "Invalid data")
(parseData r "Invalid redeemer")
(parseData c "Invalid context")
where
parseData md s = case fromBuiltinData md of
Just datum -> datum
_ -> traceError s
to see the full implementation, visit Cardano-MarketPlace-V2/SimpleMarketplace
For developers intending to use PlutusLedgerApi.V3
, there needs to be additional changes in the smart contract code. A similar implementation using PlutusV3 for a simple marketplace can be found here: Cardano-MarketPlace-V3/SimpleMarketplace. In this implementation, the validator looks like this:
{-# INLINABLE mkWrappedMarket #-}
mkWrappedMarket :: BuiltinData -> BuiltinUnit
mkWrappedMarket ctx = check $ mkMarket (parseData ctx "Invalid context")
It takes only one argument, BuiltinData
, which is parsed into the ScriptContext
.
Decoding the datum and the redeemer now depends on the script's purpose. Mainly, DApps are developed with scripts whose purpose is to either mint, spend, certify, or reward. V3 offers additional purposes, including voting and proposing, related to governance actions introduced in the Chang hard fork. Let's look into decoding datum and redeemer.
Decoding Datum
Datum is only needed when the script is of the spending type. Therefore, the datum will come from the scriptInfo
in the scriptContext
. Here is a simple example where we expect the script to be of the spending type and get the datum:
ctx :: ScriptContext -- PlutusV3 script-context
scriptInfo :: ScriptInfo
scriptInfo = scriptContextScriptInfo ctx
datum :: Datum
datum = case scriptInfo of
SpendingScript outRef datum -> case datum of
Just d -> case fromBuiltinData (getDatum d) of
Nothing -> traceError "Invalid datum format"
Just v -> v
Nothing -> traceError "Missing datum"
_ -> traceError "Script Purpose Mismatch"
Decoding Redeemer
For decoding the redeemer, we don't need to expect the script purpose to be of a particular type. It can be directly decoded from the scriptContext. For a scriptContext ctx, the redeemer can be extracted in the following way:
redeemer :: Redeemer
redeemer = case fromBuiltinData $ getRedeemer (scriptContextRedeemer ctx) of
Nothing -> traceError "Invalid Redeemer"
Just r -> r
Just like parsing datum to a correct data type, we can parse the redeemer to BuiltinData using getRedeemer first and then to the required data type using fromBuiltinData or unsafeFromBuiltinData.
For the simple cardano marketplace contract using PlutusV3, the validator logic function is available at Cardano-Marketplace-V3/SimpleMarketplace. This function takes ScriptContext only, with the decoding of datum and redeemer done inside the function.
With more functionalities introduced in PlutusV3 for governance actions, the ScriptContext
has additional fields. Because of this, the size of the script (in bytes) of smart contracts written using PlutusLedgerApi.V3
has increased a considerable amount.
A workaround to this problem is to write the validator lazily, not decoding the script-context entirely, but by only parsing the required fields contained within the script-context.
The script-context initially will be of the form BuiltinData
. We can then change this to a BuiltinList using the following function:
import qualified PlutusTx.Builtins.Internal as BI
constrArgs :: BuiltinData -> BI.BuiltinList BuiltinData
constrArgs bd = BI.snd (BI.unsafeDataAsConstr bd)
This allows us to extract only the necessary information from the script-context without needing to decode it fully, as the unsafeFromBuiltinData
function does. But, this comes at the expense of a cleaner code. Indexing of a BuiltinList
type is not supported in Plutus yet. So, we have to utilize the BI.head
and BI.tail
function repeatedly in order to get the element we want. For example, to get the datum and redeemer from script-context, the following approach can be followed:
ctx :: BuiltinData
context :: BI.BuiltinList BuiltinData
context = constrArgs ctx
redeemerFollowedByScriptInfo :: BI.BuiltinList BuiltinData
redeemerFollowedByScriptInfo = BI.tail context
redeemerBuiltinData :: BuiltinData
redeemerBuiltinData = BI.head redeemerFollowedByScriptInfo
scriptInfoData :: BuiltinData
scriptInfoData = BI.head (BI.tail redeemerFollowedByScriptInfo)
datumData :: BuiltinData
datumData = BI.head (constrArgs (BI.head (BI.tail (constrArgs scriptInfoData))))
redeemer :: Redeemer
redeemer = unsafeFromBuiltinData redeemerBuiltinData
datum :: Datum
datum = unsafeFromBuiltinData (getDatum (unsafeFromBuiltinData datumData))
The lazy validator for the simple marketplace can be found here: Cardano-Marketplace-V3/SimpleMarketplace-Lazy
This method can be taken to the next level with super lazy evaluation, where we decode the txInfo
to obtain the required inputs, outputs, signatures, etc., for the validation logic, while ignoring the unused fields. For instance, the following method can be used to get the inputs, reference inputs, outputs and signature of a transaction from a script-context.
context = constrArgs ctx
txInfoData :: BuiltinData
txInfoData = BI.head context
lazyTxInfo :: BI.BuiltinList BuiltinData
lazyTxInfo = constrArgs txInfoData
inputs :: [TxInInfo]
inputs = parseData (BI.head lazyTxInfo) "txInfoInputs: Invalid [TxInInfo] type"
refInputs :: [TxInInfo]
refInputs = parseData (BI.head (BI.tail lazyTxInfo)) "txInfoReferenceInputs: Invalid [TxInInfo] type"
outputs :: [TxOut]
outputs = parseData (BI.head (BI.tail (BI.tail lazyTxInfo))) "txInfoOutputs: Invalid [TxOut] type"
signatures :: [PubKeyHash]
signatures = parseData
(BI.head $ BI.tail $ BI.tail $ BI.tail $ BI.tail $ BI.tail $ BI.tail $ BI.tail $ BI.tail lazyTxInfo)
"txInfoSignatories: Invalid [PubKeyHash] type"
This has a very pronounced effect on the size of a cbor script.
The latest run of the Cardano Marketplace on sanchonet generated a report indicating the following:
Simple Market
Implemented With | ScriptBytes |
---|---|
V2 | 4594 |
V2 Super Lazy | 2848 |
V3 | 8118 |
V3 Lazy | 7125 |
V3 Super Lazy | 2573 |
Test Name | Ex-Units (Mem) | Ex-Units (CPU) | Fee | Tx Bytes | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | |
Mint Native Asset | - | - | - | - | - | - | - | - | - | - | 199513 | 194761 | 196345 | 193177 | 193177 | 903 | 795 | 831 | 759 | 759 |
Create reference script UTxO | - | - | - | - | - | - | - | - | - | - | 374017 | 297193 | 530657 | 486965 | 285093 | 4869 | 3123 | 8429 | 7436 | 2848 |
Place on Sell | - | - | - | - | - | - | - | - | - | - | 203341 | 203341 | 203341 | 203341 | 203341 | 990 | 990 | 990 | 990 | 990 |
Withdraw | 392969717 | 264005954 | 262872340 | 229236652 | 169215285 | 1448166 | 974134 | 1196886 | 1045672 | 768634 | 493566 | 380092 | 624743 | 569901 | 349300 | 5043 | 3297 | 8567 | 7574 | 3022 |
Buy | 507136034 | 377988271 | 358360316 | 284463862 | 224250495 | 1898158 | 1423326 | 1665762 | 1334964 | 1056726 | 528862 | 415328 | 659782 | 591675 | 370991 | 5068 | 3322 | 8592 | 7599 | 3047 |
Withdraw with RefScript | 453987773 | 264005954 | 300088646 | 266452958 | 120645750 | 1671344 | 974134 | 1364938 | 1213724 | 549124 | 378849 | 298732 | 402933 | 376888 | 258164 | 477 | 477 | 477 | 477 | 441 |
Buy with RefScript | 648618799 | 377988271 | 445095365 | 371198911 | 224250495 | 2418374 | 1423326 | 2058626 | 1727828 | 1056726 | 438889 | 334188 | 456318 | 417008 | 297826 | 543 | 507 | 543 | 543 | 507 |
Configurable Market
Implemented With | ScriptBytes |
---|---|
V2 | 3615 |
V2 Super Lazy | 2875 |
V3 | 7883 |
V3 Lazy | 7263 |
V3 Super Lazy | 2586 |
Test Name | Ex-Units (Mem) | Ex-Units (CPU) | Fee | Tx Bytes | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | V2 | V2 Super Lazy | V3 | V3 Lazy | V3 Super Lazy | |
Mint Native Asset | - | - | - | - | - | - | - | - | - | - | 197929 | 199513 | 199513 | 194761 | 196345 | 867 | 903 | 903 | 795 | 831 |
Create reference script UTxO | - | - | - | - | - | - | - | - | - | - | 330941 | 298381 | 520317 | 493037 | 285665 | 3890 | 3150 | 8194 | 7574 | 2861 |
Place on Sell | - | - | - | - | - | - | - | - | - | - | 203341 | 203341 | 203341 | 203341 | 203341 | 990 | 990 | 990 | 990 | 990 |
Create market configuration | - | - | - | - | - | - | - | - | - | - | 179141 | 179141 | 179141 | 179141 | 179141 | 440 | 440 | 440 | 440 | 440 |
Withdraw | 180264963 | 264989341 | 242637957 | 228228652 | 169727737 | 582806 | 977362 | 1106444 | 1039372 | 769862 | 385223 | 381537 | 607726 | 575537 | 349980 | 4064 | 3324 | 8332 | 7712 | 3035 |
Withdraw with RefScript | 203610477 | 325823397 | 279854263 | 265444958 | 206864043 | 653668 | 1199740 | 1274496 | 1207424 | 937414 | 287611 | 316831 | 392950 | 378741 | 288783 | 482 | 482 | 482 | 482 | 482 |
Buy | 532349306 | 707700378 | 518936842 | 455008794 | 397051879 | 1938892 | 2698356 | 2482524 | 2190640 | 1924530 | 494618 | 518522 | 714614 | 664080 | 438758 | 4195 | 3455 | 8504 | 7843 | 3166 |
Buy with RefScript | 557534820 | 768534434 | 549977615 | 535568310 | 434764185 | 2017754 | 2920734 | 2622368 | 2555296 | 2095682 | 397381 | 453596 | 497327 | 483118 | 377591 | 608 | 608 | 644 | 644 | 608 |
Conclusion
Across both Simple Market and Configurable Market implementations, V3 Super Lazy consistently emerges as the most efficient option in terms of script bytes, memory, CPU, fees, and transaction bytes.
Non Technical report
Cardano-Marketplace
The Cardano Marketplace is a decentralized application (DApp) for buying and selling NFTs, built with Plutus smart contracts. It comes in two flavors:
The DApp has versions using Plutus V2 and V3. We will analyze the changes in efficiency, specifically execution units, fees, and script sizes. The smart contracts for the marketplace are written using the following implementations:
PlutusLedgerApi.V2
and its ScriptContext
PlutusLedgerApi.V3
and its ScriptContext
PlutusLedgerApi.V3
but avoids decoding the entire ScriptContext
. It decodes the context only one step to obtain the TxInfo
field, the redeemer, and the datum from ScriptInfo
, all of which are included in the ScriptContext
. ScriptContext
. Instead, it extracts the script context to get the TxInfo
field and further extracts it to obtain the necessary transaction objects for validation, such as outputs, inputs, reference inputs, and signatures. This way, the contract does not need to decode the entire script context, only the necessary fields for validation are passed. The detailed report on script implementations for the cardano marketplace reveals significant insights into the efficiency of various versions of Plutus scripts. The marketplace was tested using the afforementioned implementations. Each implementation was evaluated based on script size (in Bytes), execution units (Ex-Units) for both memory and CPU, transaction fees, and transaction bytes.
Market Type | Implemented With | ScriptBytes |
---|---|---|
Simple Market | ||
V2 | 4594 | |
V2 Super Lazy | 2848 | |
V3 | 8118 | |
V3 Lazy | 7125 | |
V3 Super Lazy | 2573 | |
Configurable Market | ||
V2 | 3615 | |
V2 Super Lazy | 2875 | |
V3 | 7883 | |
V3 Lazy | 7263 | |
V3 Super Lazy | 2586 |
Click here to view the full report
Script Size
It's evident that the V3 implementations tend to have larger script sizes compared to the V2 implementations. This is due to the additional fields included in the TxInfo
field of V3 to support governance actions. However, the Super Lazy versions of both V2 and V3 demonstrate remarkable efficiency in script size. This reduction is achieved through optimization techniques in the Super Lazy implementations, where only the necessary parts of the ScriptContext are decoded. However, the Super Lazy implementation of V3 is still more efficient in terms of size compared to the same implementation in V2.
Fees
For transactions using V2 and V3 scripts without the lazy implementation, V3 transactions generally incur higher fees compared to V2. This is due to the larger script size of V3, which results in a bigger transaction size and thus higher fees. However, with the super lazy implementation, V3 has a slight advantage over V2, as it results in a lower fee.
Execution Units
V3 scripts are more efficient than V2 scripts in terms of execution units, including both memory and CPU, even without the lazy implementation. Despite the additional fields in the TxInfo
field that increase the script size, V3 transactions require less memory and CPU compared to V2. This efficiency is further enhanced with the Super Lazy implementation of V3, where the difference in execution unit efficiency compared to V2 becomes even more pronounced.
Overall, while V3 scripts inherently involve larger sizes and potentially higher fees, their efficiency in terms of execution units—especially with the Super Lazy implementation—demonstrates a significant advantage over V2.
TODO make a docs file from these content
This is the final document shared with Intersect to share with the community. https://github.com/cardanoapi/hardfork-testing/blob/main/migration.md
Prepare a migration guide similar to one we prepared for vasil hardfork.