morley-1.19.2: src/Morley/Michelson/Runtime.hs
-- 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 :))