cardanoapi / hardfork-testing

Hardfork testing by Dquadrant Team (Project management repository)
0 stars 0 forks source link

Documentation/Migration guide for PlutusV3 #6

Closed mesudip closed 1 month ago

mesudip commented 3 months ago

Prepare a migration guide similar to one we prepared for vasil hardfork.

mesudip commented 3 months ago

@reeshavacharya @mesudip @nabinpkl , Whenever you encounter anything that needs to be noted for PlutusV3, we add comment here to remind us later.

mesudip commented 3 months ago

@reeshavacharya encountered change in fee field in PlutusV3

mesudip commented 3 months ago

Compiling PlutusV2 with latest plutus

{-# 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

reeshavacharya commented 1 month ago

Migrating Cardano-Marketplace from Plutus V2 to Plutus V3

Cardano-Marketplace

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.

Effects of Chang Hard Fork on DApps and Smart Contracts

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.

References

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 = ...

What To Do

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 Datum and Redeemer

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.

Changes In Efficiency

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.

reeshavacharya commented 1 month ago

Non Technical report

Implementing Cardano Marketplace in Plutus V3

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:

Efficiency Report

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


Analysis


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.

mesudip commented 1 month ago

TODO make a docs file from these content

reeshavacharya commented 1 month ago

Article:

Migration Guide

spannercode commented 1 month ago

This is the final document shared with Intersect to share with the community. https://github.com/cardanoapi/hardfork-testing/blob/main/migration.md