morley-1.17.0: 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
-- * Other helpers
, parseContract
, parseExpandContract
, readAndParseContract
, prepareContract
-- * Re-exports
, ContractState (..)
, AddressState (..)
, VotingPowers
, mkVotingPowers
, mkVotingPowersFromMap
, TxData (..)
, TxParam (..)
-- * For testing
, ExecutorOp (..)
, ExecutorRes (..)
, erGState
, erUpdates
, erInterpretResults
, erRemainingSteps
, ExecutorError' (..)
, ExecutorError
, ExecutorM
, runExecutorM
, runExecutorMWithDB
, executeGlobalOperations
, executeGlobalOrigination
, executeOrigination
, executeTransfer
, ExecutorState(..)
, esGState
, esRemainingSteps
, esSourceAddress
, esLog
, esOperationHash
, esPrevCounters
, ExecutorLog(..)
, elInterpreterResults
, elUpdates
) where
import Control.Lens (assign, at, makeLenses, (.=), (<>=))
import Control.Monad.Except (Except, liftEither, runExcept, throwError)
import Data.Default (def)
import Data.HashSet qualified as HS
import Data.Semigroup.Generic (GenericSemigroupMonoid(..))
import Data.Text.IO (getContents)
import Data.Text.IO.Utf8 qualified as Utf8 (readFile)
import Fmt (Buildable(build), blockListF, fmt, fmtLn, indentF, nameF, pretty, (+|), (|+))
import Text.Megaparsec (parse)
import Morley.Michelson.Interpret
(ContractEnv(..), InterpretError(..), InterpretResult(..), InterpreterState(..), MorleyLogs(..),
RemainingSteps(..), assignBigMapIds, handleContractReturn, 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.TxData
import Morley.Michelson.TypeCheck
import Morley.Michelson.Typed
(CreateContract(..), EntrypointCallT, EpName, Operation'(..), SomeConstrainedValue(..),
SomeContractAndStorage(..), SomeStorage, TransferTokens(..), untypeValue)
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 (Address(..), GlobalCounter(..), isKeyAddress)
import Morley.Tezos.Address.Alias (AddressOrAlias(..), Alias)
import Morley.Tezos.Core
(Mutez, Timestamp(..), getCurrentTime, unsafeAddMutez, unsafeSubMutez, zeroMutez)
import Morley.Tezos.Crypto (KeyHash, parseHash)
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.
deriving stock (Show)
instance Buildable ExecutorOp where
build = \case
TransferOp (TransferOperation addr TxData{..} _)->
"Transfer " +| tdAmount |+ " tokens from " +| tdSenderAddress |+ " to " +| addr |+ ""
OriginateOp OriginationOperation{..} ->
"Originate a contract with" <>
" delegate " +| maybe "<nobody>" build ooDelegate |+
" and balance = " +| ooBalance |+ ""
SetDelegateOp SetDelegateOperation{..} ->
"Set delegate of contract " +| sdoContract |+
" to " +| maybe "<nobody>" build sdoDelegate |+ ""
-- | Result of a single execution of interpreter.
data ExecutorRes = ExecutorRes
{ _erGState :: GState
-- ^ New 'GState'.
, _erUpdates :: [GStateUpdate]
-- ^ Updates applied to 'GState'.
, _erInterpretResults :: [(Address, InterpretResult)]
-- ^ 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
}
deriving stock (Show, Generic)
data ExecutorState = ExecutorState
{ _esGState :: GState
, _esRemainingSteps :: RemainingSteps
, _esSourceAddress :: Maybe Address
, _esLog :: ExecutorLog
, _esOperationHash :: ~OperationHash
, _esPrevCounters :: HashSet GlobalCounter
}
deriving stock (Show, Generic)
data ExecutorLog = ExecutorLog
{ _elUpdates :: [GStateUpdate]
, _elInterpreterResults :: [(Address, InterpretResult)]
}
deriving stock (Show, Generic)
deriving (Semigroup, Monoid) via GenericSemigroupMonoid ExecutorLog
makeLenses ''ExecutorRes
makeLenses ''ExecutorEnv
makeLenses ''ExecutorState
makeLenses ''ExecutorLog
-- | Errors that can happen during contract interpreting.
-- Type parameter @a@ determines how contracts will be represented
-- in these errors, e.g. 'Address'.
data ExecutorError' a
= EEUnknownContract a
-- ^ The interpreted contract hasn't been originated.
| EEInterpreterFailed a
InterpretError
-- ^ Interpretation of Michelson contract failed.
| EEUnknownAddressAlias Alias
-- ^ Given alias doesn't refer to any 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 tezos-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.
| 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.
deriving stock (Show, Functor)
instance (Buildable a) => Buildable (ExecutorError' a) where
build =
\case
EEUnknownAddressAlias alias ->
"The alias " +| alias |+ " doesn't have any associated addresses"
EEUnknownContract addr -> "The contract is not originated " +| addr |+ ""
EEInterpreterFailed addr err ->
"Michelson interpreter failed for contract " +| addr |+ ": " +| 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 |+ ""
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) |+ ""
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
:: FilePath
-> TypeCheckOptions
-> Address
-> Maybe Alias
-> Maybe KeyHash
-> Mutez
-> U.Value
-> U.Contract
-> "verbose" :! Bool
-> IO Address
originateContract dbPath tcOpts originator mbAlias delegate balance uStorage uContract verbose = do
origination <- either throwM pure . typeCheckingWith tcOpts $
mkOrigination <$> typeCheckContractAndStorage uContract uStorage
-- pass 100500 as maxSteps, because it doesn't matter for origination,
-- as well as 'now'
fmap snd $ runExecutorMWithDB Nothing Nothing Nothing dbPath 100500 verbose (#dryRun :? Nothing) $ do
executeGlobalOrigination origination
where
mkOrigination (SomeContractAndStorage contract storage) = OriginationOperation
{ ooOriginator = originator
, ooDelegate = delegate
, ooBalance = balance
, ooStorage = storage
, ooContract = contract
, ooCounter = 0
, ooAlias = mbAlias
}
-- | 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
:: Maybe Timestamp
-> Maybe Natural
-> Maybe Natural
-> Word64
-> Mutez
-> FilePath
-> TypeCheckOptions
-> U.Value
-> U.Contract
-> TxData
-> "verbose" :! Bool
-> "dryRun" :! Bool
-> IO SomeStorage
runContract maybeNow maybeLevel maybeMinBlockTime maxSteps initBalance dbPath tcOpts uStorage uContract txData
verbose (arg #dryRun -> dryRun) = do
origination <- either throwM pure . typeCheckingWith tcOpts $
mkOrigination <$> typeCheckContractAndStorage uContract uStorage
(_, newSt) <- runExecutorMWithDB maybeNow maybeLevel maybeMinBlockTime dbPath
(RemainingSteps maxSteps) verbose ! #dryRun dryRun $ do
-- Here we are safe to bypass executeGlobalOperations for origination,
-- since origination can't generate more operations.
addr <- executeGlobalOrigination origination
let transferOp = TransferOp $ TransferOperation addr txData 1
executeGlobalOperations tcOpts [transferOp]
getContractStorage addr
return newSt
where
-- 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.
delegate :: KeyHash
delegate =
either (error . mappend "runContract can't parse delegate: " . pretty) id $
parseHash "tz1YCABRTa6H8PLKx2EtDWeCGPaKxUhNgv47"
mkOrigination (SomeContractAndStorage contract storage) = OriginationOperation
{ ooOriginator = genesisAddress
, ooDelegate = Just delegate
, ooBalance = initBalance
, ooStorage = storage
, ooContract = contract
, ooCounter = 0
, ooAlias = Nothing
}
getContractStorage :: Address -> ExecutorM SomeStorage
getContractStorage addr = do
addrs <- use (esGState . gsAddressesL)
case addrs ^. at addr of
Nothing -> error $ pretty addr <> " is unknown"
Just (ASSimple {}) -> error $ pretty addr <> " is a simple address"
Just (ASContract (ContractState{..})) -> return $ SomeStorage csStorage
-- | Send a transaction to given address with given parameters.
transfer ::
Maybe Timestamp
-> Maybe Natural
-> Maybe Natural
-> Word64
-> FilePath
-> TypeCheckOptions
-> AddressOrAlias
-> TxData
-> "verbose" :! Bool
-> "dryRun" :? Bool
-> IO ()
transfer maybeNow maybeLevel maybeMinBlockTime maxSteps dbPath tcOpts destination txData verbose dryRun = do
void $ runExecutorMWithDB maybeNow maybeLevel maybeMinBlockTime dbPath (RemainingSteps maxSteps) verbose dryRun $ do
destAddr <- resolveAddress destination
executeGlobalOperations tcOpts [TransferOp $ TransferOperation 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
-> GState
-> ExecutorM a
-> Either ExecutorError (ExecutorRes, a)
runExecutorM now level minBlockTime remainingSteps gState action =
fmap preResToRes
$ runExcept
$ runStateT (runReaderT action $ ExecutorEnv now level minBlockTime)
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.
--
-- Unless @dryRun@ is @False@, the final state is written back to the disk.
--
-- If the executor fails with 'ExecutorError' it will be thrown as an exception.
runExecutorMWithDB
:: Maybe Timestamp
-> Maybe Natural
-> Maybe Natural
-> FilePath
-> RemainingSteps
-> "verbose" :! Bool
-> "dryRun" :? Bool
-> ExecutorM a
-> IO (ExecutorRes, a)
runExecutorMWithDB maybeNow maybeLevel maybeMinBlockTime dbPath remainingSteps
(arg #verbose -> verbose)
(argDef #dryRun False -> dryRun)
action = do
gState <- readGState dbPath
now <- maybe getCurrentTime pure maybeNow
let level = fromMaybe 0 maybeLevel
mbt = fromMaybe dummyMinBlockTime maybeMinBlockTime
(res@ExecutorRes{..}, a) <- either throwM pure $
runExecutorM now level mbt remainingSteps gState action
unless dryRun $
writeGState dbPath _erGState
mapM_ printInterpretResult _erInterpretResults
when (verbose && not (null _erUpdates)) $ do
fmtLn $ nameF "Updates" (blockListF _erUpdates)
putTextLn $ "Remaining gas: " <> pretty _erRemainingSteps <> "."
return (res, a)
where
printInterpretResult
:: (Address, InterpretResult) -> IO ()
printInterpretResult (addr, InterpretResult {..}) = do
putTextLn $ "Executed contract " <> pretty addr
case iurOps of
[] -> putTextLn "It didn't return any operations."
_ -> fmt $ nameF "It returned operations" (blockListF iurOps)
putTextLn $
"It returned storage: " <> pretty (untypeValue iurNewStorage) <> "."
let MorleyLogs logs = iurMorleyLogs
unless (null logs) $ do
putTextLn "And produced logs:"
mapM_ putTextLn logs
putTextLn "" -- extra break line to separate logs from two sequence contracts
-- | Resolves 'AddressOrAlias' type to 'Address'
resolveAddress :: AddressOrAlias -> ExecutorM Address
resolveAddress (AddressResolved addr) = pure addr
resolveAddress (AddressAlias alias) = do
addrAliases <- use $ esGState . gsAddressAliasesL
case lookupAddress alias addrAliases of
Just addr -> pure addr
Nothing -> throwError $ EEUnknownAddressAlias alias
-- | Execute a list of global operations, discarding their results.
executeGlobalOperations
:: TypeCheckOptions
-> [ExecutorOp]
-> ExecutorM ()
executeGlobalOperations tcOpts = mapM_ $ \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 ()
executeMany isGlobalOp = \case
[] -> pass
(op:opsTail) -> 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 Nothing tcOpts transferOperation
executeMany (#isGlobalOp :! False) $ moreOps <> opsTail
-- | Execute a global origination operation.
executeGlobalOrigination :: OriginationOperation -> ExecutorM Address
executeGlobalOrigination = executeOrigination ! #isGlobalOp True
-- | Execute an origination operation.
executeOrigination
:: "isGlobalOp" :! Bool
-> OriginationOperation
-> ExecutorM Address
executeOrigination (arg #isGlobalOp -> isGlobalOp) origination@(OriginationOperation{..}) = do
when isGlobalOp $ do
beginGlobalOperation
assign esOperationHash $ mkOriginationOperationHash origination
checkOperationReplay $ OriginateOp origination
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 gsAddresses gs ^. at originatorAddress of
Nothing -> throwError (EEUnknownManager originatorAddress)
Just (asBalance -> oldBalance)
| oldBalance < ooBalance ->
throwError $ EENotEnoughFunds originatorAddress 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 GSAddAddressAlias ooAlias (Just address)
, Just $ GSAddAddress address (ASContract contractState)
, Just $ GSSetBalance originatorAddress originatorBalance
, Just GSIncrementCounter
, if bigMapCounter0 == bigMapCounter1
then Nothing
else Just $ GSSetBigMapCounter bigMapCounter1
]
case applyUpdates updates gs of
Left err -> throwError $ 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
let updates = [GSSetDelegate sdoContract sdoDelegate]
case applyUpdates updates gs of
Left err -> throwError $ EEFailedToApplyUpdates err
Right newGS -> do
esGState .= newGS
esLog <>= ExecutorLog updates []
return ()
-- | Execute a transfer operation.
executeTransfer
:: "isGlobalOp" :! Bool
-> Maybe Mutez
-- ^ Whether to override the destination's balance.
--
-- When it's a `Just`, the destination's balance will be set to the given amount
-- and no tz will be debited from the sender's account.
-- This is useful to emulate the behaviour of the Tezos RPC's @/run_code@ endpoint.
-> TypeCheckOptions
-> TransferOperation
-> ExecutorM [ExecutorOp]
executeTransfer (arg #isGlobalOp -> isGlobalOp)
overrideBalanceMb tcOpts transferOperation@(TransferOperation addr txData _) = do
when isGlobalOp $
beginGlobalOperation
now <- view eeNow
level <- view eeLevel
mbt <- view eeMinBlockTime
gs <- use esGState
remainingSteps <- use esRemainingSteps
mSourceAddr <- use esSourceAddress
let globalCounter = gsCounter gs
let addresses = gsAddresses gs
let senderAddr = tdSenderAddress txData
let sourceAddr = fromMaybe senderAddr mSourceAddr
let isZeroTransfer = tdAmount txData == zeroMutez
checkOperationReplay $ TransferOp transferOperation
-- Implicit addresses can't be senders with a balance of 0tz even when the transfer amount
-- is zero.
when (isKeyAddress senderAddr && isNothing overrideBalanceMb) $
case addresses ^. at senderAddr of
Nothing -> throwError $ EEEmptyImplicitContract senderAddr
Just (asBalance -> balance) | balance == zeroMutez ->
throwError $ EEEmptyImplicitContract senderAddr
_ -> pass
when (badParamToImplicitAccount addr $ tdParameter txData) $
throwError $ EEWrongParameterType addr
-- Transferring 0 XTZ to a key address is prohibited.
when (isZeroTransfer && isKeyAddress addr) $
throwError $ EEZeroTransaction addr
mDecreaseSenderBalance <-
if isNothing overrideBalanceMb && not isZeroTransfer
then case addresses ^. at senderAddr of
Nothing -> throwError $ EEUnknownSender senderAddr
Just (asBalance -> balance)
| balance < tdAmount txData ->
throwError $ EENotEnoughFunds senderAddr balance
| otherwise ->
-- Subtraction is safe because we have checked its
-- precondition in guard.
return $ Just $ GSSetBalance senderAddr (balance `unsafeSubMutez` tdAmount txData)
else pure Nothing
when (not (isKeyAddress senderAddr) && isGlobalOp && not isZeroTransfer) $
throwError $ EETransactionFromContract senderAddr $ tdAmount txData
let onlyUpdates updates = return (updates, [], Nothing, remainingSteps)
(otherUpdates, sideEffects, maybeInterpretRes :: Maybe InterpretResult, newRemSteps)
<- case (addresses ^. at addr, addr) of
(Nothing, TransactionRollupAddress _) ->
-- TODO [#838]: support transaction rollups on the emulator
throwError $ EEUnknownContract addr
(Nothing, ContractAddress _) ->
throwError $ EEUnknownContract addr
(Nothing, KeyAddress _) -> do
let
transferAmount = tdAmount txData
addrState = ASSimple transferAmount
upd = GSAddAddress addr addrState
onlyUpdates [upd]
(Just (ASSimple oldBalance), _) -> do
let
-- Calculate the account's new balance, unless `overrideBalanceMb` is used.
-- Note: `unsafeAddMutez` can't overflow if global state is correct (because we can't
-- create money out of nowhere)
newBalance =
fromMaybe
(oldBalance `unsafeAddMutez` tdAmount txData)
overrideBalanceMb
upd = GSSetBalance addr newBalance
onlyUpdates [upd]
(Just (ASContract (ContractState {..})), _) -> do
let
existingContracts = extractAllContracts gs
newBalance =
-- Calculate the contract's new balance, unless `overrideBalanceMb` is used.
-- Note: `unsafeAddMutez` can't overflow if global state is correct (because we can't
-- create money out of nowhere)
fromMaybe
(csBalance `unsafeAddMutez` tdAmount txData)
overrideBalanceMb
epName = tdEntrypoint txData
T.MkEntrypointCallRes _ (epc :: EntrypointCallT cp epArg)
<- T.mkEntrypointCall epName (T.cParamNotes csContract)
& maybe (throwError $ EEUnknownEntrypoint epName) pure
-- 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 txData of
TxTypedParam (typedVal :: T.Value t) -> do
T.castM @t @epArg typedVal (throwError . EEUnexpectedParameterType addr)
TxUntypedParam untypedVal ->
liftEither $ first (EEIllTypedParameter addr) $ typeCheckingWith tcOpts $
typeVerifyParameter @epArg existingContracts untypedVal
let bigMapCounter0 = gs ^. gsBigMapCounterL
let (typedParameterWithIds, bigMapCounter1) = runState (assignBigMapIds False typedParameter) bigMapCounter0
-- 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 .= mkTransferOperationHash
addr
typedParameterWithIds
(tdEntrypoint txData)
(tdAmount txData)
opHash <- use esOperationHash
let
contractEnv = ContractEnv
{ ceNow = now
, ceMaxSteps = remainingSteps
, ceBalance = newBalance
, ceContracts = gsAddresses gs
, ceSelf = addr
, ceSource = sourceAddr
, ceSender = senderAddr
, ceAmount = tdAmount txData
, ceVotingPowers = gsVotingPowers gs
, ceChainId = gsChainId gs
, ceOperationHash = Just opHash
, ceLevel = level
, ceErrorSrcPos = def
, ceMinBlockTime = mbt
}
iur@InterpretResult
{ iurOps = sideEffects
, iurNewStorage = newValue
, iurNewState = InterpreterState newRemainingSteps globalCounter2 bigMapCounter2
}
<- liftEither $ first (EEInterpreterFailed addr) $
handleContractReturn $
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
| bigMapCounter0 == bigMapCounter2 = Nothing
| otherwise = Just $ GSSetBigMapCounter bigMapCounter2
updGlobalCounter
| globalCounter == globalCounter2 = Nothing
| otherwise = Just $ GSUpdateCounter globalCounter2
updates = catMaybes
[ updBalance
, updStorage
, updBigMapCounter
, updGlobalCounter
]
return (updates, sideEffects, Just iur, newRemainingSteps)
let
-- According to the reference implementation, counter is incremented for transfers as well.
updates = (maybe id (:) mDecreaseSenderBalance otherUpdates) ++ [GSIncrementCounter]
newGState <- liftEither $ first EEFailedToApplyUpdates $ applyUpdates updates gs
esGState .= newGState
esRemainingSteps .= newRemSteps
esSourceAddress .= Just sourceAddr
esLog <>= ExecutorLog updates (maybe mempty (one . (addr, )) maybeInterpretRes)
return $ convertOp addr <$> sideEffects
----------------------------------------------------------------------------
-- Simple helpers
----------------------------------------------------------------------------
checkOperationReplay :: ExecutorOp -> ExecutorM ()
checkOperationReplay op = do
let
opCounter = op & \case
OriginateOp OriginationOperation{..} -> ooCounter
TransferOp TransferOperation{..} -> toCounter
SetDelegateOp SetDelegateOperation{..} -> sdoCounter
prevCounters <- use esPrevCounters
when (opCounter `HS.member` prevCounters) $
throwError $ EEOperationReplay op
esPrevCounters <>= one opCounter
-- The argument is the address of the contract that generated this operation.
convertOp :: Address -> T.Operation -> ExecutorOp
convertOp interpretedAddr =
\case
OpTransferTokens tt ->
case ttContract tt of
T.VContract destAddress sepc ->
let txData =
TxData
{ tdSenderAddress = 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{..} -> SetDelegateOp SetDelegateOperation
{ sdoContract = interpretedAddr
, sdoDelegate = sdMbKeyHash
, sdoCounter = sdCounter
}
OpCreateContract cc ->
let origination = OriginationOperation
{ ooOriginator = ccOriginator cc
, ooDelegate = ccDelegate cc
, ooBalance = ccBalance cc
, ooStorage = ccStorageVal cc
, ooContract = ccContract cc
, ooCounter = ccCounter cc
, ooAlias = Nothing
}
in OriginateOp origination
-- | Reset source address before executing a global operation.
beginGlobalOperation :: ExecutorM ()
beginGlobalOperation =
esSourceAddress .= Nothing
-- | Return True if address is an implicit account yet the param is not Unit.
badParamToImplicitAccount :: Address -> TxParam -> Bool
badParamToImplicitAccount (ContractAddress _) _ = False
badParamToImplicitAccount (KeyAddress _) (TxTypedParam T.VUnit) = False
badParamToImplicitAccount (KeyAddress _) (TxUntypedParam U.ValueUnit) = False
badParamToImplicitAccount _ _ = True