IntersectMBO / plutus

The Plutus language implementation and tools
Apache License 2.0
1.57k stars 476 forks source link

PlutusV3 script size is larger than PlutusV2 script size #6151

Closed reeshavacharya closed 5 months ago

reeshavacharya commented 5 months ago

Summary

The issue came up when I compared two similar smart contracts for minting an NFT: one written using PlutusLedgerApi.V3 and the other using PlutusLedgerApi.V2. When I compare the ShortByteString of the SerialisedScript generated by the serialiseCompiledCode function, I see that the V3 script is significantly larger than the V2 script. I might be mistaken about this, so please clarify if I am.

Steps to reproduce the behavior

This contract is the one written using PlutusLedgerApi.V3 and compiled using plcVersion110:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.1.0 #-}

module V3.Mint.VerifyMintingMaxExUnits where

import PlutusCore.Core (plcVersion110)
import PlutusLedgerApi.V1.Value (flattenValue)
import PlutusLedgerApi.V3
import PlutusTx (liftCode)
import PlutusTx qualified
import PlutusTx.Code (unsafeApplyCode)
import PlutusTx.Prelude

{-# INLINEABLE mkValidator #-}
mkValidator :: TxOutRef -> () -> ScriptContext -> Bool
mkValidator txIn _ ctx = mustMintNFT && hasInput
  where
    info = scriptContextTxInfo ctx
    mint = txInfoMint info
    inputs = map txInInfoOutRef $ txInfoInputs info
    mintedValue = flattenValue mint
    mustMintNFT = case mintedValue of
        [(_, _, n)] -> n == 1
        _ -> traceError "Must mint a single NFT"
    hasInput = txIn `elem` inputs

mkWrappedValidator :: TxOutRef -> BuiltinData -> BuiltinData -> ()
mkWrappedValidator input red_ ctx_ = check $ mkValidator input (unsafeFromBuiltinData red_) (unsafeFromBuiltinData ctx_)

validator :: TxOutRef -> PlutusTx.CompiledCode (BuiltinData -> BuiltinData -> ())
validator input = $$(PlutusTx.compile [||mkWrappedValidator||]) `unsafeApplyCode` liftCode plcVersion110 input

And this one is written using PlutusLedgerApi.V2 and compiled using plcVersion100:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.0.0 #-}

module V2.Mint.VerifyMintingMaxExUnits where

import PlutusCore.Core (plcVersion100)
import PlutusLedgerApi.V1.Value (flattenValue)
import PlutusLedgerApi.V2
import PlutusTx (liftCode)
import PlutusTx qualified
import PlutusTx.Code (unsafeApplyCode)
import PlutusTx.Prelude

{-# INLINEABLE mkValidator #-}
mkValidator :: TxOutRef -> () -> ScriptContext -> Bool
mkValidator txIn _ ctx = mustMintNFT && hasInput
  where
    info = scriptContextTxInfo ctx
    mint = txInfoMint info
    inputs = map txInInfoOutRef $ txInfoInputs info
    mintedValue = flattenValue mint
    mustMintNFT = case mintedValue of
        [(_, _, n)] -> n == 1
        _ -> traceError "Must mint a single NFT"
    hasInput = txIn `elem` inputs

mkWrappedValidator :: TxOutRef -> BuiltinData -> BuiltinData -> ()
mkWrappedValidator input red_ ctx_ = check $ mkValidator input (unsafeFromBuiltinData red_) (unsafeFromBuiltinData ctx_)

validator :: TxOutRef -> PlutusTx.CompiledCode (BuiltinData -> BuiltinData -> ())
validator input = $$(PlutusTx.compile [||mkWrappedValidator||]) `unsafeApplyCode` liftCode plcVersion100 input

Now, generating serialisedScript:

v3sbs = serialiseCompiledCode $ V3.Mint.VerifyMintingMaxExUnits.validator (TxOutRef  "0d967444377dd05f4a5e9d9bfdf168b959bed43655635f8daa63a07f1bd2ff7c" 0)

v2sbs = serialiseCompiledCode $ V2.Mint.VerifyMintingMaxExUnits.validator (TxOutRef  "0d967444377dd05f4a5e9d9bfdf168b959bed43655635f8daa63a07f1bd2ff7c" 0)

efficiency = v3sbs < v2sbs

main :: IO ()
main = do
  putStrLn (show efficiency)

I also recorded some data of the length of the shortByteString generated with 2 different plcVersions using both V2 and V3: PlutusLedgerApi.V3 with plcVersion110 : 7986 PlutusLedgerApi.V2 with plcVersion110 : 4781 PlutusLedgerApi.V3 with plcVersion100 : 9288 PlutusLedgerApi.V2 with plcVersion100 : 5369

In all the cases, it looks like V2 is generating a shorter length of code.

Actual Result

False

Expected Result

True

Describe the approach you would take to fix this

No response

System info

data from cabal.project.freeze:

any.plutus-core ==1.21.0.0, plutus-core -with-cert -with-inline-r, any.plutus-ledger-api ==1.21.0.0, any.plutus-tx ==1.21.0.0, any.plutus-tx-plugin ==1.21.0.0

any.cardano-api ==8.38.0.2

effectfully commented 5 months ago

If you happen to have Plutus IR of these two contracts, could you please post it as well?

reeshavacharya commented 5 months ago

Could you tell me how to generate Plutus IR? or just guide me to some source?

effectfully commented 5 months ago

Don't bother then, we'll look into it. Thanks a lot for reporting the issue.

reeshavacharya commented 5 months ago

awesome. thanks.

zliu41 commented 5 months ago

V3's ScriptContext is much larger than V2's, so unsafeFromBuiltinData ctx_ does much more with V3 than with V2.

Since you are only using two fields of ScriptContext: txInfoInputs and txInfoMint, you can avoid unsafeFromBuiltinData ctx_, which traverse and processes the entire ScriptContext, by directly extracting these two fields from BuiltinData. We are working on making it easier to do but in the meantime, you can use builtins that operate on BuiltinData to extract the fields, e.g., given scriptContext :: BuiltinData:

import PlutusTx.Builtins.Internal qualified as BI
ds :: BuiltinList BuiltinData
ds = BI.snd (BI.unsafeDataAsConstr scriptContext)
txInfo, scriptPurpose :: BuiltinData
txInfo = BI.head ds
scriptPurpose = BI.head (BI.tail ds)

If you do this for both V2 and V3 scripts I bet their sizes would be much more similar if not identical

reeshavacharya commented 5 months ago

I tried it as @zliu41 suggested and the script size came out exactly the same.

{-# INLINEABLE mkValidator #-}
mkValidator :: TxOutRef -> () -> [TxInInfo] -> Value -> Bool
mkValidator txIn _ inputsInfo mint = mustMintNFT && hasInput
  where
    inputs = map txInInfoOutRef $ inputsInfo
    mintedValue = flattenValue mint
    mustMintNFT = case mintedValue of
        [(_, _, n)] -> n == 1
        _ -> traceError "Must mint a single NFT"
    hasInput = txIn `elem` inputs

mkWrappedValidator :: TxOutRef -> BuiltinData -> BuiltinData -> ()
mkWrappedValidator input red_ ctx_ = check $ mkValidator input (unsafeFromBuiltinData red_) (unsafeFromBuiltinData txInputs) (unsafeFromBuiltinData txMint)
  where 
    ds :: BuiltinData -> BI.BuiltinList BuiltinData
    ds bd = BI.snd (BI.unsafeDataAsConstr bd)

    context = ds ctx_

    txInfo = BI.head context 

    txInputs = BI.head $ ds txInfo

    txMint = BI.head $ BI.tail $ BI.tail $ BI.tail $ BI.tail $ ds txInfo

Thank You.

effectfully commented 5 months ago

Seems like the issue is resolved, so I'm closing it. Thank you for the report!