-- SPDX-FileCopyrightText: 2021 Oxhead Alpha -- SPDX-License-Identifier: LicenseRef-MIT-OA -- | Executor and typechecker of a contract in Morley language. module Morley.Michelson.Runtime ( -- * High level interface for end user originateContract , runContract , transfer , runCode , runView , RunCodeParameters(..) , runCodeParameters , resolveRunCodeBigMaps , mkBigMapFinder , CommonRunOptions(..) , ContractSpecification (..) , ContractSimpleOriginationData(..) -- * Other helpers , parseContract , parseExpandContract , readAndParseContract , prepareContract -- * Re-exports , ContractState (..) , VotingPowers , mkVotingPowers , mkVotingPowersFromMap , TxData (..) , TxParam (..) -- * For testing , ExecutorOp (..) , ExecutorRes (..) , erGState , erUpdates , erInterpretResults , erRemainingSteps , ExecutorError' (..) , ExecutorErrorPrim (..) , ExecutorError , ExecutorM , runExecutorM , runExecutorMWithDB , executeGlobalOperations , executeGlobalOrigination , executeOrigination , executeTransfer , ExecutorState(..) , esGState , esRemainingSteps , esSourceAddress , esLog , esOperationHash , esPrevCounters , ExecutorLog(..) , SomeInterpretResult(..) , elInterpreterResults , elUpdates ) where import Control.Lens (assign, at, each, ix, makeLenses, to, (.=), (<>=)) import Control.Monad.Except (Except, liftEither, runExcept, throwError) import Data.Coerce (coerce) import Data.Constraint (Dict(..), (\\)) import Data.Default (Default(..)) import Data.HashSet qualified as HS import Data.Semigroup.Generic (GenericSemigroupMonoid(..)) import Data.Singletons (demote) import Data.Text.IO (getContents) import Data.Text.IO.Utf8 qualified as Utf8 (readFile) import Data.Type.Equality (pattern Refl) import Data.Typeable (cast) import Fmt (Buildable(build), blockListF, fmt, indentF, nameF, pretty, unlinesF, (+|), (|+)) import Text.Megaparsec (parse) import Morley.Michelson.Interpret import Morley.Michelson.Macro (ParsedOp, expandContract) import Morley.Michelson.Parser qualified as P import Morley.Michelson.Runtime.Dummy import Morley.Michelson.Runtime.GState import Morley.Michelson.Runtime.RunCode import Morley.Michelson.Runtime.TxData import Morley.Michelson.TypeCheck import Morley.Michelson.TypeCheck.Helpers (checkContractDeprecations, checkSingDeprecations) import Morley.Michelson.Typed (Constrained(..), CreateContract(..), EntrypointCallT, EpName, Operation'(..), SomeContractAndStorage(..), SomeStorage, TransferTokens(..), sing) import Morley.Michelson.Typed qualified as T import Morley.Michelson.Typed.Operation import Morley.Michelson.Untyped (Contract) import Morley.Michelson.Untyped qualified as U import Morley.Tezos.Address import Morley.Tezos.Address.Alias import Morley.Tezos.Address.Kinds import Morley.Tezos.Core (Mutez, Timestamp(..), getCurrentTime, unsafeAddMutez, unsafeSubMutez, zeroMutez) import Morley.Tezos.Crypto (KeyHash) import Morley.Util.Interpolate (itu) import Morley.Util.MismatchError import Morley.Util.Named ---------------------------------------------------------------------------- -- Auxiliary types ---------------------------------------------------------------------------- -- | Operations executed by interpreter. -- In our model one Michelson's operation (@operation@ type in Michelson) -- corresponds to 0 or 1 interpreter operation. -- -- Note: 'Address' is not part of 'TxData', because 'TxData' is -- supposed to be provided by the user, while 'Address' can be -- computed by our code. data ExecutorOp = OriginateOp OriginationOperation -- ^ Originate a contract. | TransferOp TransferOperation -- ^ Transfer tokens to the address. | SetDelegateOp SetDelegateOperation -- ^ Set the delegate of a contract. | EmitOp EmitOperation -- ^ Emit contract event. deriving stock (Show) instance Buildable ExecutorOp where build = \case TransferOp op -> build op OriginateOp op -> build op SetDelegateOp op -> build op EmitOp op -> build op data SomeInterpretResult = forall st. SomeInterpretResult { unSomeInterpretResult :: InterpretResult st } deriving stock instance Show SomeInterpretResult -- | Result of a single execution of interpreter. data ExecutorRes = ExecutorRes { _erGState :: GState -- ^ New 'GState'. , _erUpdates :: [GStateUpdate] -- ^ Updates applied to 'GState'. , _erInterpretResults :: [(Address, SomeInterpretResult)] -- ^ During execution a contract can print logs and in the end it returns -- a pair. All logs and returned values are kept until all called contracts -- are executed. In the end they are printed. , _erRemainingSteps :: RemainingSteps -- ^ Now much gas all remaining executions can consume. } deriving stock Show data ExecutorEnv = ExecutorEnv { _eeNow :: Timestamp , _eeLevel :: Natural , _eeMinBlockTime :: Natural , _eeTcOpts :: TypeCheckOptions , _eeCallChain :: [ExecutorOp] } deriving stock (Generic) data ExecutorState = ExecutorState { _esGState :: GState , _esRemainingSteps :: RemainingSteps , _esSourceAddress :: Maybe L1Address , _esLog :: ExecutorLog , _esOperationHash :: ~OperationHash , _esPrevCounters :: HashSet GlobalCounter } deriving stock (Show, Generic) data ExecutorLog = ExecutorLog { _elUpdates :: [GStateUpdate] , _elInterpreterResults :: [(Address, SomeInterpretResult)] } deriving stock (Show, Generic) deriving (Semigroup, Monoid) via GenericSemigroupMonoid ExecutorLog makeLenses ''ExecutorRes makeLenses ''ExecutorEnv makeLenses ''ExecutorState makeLenses ''ExecutorLog -- | 'ExecutorErrorPrim', enriched by the list of operations that succeeded -- before the error. data ExecutorError' a = ExecutorError { eeCallStack :: [ExecutorOp] , eeError :: ExecutorErrorPrim a } deriving stock (Show, Functor, Foldable, Traversable) -- | Errors that can happen during contract interpreting. -- Type parameter @a@ determines how contracts will be represented -- in these errors, e.g. 'Address'. data ExecutorErrorPrim a = EEUnknownContract a -- ^ The interpreted contract hasn't been originated. | EEInterpreterFailed a (InterpretError Void) -- ^ Interpretation of Michelson contract failed. | EEViewLookupError a ViewLookupError -- ^ Error looking up view while trying to call it. | EEViewArgTcError a TcError -- ^ Error type-checking untyped view argument. | EEUnknownAddressAlias SomeAlias -- ^ The given alias isn't associated with any address -- OR is associated with an address of an unexpected kind -- (e.g. we expected an implicit address and found a contract address, or vice-versa). | EEUnknownL1AddressAlias Text -- ^ The given alias is not associated with any address. | EEAmbiguousAlias Text ImplicitAddress ContractAddress -- ^ The given alias is ambiguous, i.e. it is associated with __both__ an -- implicit address and a contract address. | EEUnknownSender a -- ^ Sender address is unknown. | EEUnknownManager a -- ^ Manager address is unknown. | EENotEnoughFunds a Mutez -- ^ Sender doesn't have enough funds. | EEEmptyImplicitContract a -- ^ Sender is an implicit address with the balance of 0. We mimic -- @octez-client@ in calling it "Empty implicit contract". | EEZeroTransaction a -- ^ Sending 0tz towards an address. | EEFailedToApplyUpdates GStateUpdateError -- ^ Failed to apply updates to GState. | EEIllTypedParameter a TcError -- ^ Contract parameter is ill-typed. | EEDeprecatedType TcError -- ^ Found deprecated types. | EEUnexpectedParameterType a (MismatchError T.T) -- ^ Contract parameter is well-typed, but its type does -- not match the entrypoint's type. | EEUnknownEntrypoint EpName -- ^ Specified entrypoint to run is not found. | EETransactionFromContract a Mutez -- ^ A transaction from an originated contract was attempted as a global operation. | EEWrongParameterType a -- ^ Type of parameter in transfer to an implicit account is not Unit. | EEOperationReplay ExecutorOp -- ^ An attempt to perform the operation duplicated with @DUP@ instruction. | EEGlobalOperationSourceNotImplicit Address -- ^ Attempted to initiate global operation from a non-implicit address. | EEGlobalEmitOp -- ^ Trying to run emit operation as a global operation, which should be impossible. deriving stock (Show, Functor, Foldable, Traversable) instance (Buildable a) => Buildable (ExecutorErrorPrim a) where build = \case EEUnknownAddressAlias (SomeAlias (alias :: Alias kind)) -> [itu|The alias '#{alias}' is not associated to a #{kind} address|] where kind = demote @kind \\ aliasKindSanity alias :: AddressKind EEUnknownL1AddressAlias aliasText -> [itu|The alias '#{aliasText}' is not associated with any address|] EEAmbiguousAlias aliasText implicitAddr contractAddr -> [itu| The alias '#{aliasText}' is assigned to both: * a contract address: #{contractAddr} * and an implicit address: #{implicitAddr} Use '#{contractPrefix}:#{aliasText}' or '#{implicitPrefix}:#{aliasText}' to disambiguate. |] EEUnknownContract addr -> "The contract is not originated " +| addr |+ "" EEInterpreterFailed addr err -> "Michelson interpreter failed for contract " +| addr |+ ": " +| err |+ "" EEViewLookupError addr err -> nameF ("View lookup for contract " +| addr |+ " failed") $ build err EEViewArgTcError addr err -> nameF ("Typechecking view argument for contract " +| addr |+ " failed") $ build err EEUnknownSender addr -> "The sender address is unknown " +| addr |+ "" EEUnknownManager addr -> "The manager address is unknown " +| addr |+ "" EENotEnoughFunds addr amount -> "The sender (" +| addr |+ ") doesn't have enough funds (has only " +| amount |+ ")" EEEmptyImplicitContract addr -> "Empty implicit contract (" +| addr |+ ")" EEZeroTransaction addr -> "Transaction of 0ꜩ towards a key address " +| addr |+ " which has no code is prohibited" EEFailedToApplyUpdates err -> "Failed to update GState: " +| err |+ "" EEIllTypedParameter _ err -> "The contract parameter is ill-typed: " +| err |+ "" EEDeprecatedType err -> nameF "Deprecation error" $ build err EEUnexpectedParameterType _ merr -> "The contract parameter is well-typed, but did not match the contract's entrypoint's type.\n" +| merr |+ "" EEUnknownEntrypoint epName -> "The contract does not contain entrypoint '" +| epName |+ "'" EETransactionFromContract addr amount -> "Global transaction of funds (" +| amount |+ ") from an originated contract (" +| addr |+ ") is prohibited." EEWrongParameterType addr -> "Bad contract parameter for: " +| addr |+ "" EEOperationReplay op -> "Operation replay attempt:\n" +| indentF 2 (build op) |+ "" EEGlobalOperationSourceNotImplicit addr -> "Attempted to initiate global operation from a non-implicit address " +| addr |+ "" EEGlobalEmitOp -> "Attempted to run emit event as a global operation, this should be impossible." -- | To reduce friction between 'ExecutorError'' and 'ExecutorErrorPrim', this -- instance will try to run 'fromException' for both. instance (Typeable a, Show a, Buildable a) => Exception (ExecutorErrorPrim a) where displayException = pretty fromException (SomeException exc) = cast exc <|> fmap eeError (cast exc) instance (Buildable a) => Buildable (ExecutorError' a) where build ExecutorError{..} = unlinesF [ build eeError , nameF "While running" $ unlinesF $ build <$> eeCallStack ] type ExecutorError = ExecutorError' Address instance (Typeable a, Show a, Buildable a) => Exception (ExecutorError' a) where displayException = pretty ---------------------------------------------------------------------------- -- Interface ---------------------------------------------------------------------------- -- | Parse a contract from 'Text'. parseContract :: P.MichelsonSource -> Text -> Either P.ParserException (U.Contract' ParsedOp) parseContract source = first P.ParserException . parse P.program (pretty source) -- | Parse a contract from 'Text' and expand macros. parseExpandContract :: P.MichelsonSource -> Text -> Either P.ParserException Contract parseExpandContract = fmap expandContract ... parseContract -- | Read and parse a contract from give path or `stdin` (if the -- argument is 'Nothing'). The contract is not expanded. readAndParseContract :: Maybe FilePath -> IO (U.Contract' ParsedOp) readAndParseContract mFilename = do code <- readCode mFilename either throwM pure $ parseContract (toSrc mFilename) code where readCode :: Maybe FilePath -> IO Text readCode = maybe getContents Utf8.readFile toSrc :: Maybe FilePath -> P.MichelsonSource toSrc = maybe P.MSUnspecified P.MSFile -- | Read a contract using 'readAndParseContract', expand and -- flatten. The contract is not type checked. prepareContract :: Maybe FilePath -> IO Contract prepareContract mFile = expandContract <$> readAndParseContract mFile -- | Originate a contract. Returns the address of the originated -- contract. originateContract :: "dbPath" :! FilePath -> "tcOpts" :? TypeCheckOptions -> "originator" :? ImplicitAddress -> "alias" :? ContractAlias -> "delegate" :? KeyHash -> "csod" :! ContractSimpleOriginationData U.Contract -> "verbose" :? Bool -> IO ContractAddress originateContract (arg #dbPath -> croDBPath) (argDef #tcOpts def -> croTCOpts) originator alias (argF #delegate -> mbDelegate) (arg #csod -> csod) (argDef #verbose False -> croVerbose) = do origination <- either throwM pure $ mkOrigination croTCOpts csod originator alias ! #delegate mbDelegate let croDryRun = False fmap snd $ runExecutorMWithDB def{croDBPath, croDryRun, croVerbose, croTCOpts} $ executeGlobalOrigination origination -- | Run a contract. The contract is originated first (if it's not -- already) and then we pretend that we send a transaction to it. runContract :: CommonRunOptions -> ContractSimpleOriginationData U.Contract -> TxData -> IO SomeStorage runContract cro@CommonRunOptions{..} csOrig txData = do origination <- either throwM pure $ mkOrigination croTCOpts csOrig ! def (_, newSt) <- runExecutorMWithDB cro do -- Here we are safe to bypass executeGlobalOperations for origination, -- since origination can't generate more operations. addr <- executeGlobalOrigination origination let transferOp = TransferOp $ TransferOperation (MkAddress addr) txData 1 void $ executeGlobalOperations [transferOp] getContractStorage addr return newSt where getContractStorage :: ContractAddress -> ExecutorM SomeStorage getContractStorage addr = do addrs <- use (esGState . gsContractAddressesL) case addrs ^. at addr of Nothing -> error $ pretty addr <> " is unknown" Just ContractState{..} -> return $ SomeStorage csStorage data ContractSpecification a = ContractSpecAddressOrAlias ContractAddressOrAlias | ContractSpecOrigination a deriving stock (Functor, Foldable, Traversable) data ContractSimpleOriginationData a = ContractSimpleOriginationData { csodContract :: a , csodStorage :: U.Value , csodBalance :: Mutez } deriving stock (Functor, Foldable, Traversable) data CommonRunOptions = CommonRunOptions { croNow :: Maybe Timestamp , croLevel :: Natural , croMinBlockTime :: Natural , croMaxSteps :: RemainingSteps , croDBPath :: FilePath , croTCOpts :: TypeCheckOptions , croVerbose :: Bool , croDryRun :: Bool } instance Default CommonRunOptions where def = CommonRunOptions { croNow = Nothing , croLevel = 0 , croMinBlockTime = dummyMinBlockTime , croMaxSteps = dummyMaxSteps , croDBPath = "db.json" , croTCOpts = def , croVerbose = False , croDryRun = True } -- | Run a contract view. The contract is originated first (if it's not already) -- and then we pretend that we send a transaction to it. runView :: CommonRunOptions -> ContractSpecification (ContractSimpleOriginationData U.Contract) -> U.ViewName -> SomeAddressOrAlias -> TxParam -> IO T.SomeValue runView cro@CommonRunOptions{..} contractOrAddr viewName sender' viewArg = do origination <- traverse (either throwM pure . (mkOrigination croTCOpts ! def)) contractOrAddr (_, newSt) <- runExecutorMWithDB cro do addr <- case origination of ContractSpecAddressOrAlias addr -> resolveContractAddress addr ContractSpecOrigination origOp -> executeGlobalOrigination origOp -- Here we are safe to bypass executeGlobalOperations for origination, -- since origination can't generate more operations. sender <- resolveAddress sender' callView sender addr viewName viewArg return newSt mkOrigination :: TypeCheckOptions -> ContractSimpleOriginationData U.Contract -> "originator" :? ImplicitAddress -> "alias" :? ContractAlias -> "delegate" :? Maybe KeyHash -> Either TcError OriginationOperation mkOrigination tcOpts ContractSimpleOriginationData{..} (argDef #originator genesisAddress -> ooOriginator) (argF #alias -> ooAlias) (argDef #delegate (Just dummyDelegate) -> ooDelegate) = do SomeContractAndStorage ooContract ooStorage <- typeCheckingWith tcOpts $ typeCheckContractAndStorage csodContract csodStorage pure OriginationOperation { ooBalance = csodBalance , ooCounter = 0 , .. } -- | We hardcode some random key hash here as delegate to make sure that: -- -- 1. Contract's address won't clash with already originated one (because it may -- have different storage value which may be confusing). -- -- 2. If one uses this functionality twice with the same contract and other -- data, the contract will have the same address. dummyDelegate :: KeyHash dummyDelegate = let ImplicitAddress kh = [ta|tz1YCABRTa6H8PLKx2EtDWeCGPaKxUhNgv47|] in kh -- | Construct 'BigMapFinder' using the current executor context. mkBigMapFinder :: ExecutorM BigMapFinder mkBigMapFinder = do pureState <- get pure \bigMapId -> pureState ^? esGState . gsContractAddressesL . each . to getContractStorage . to (getBigMapsWithId bigMapId) . each where getContractStorage :: ContractState -> T.SomeValue getContractStorage (ContractState _ _ storage _) = T.SomeValue storage getBigMapsWithId :: Natural -> T.SomeValue -> [T.SomeVBigMap] getBigMapsWithId bigMapId (T.SomeValue val) = T.dfsFoldMapValue (\v -> case v of T.VBigMap (Just bigMapId') _ | bigMapId' == bigMapId -> [T.SomeVBigMap v] _ -> [] ) val -- | Send a transaction to given address with given parameters. transfer :: CommonRunOptions -> SomeAddressOrAlias -> TxData -> IO () transfer cro destination txData = do -- TODO [#905]: simplify with convertAddress void $ runExecutorMWithDB @[EmitOperation] cro $ do Constrained destAddr <- resolveAddress destination executeGlobalOperations [TransferOp $ TransferOperation (MkAddress destAddr) txData 0] ---------------------------------------------------------------------------- -- Executor ---------------------------------------------------------------------------- -- | A monad in which contract executor runs. type ExecutorM = ReaderT ExecutorEnv (StateT ExecutorState (Except ExecutorError) ) -- | Run some executor action, returning its result and final executor state in 'ExecutorRes'. -- -- The action has access to the hash of currently executed global operation, in order to construct -- addresses of originated contracts. It is expected that the action uses @#isGlobalOp :! True@ -- to specify this hash. Otherwise it is initialized with 'error'. runExecutorM :: Timestamp -> Natural -> Natural -> RemainingSteps -> TypeCheckOptions -> GState -> ExecutorM a -> Either ExecutorError (ExecutorRes, a) runExecutorM now level minBlockTime remainingSteps tcOpts gState action = fmap preResToRes $ runExcept $ runStateT (runReaderT action $ ExecutorEnv now level minBlockTime tcOpts mempty) initialState where initialOpHash = error "Initial OperationHash touched" initialState = ExecutorState { _esGState = gState , _esRemainingSteps = remainingSteps , _esSourceAddress = Nothing , _esLog = mempty , _esOperationHash = initialOpHash , _esPrevCounters = mempty } preResToRes :: (a, ExecutorState) -> (ExecutorRes, a) preResToRes (r, ExecutorState{..}) = ( ExecutorRes { _erGState = _esGState , _erUpdates = _esLog ^. elUpdates , _erInterpretResults = _esLog ^. elInterpreterResults , _erRemainingSteps = _esRemainingSteps } , r ) -- | Run some executor action, reading state from the DB on disk. -- -- If 'croDryRun' is @False@, the final state is written back to the disk. -- -- If the executor fails with t'ExecutorError' it will be thrown as an exception. runExecutorMWithDB :: CommonRunOptions -> ExecutorM a -> IO (ExecutorRes, a) runExecutorMWithDB (CommonRunOptions mNow level minBlockTime steps dbPath tcOpts verbose dryRun) action = do gState <- readGState dbPath now <- maybe getCurrentTime pure mNow (res@ExecutorRes{..}, a) <- either throwM pure $ runExecutorM now level minBlockTime steps tcOpts gState action unless dryRun $ writeGState dbPath _erGState mapM_ printInterpretResult _erInterpretResults when (verbose && not (null _erUpdates)) $ do putTextLn $ fmt $ nameF "Updates" (blockListF _erUpdates) putTextLn $ "Remaining gas: " <> pretty _erRemainingSteps <> "." return (res, a) where printInterpretResult :: (Address, SomeInterpretResult) -> IO () printInterpretResult (addr, SomeInterpretResult ResultStateLogs{..}) = do putTextLn $ "Executed contract " <> pretty addr () <- case rslResult of T.VPair (ops@T.VList{}, res) | _ :: T.Value ('T.TList ops) <- ops , T.STOperation <- T.sing @ops \\ T.valueTypeSanity ops -> do putTextLn $ case T.fromVal @[T.Operation] ops of [] -> "It didn't return any operations." xs -> fmt $ nameF "It returned operations" (blockListF xs) putTextLn $ "It returned: " <> pretty res <> "." _ -> putTextLn $ "It returned: " <> pretty rslResult <> "." let MorleyLogs logs = rslLogs unless (null logs) $ do putTextLn "And produced logs:" mapM_ putTextLn logs putTextLn "" -- extra break line to separate logs from two sequence contracts -- | Resolves 'SomeAddressOrAlias' type to an address. resolveAddress :: SomeAddressOrAlias -> ExecutorM L1Address resolveAddress = \case SAOAKindUnspecified aliasText -> do implicitAddrMb <- preuse $ esGState . gsImplicitAddressAliasesL . ix (ImplicitAlias aliasText) contractAddrMb <- preuse $ esGState . gsContractAddressAliasesL . ix (ContractAlias aliasText) case (implicitAddrMb, contractAddrMb) of (Nothing, Nothing) -> throwEE $ EEUnknownL1AddressAlias aliasText (Just implicitAddr, Nothing) -> pure $ Constrained implicitAddr (Nothing, Just contractAddr) -> pure $ Constrained contractAddr (Just implicitAddr, Just contractAddr) -> throwEE $ EEAmbiguousAlias aliasText implicitAddr contractAddr SAOAKindSpecified (AddressResolved (addr@ContractAddress{})) -> pure $ Constrained addr SAOAKindSpecified (AddressResolved (addr@ImplicitAddress{})) -> pure $ Constrained addr SAOAKindSpecified (AddressAlias alias) -> do addrMb <- preuse $ case alias of ImplicitAlias{} -> esGState . gsImplicitAddressAliasesL . ix alias . to Constrained ContractAlias{} -> esGState . gsContractAddressAliasesL . ix alias . to Constrained case addrMb of Just addr -> pure addr Nothing -> throwEE $ EEUnknownAddressAlias $ SomeAlias alias -- | Resolves 'ContractAddressOrAlias' type to an address. resolveContractAddress :: ContractAddressOrAlias -- TODO [#905] or [#889]: Change the return type to `L1Address` -> ExecutorM ContractAddress resolveContractAddress ct = case ct of AddressResolved r -> pure r AddressAlias alias -> resolveAddress (SAOAKindSpecified ct) >>= \case Constrained result -> case result of ContractAddress{} -> pure result ImplicitAddress{} -> throwEE $ EEUnknownAddressAlias (SomeAlias alias) -- | Execute a list of global operations, returning a list of generated events. executeGlobalOperations :: [ExecutorOp] -> ExecutorM [EmitOperation] executeGlobalOperations = concatMapM $ \op -> executeMany (#isGlobalOp :! True) [op] where -- Execute a list of operations and additional operations they return, until there are none. executeMany :: "isGlobalOp" :! Bool -> [ExecutorOp] -> ExecutorM [EmitOperation] executeMany isGlobalOp = \case [] -> pure [] (op:opsTail) -> addStackEntry op do case op of OriginateOp origination -> do void $ executeOrigination isGlobalOp origination executeMany (#isGlobalOp :! False) opsTail SetDelegateOp operation -> do executeDelegation isGlobalOp operation executeMany (#isGlobalOp :! False) opsTail TransferOp transferOperation -> do moreOps <- executeTransfer isGlobalOp transferOperation executeMany (#isGlobalOp :! False) $ moreOps <> opsTail EmitOp emitOperation -> do liftM2 (:) (executeEmit isGlobalOp emitOperation) $ executeMany (#isGlobalOp :! False) opsTail -- | Execute a global origination operation. executeGlobalOrigination :: OriginationOperation -> ExecutorM ContractAddress executeGlobalOrigination = executeOrigination ! #isGlobalOp True -- | Execute an origination operation. executeOrigination :: "isGlobalOp" :! Bool -> OriginationOperation -> ExecutorM ContractAddress executeOrigination (arg #isGlobalOp -> isGlobalOp) origination@(OriginationOperation{..}) = do when isGlobalOp $ do beginGlobalOperation assign esOperationHash $ mkOriginationOperationHash origination checkOperationReplay $ OriginateOp origination tcOpts <- view eeTcOpts when (tcStrict tcOpts) $ liftEE $ first EEDeprecatedType $ checkContractDeprecations ooContract opHash <- use esOperationHash gs <- use esGState -- Add big_map IDS to storage let bigMapCounter0 = gs ^. gsBigMapCounterL let (storageWithIds, bigMapCounter1) = runState (assignBigMapIds False ooStorage) bigMapCounter0 let contractState = ContractState ooBalance ooContract storageWithIds ooDelegate let originatorAddress = ooOriginator originatorBalance <- case lookupBalance originatorAddress gs of Nothing -> throwEE $ EEUnknownManager $ MkAddress ooOriginator Just oldBalance | oldBalance < ooBalance -> throwEE $ EENotEnoughFunds (MkAddress ooOriginator) oldBalance | otherwise -> -- Subtraction is safe because we have checked its -- precondition in guard. return $ oldBalance `unsafeSubMutez` ooBalance let address = mkContractAddress opHash ooCounter updates = catMaybes [ liftA2 GSAddContractAddressAlias ooAlias (Just address) , Just $ GSAddContractAddress address contractState , Just $ GSSetBalance originatorAddress originatorBalance , Just GSIncrementCounter , if bigMapCounter0 == bigMapCounter1 then Nothing else Just $ GSSetBigMapCounter bigMapCounter1 ] case applyUpdates updates gs of Left err -> throwEE $ EEFailedToApplyUpdates err Right newGS -> do esGState .= newGS esLog <>= ExecutorLog updates [] return address -- | Execute delegation operation. executeDelegation :: "isGlobalOp" :! Bool -> SetDelegateOperation -> ExecutorM () executeDelegation (arg #isGlobalOp -> isGlobalOp) delegation@SetDelegateOperation{..} = do when isGlobalOp $ do beginGlobalOperation assign esOperationHash $ mkDelegationOperationHash delegation checkOperationReplay $ SetDelegateOp delegation gs <- use esGState Constrained address <- pure sdoContract let updates = [GSSetDelegate address sdoDelegate] case applyUpdates updates gs of Left err -> throwEE $ EEFailedToApplyUpdates err Right newGS -> do esGState .= newGS esLog <>= ExecutorLog updates [] return () -- | Execute delegation operation. executeEmit :: "isGlobalOp" :! Bool -> EmitOperation -> ExecutorM EmitOperation executeEmit (arg #isGlobalOp -> isGlobalOp) op = do when isGlobalOp $ throwEE EEGlobalEmitOp checkOperationReplay $ EmitOp op pure op mkContractEnv :: ("balance" :! Mutez) -> ("self" :! ContractAddress) -> ("sender" :! L1Address) -> ("amount" :! Mutez) -> ("useOpHash" :! Bool) -> ExecutorM ContractEnv mkContractEnv (arg #balance -> ceBalance) (arg #self -> ceSelf) (arg #sender -> ceSender) (arg #amount -> ceAmount) (arg #useOpHash -> useOpHash) = do ceNow <- view eeNow ceLevel <- view eeLevel ceMinBlockTime <- view eeMinBlockTime ceOperationHash <- if useOpHash then Just <$> use esOperationHash else pure Nothing GState { gsChainId = ceChainId , gsContractAddresses=ceContractsMap , gsVotingPowers = ceVotingPowers } <- use esGState ceMaxSteps <- use esRemainingSteps ceSource <- fromMaybe ceSender <$> use esSourceAddress pure ContractEnv { ceErrorSrcPos = def , ceMetaWrapper = id , ceContracts = \addr -> pure $ ceContractsMap ^. at addr , .. } -- | Typeckeck if necessary and assign big map ids to a parameter. prepareParameter :: forall arg. T.SingI arg => ContractAddress -> TxParam -> "typedParamError" :! (Address -> MismatchError T.T -> ExecutorErrorPrim Address) -> "untypedParamError" :! (Address -> TcError -> ExecutorErrorPrim Address) -> ExecutorM (T.Value arg, BigMapCounter) prepareParameter addr tdParameter (arg #typedParamError -> tyParErr) (arg #untypedParamError -> unTyParErr) = do tcOpts <- view eeTcOpts gs <- use esGState let existingContracts = extractAllContracts gs when (tcStrict tcOpts) $ liftEE $ first EEDeprecatedType $ checkSingDeprecations (sing @arg) -- If the parameter has already been typechecked, simply check if -- its type matches the contract's entrypoint's type. -- Otherwise (e.g. if it was parsed from stdin via the CLI), -- we need to typecheck the parameter. typedParameter <- case tdParameter of TxTypedParam (typedVal :: T.Value t) -> T.castM @t @arg typedVal $ throwEE . tyParErr (MkAddress addr) TxUntypedParam untypedVal -> liftEE $ first (unTyParErr $ MkAddress addr) $ typeCheckingWith tcOpts $ typeVerifyParameter @arg existingContracts untypedVal pure $ runState (assignBigMapIds False typedParameter) $ gs ^. gsBigMapCounterL -- | Execute a transfer operation. -- -- Note: we're handling both XTZ and ticket transfers here to avoid code -- duplication. We assume that if an implicit account sends tickets via -- 'TxTypedParam', it should be interpreted as @transfer_ticket@ manager -- operation, and not a regular transfer. -- -- Note that this only works for 'TxTypedParam', as for ticket transfers between -- implicit accounts we can't know the exact type of the ticket to transfer if -- the value is untyped. executeTransfer :: "isGlobalOp" :! Bool -> TransferOperation -> ExecutorM [ExecutorOp] executeTransfer (arg #isGlobalOp -> isGlobalOp) transferOperation | TransferOperation addr' txData _ <- transferOperation , MkAddress (addr :: KindedAddress kind) <- addr' , TxData{tdSenderAddress=Constrained senderAddr,..} <- txData = do when isGlobalOp $ beginGlobalOperation gs <- use esGState remainingSteps <- use esRemainingSteps sourceAddr <- fromMaybe (tdSenderAddress txData) <$> use esSourceAddress let globalCounter = gsCounter gs let addresses :: Map (KindedAddress kind) (AddressStateFam kind) addresses = gs ^. addressesL addr let isZeroTransfer = tdAmount == zeroMutez let senderBalance = lookupBalance senderAddr gs checkOperationReplay $ TransferOp transferOperation -- Implicit addresses can't be senders with a balance of 0tz even when the transfer amount -- is zero. case isImplicitAddress senderAddr of Nothing -> do when (isGlobalOp && not isZeroTransfer) $ throwEE $ EETransactionFromContract (MkAddress senderAddr) tdAmount Just Refl -> do case senderBalance of Nothing -> throwEE $ EEEmptyImplicitContract $ MkAddress senderAddr Just balance | balance == zeroMutez -> throwEE $ EEEmptyImplicitContract $ MkAddress senderAddr _ -> pass case isImplicitAddress addr of Nothing -> pass Just Refl -> do when (badParamToImplicitAccount tdParameter) $ throwEE $ EEWrongParameterType $ MkAddress addr -- Transferring 0 XTZ to a key address is prohibited. when (isZeroTransfer && isUnitParam tdParameter) $ throwEE $ EEZeroTransaction $ MkAddress addr mDecreaseSenderBalance <- case senderBalance of _ | isZeroTransfer -> pure Nothing Nothing -> throwEE $ EEUnknownSender $ MkAddress senderAddr Just balance | balance < tdAmount -> throwEE $ EENotEnoughFunds (MkAddress senderAddr) balance | otherwise -> do -- Subtraction is safe because we have checked its -- precondition in guard. let newBal = balance `unsafeSubMutez` tdAmount pure $ Just $ GSSetBalance senderAddr newBal let mDecreaseSenderTickets :: Maybe GStateUpdate | Just Refl <- isImplicitAddress senderAddr -- if an implicit account sends tickets, it can't forge them, so it -- must own them. = uncurry (GSRemoveTickets senderAddr) <$> sentTickets | otherwise = Nothing sentTickets :: Maybe (TicketKey, Natural) | TxTypedParam v@T.VTicket{} <- tdParameter = Just $ toTicketKey v | otherwise = Nothing let commonFinishup :: Dict (L1AddressKind kind) -- NB: this is a Dict and not a constraint because GHC desugars these -- let-bindings such that it expects this constraint at the definition -- site. -> [GStateUpdate] -> [T.Operation] -> Maybe SomeInterpretResult -> RemainingSteps -> ExecutorM [ExecutorOp] commonFinishup Dict otherUpdates sideEffects maybeInterpretRes newRemSteps = do let -- According to the reference implementation, counter is incremented for transfers as well. updates = catMaybes [mDecreaseSenderBalance, mDecreaseSenderTickets] <> otherUpdates <> [GSIncrementCounter] newGState <- liftEE $ first EEFailedToApplyUpdates $ applyUpdates updates gs esGState .= newGState esRemainingSteps .= newRemSteps esSourceAddress .= Just sourceAddr esLog <>= ExecutorLog updates ( maybe mempty (one . (MkAddress addr,)) maybeInterpretRes ) mapM (convertOp addr) $ sideEffects onlyUpdates :: Dict (L1AddressKind kind) -> [GStateUpdate] -> ExecutorM [ExecutorOp] onlyUpdates dict updates = commonFinishup dict updates [] Nothing remainingSteps case addr of SmartRollupAddress{} -> throwEE $ EEUnknownContract (MkAddress addr) ImplicitAddress{} -> case addresses ^. at addr of Nothing -> onlyUpdates Dict . one $ GSAddImplicitAddress addr tdAmount $ maybeToList sentTickets Just ImplicitState{..} -> do let -- Calculate the account's new balance. -- -- Note: `unsafeAddMutez` can't overflow if global state is correct -- (because we can't create money out of nowhere) newBalance = isBalance `unsafeAddMutez` tdAmount updBalance | tdAmount == zeroMutez = Nothing | otherwise = Just $ GSSetBalance addr newBalance updTickets = uncurry (GSAddTickets addr) <$> sentTickets onlyUpdates Dict $ catMaybes [updBalance, updTickets] ContractAddress{} -> case addresses ^. at addr of Nothing -> throwEE $ EEUnknownContract (MkAddress addr) Just ContractState{..} -> do let -- Calculate the contract's new balance. -- -- Note: `unsafeAddMutez` can't overflow if global state is -- correct (because we can't create money out of nowhere) newBalance = csBalance `unsafeAddMutez` tdAmount epName = tdEntrypoint T.MkEntrypointCallRes _ (epc :: EntrypointCallT cp epArg) <- T.mkEntrypointCall epName (T.cParamNotes csContract) & maybe (throwEE $ EEUnknownEntrypoint epName) pure (typedParameterWithIds, bigMapCounter1) <- prepareParameter addr tdParameter ! #typedParamError EEUnexpectedParameterType ! #untypedParamError EEIllTypedParameter -- I'm not entirely sure why we need to pattern match on `()` here, -- but, if we don't, we get a compiler error that I suspect is somehow related -- to the existential types we're matching on a few lines above. -- -- • Couldn't match type ‘a0’ -- with ‘(InterpretResult, RemainingSteps, [Operation], [GStateUpdate])’ -- ‘a0’ is untouchable inside the constraints: StorageScope st1 () <- when isGlobalOp $ esOperationHash .= case isImplicitAddress senderAddr of Just Refl | Just (tKey, tAmount) <- sentTickets -- transfer_ticket is only used when sender is implicit address, -- contracts use regular transfer to send tickets. -> mkTransferTicketOperationHash tKey tAmount (MkAddress addr) tdEntrypoint _ -> mkTransferOperationHash addr typedParameterWithIds tdEntrypoint tdAmount contractEnv <- mkContractEnv ! #balance newBalance ! #self addr ! #sender (Constrained senderAddr) ! #amount tdAmount ! #useOpHash True iur@(ResultStateLogs { rslResult = extractValOps -> (sideEffects, newValue) , rslState = InterpreterState newRemainingSteps globalCounter2 bigMapCounter2 }) <- liftEE $ first (EEInterpreterFailed (MkAddress addr)) $ handleReturn $ interpret csContract epc typedParameterWithIds csStorage (gsCounter gs) bigMapCounter1 contractEnv let updBalance | newBalance == csBalance = Nothing | otherwise = Just $ GSSetBalance addr newBalance updStorage | SomeValue newValue == SomeValue csStorage = Nothing | otherwise = Just $ GSSetStorageValue addr newValue updBigMapCounter | gs ^. gsBigMapCounterL == bigMapCounter2 = Nothing | otherwise = Just $ GSSetBigMapCounter bigMapCounter2 updGlobalCounter | globalCounter == globalCounter2 = Nothing | otherwise = Just $ GSUpdateCounter globalCounter2 updates = catMaybes [ updBalance , updStorage , updBigMapCounter , updGlobalCounter ] commonFinishup Dict updates sideEffects (Just $ SomeInterpretResult iur) newRemainingSteps -- | Execute a view. callView :: L1Address -> ContractAddress -> U.ViewName -> TxParam -> ExecutorM T.SomeValue callView sender addr viewName viewArg = do ContractState{..} <- use (esGState . gsContractAddressesL . at addr) >>= maybe (throwEE $ EEUnknownContract (MkAddress addr)) pure T.SomeView (view'@T.View{} :: T.View viewArg st viewRet) <- liftEE $ first (EEViewLookupError (Constrained addr)) $ getViewByName csContract viewName (typedParameterWithIds, bigMapCounter1) <- prepareParameter addr viewArg ! #typedParamError (\a -> EEViewLookupError a . ViewArgMismatch) ! #untypedParamError EEViewArgTcError contractEnv <- mkContractEnv ! #balance csBalance ! #self addr ! #sender sender ! #amount zeroMutez ! #useOpHash False counter <- use $ esGState . gsCounterL remainingSteps <- use esRemainingSteps iur@ResultStateLogs{..} <- liftEE $ first (EEInterpreterFailed (MkAddress addr)) $ handleReturn $ interpretView view' csStorage typedParameterWithIds contractEnv (InterpreterState remainingSteps counter bigMapCounter1) esLog <>= ExecutorLog [] (one . (MkAddress addr, ) $ SomeInterpretResult iur) pure . SomeValue $ rslResult ---------------------------------------------------------------------------- -- Simple helpers ---------------------------------------------------------------------------- checkOperationReplay :: ExecutorOp -> ExecutorM () checkOperationReplay op = do let opCounter = op & \case OriginateOp OriginationOperation{..} -> ooCounter TransferOp TransferOperation{..} -> toCounter SetDelegateOp SetDelegateOperation{..} -> sdoCounter EmitOp (EmitOperation _ T.Emit{..}) -> emCounter prevCounters <- use esPrevCounters when (opCounter `HS.member` prevCounters) $ throwEE $ EEOperationReplay op esPrevCounters <>= one opCounter -- The argument is the address of the contract that generated this operation. convertOp :: L1AddressKind kind => KindedAddress kind -> T.Operation -> ExecutorM ExecutorOp convertOp interpretedAddr = \case OpTransferTokens tt -> pure $ case ttContract tt of T.VContract destAddress sepc -> let txData = TxData { tdSenderAddress = Constrained interpretedAddr , tdEntrypoint = T.sepcName sepc , tdParameter = TxTypedParam (ttTransferArgument tt) , tdAmount = ttAmount tt } transferOperation = TransferOperation { toDestination = destAddress , toTxData = txData , toCounter = ttCounter tt } in TransferOp transferOperation OpSetDelegate T.SetDelegate{..} -> pure $ SetDelegateOp SetDelegateOperation { sdoContract = Constrained interpretedAddr , sdoDelegate = sdMbKeyHash , sdoCounter = sdCounter } OpCreateContract CreateContract{ccOriginator=Constrained ccOriginator, ..} -> pure $ OriginateOp OriginationOperation { ooOriginator = ccOriginator , ooDelegate = ccDelegate , ooBalance = ccBalance , ooStorage = ccStorageVal , ooContract = ccContract , ooCounter = ccCounter , ooAlias = Nothing } OpEmit emit -> case interpretedAddr of ContractAddress{} -> pure $ EmitOp $ EmitOperation interpretedAddr emit _ -> throwEE $ EEUnknownContract $ MkAddress interpretedAddr -- | Reset source address before executing a global operation. beginGlobalOperation :: ExecutorM () beginGlobalOperation = esSourceAddress .= Nothing -- | Return True if the param is not Unit or ticket. badParamToImplicitAccount :: TxParam -> Bool badParamToImplicitAccount (TxTypedParam T.VTicket{}) = False badParamToImplicitAccount param = not $ isUnitParam param -- | Return True if parameter is @Unit@. isUnitParam :: TxParam -> Bool isUnitParam (TxTypedParam T.VUnit) = True isUnitParam (TxUntypedParam U.ValueUnit) = True isUnitParam _ = False getContractStack :: ExecutorM [ExecutorOp] getContractStack = reverse <$> view eeCallChain throwEE :: ExecutorErrorPrim Address -> ExecutorM r throwEE err = throwError . flip ExecutorError err =<< getContractStack liftEE :: Either (ExecutorErrorPrim Address) r -> ExecutorM r liftEE x = do stack <- getContractStack liftEither . first (ExecutorError stack) $ x addStackEntry :: ExecutorOp -> ExecutorM a -> ExecutorM a addStackEntry entry = local (eeCallChain %~ coerce (entry :))