packages feed

cleveland-0.1.1: morley-test/Test/Interpreter/Reference.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

module Test.Interpreter.Reference
  ( test_InterpreterWithReferenceImplementation
  , test_Regression688
  ) where

import Data.Default (def)
import Data.Singletons (Sing)
import Data.Typeable ((:~:)(Refl))
import Fmt (pretty)
import Hedgehog (MonadTest, PropertyT, annotate, evalIO, forAll, property, withTests, (===))
import System.IO.Silently (silence)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Hedgehog (testProperty)

import Hedgehog.Gen.Michelson.Typed (genValue)
import Morley.AsRPC (AsRPC, MaybeRPC(..), TAsRPC, rpcStorageScopeEvi, valueAsRPC)
import Morley.Client (RunError(..), runMorleyClientM)
import Morley.Client.RPC (RunCodeErrors(..))
import Morley.Client.Util as Reference (RunContractParameters(..), runContract)
import Morley.Michelson.Interpret
  (InterpretError(..), MichelsonFailed(..), MichelsonFailureWithStack(..))
import Morley.Michelson.Runtime as Morley
import Morley.Michelson.Runtime.GState (genesisAddress)
import Morley.Michelson.TypeCheck (TypeCheckOptions(..))
import Morley.Michelson.Typed
  (Contract, Contract'(..), EpAddress(..), SingT(..), SomeConstrainedValue(..), SomeContract(..),
  SomeStorage, Value, Value'(..), dfsMapValue)
import Morley.Michelson.Typed.Arith (ArithError(..), MutezArithErrorType(..))
import Morley.Michelson.Typed.Convert
import Morley.Michelson.Typed.Scope
import Morley.Michelson.Typed.T
import Morley.Michelson.Untyped qualified as U
import Morley.Tezos.Address
import Morley.Tezos.Core (Timestamp(..), dummyChainId, getCurrentTime, zeroMutez)
import Morley.Tezos.Crypto
import Morley.Util.Named
import Morley.Util.Sing (eqI)
import Test.Cleveland (NetworkEnv(neMorleyClientEnv))
import Test.Cleveland.Michelson.Import
import Test.Cleveland.Tasty (whenNetworkEnabled)
import Test.Cleveland.Util (failedTest)
import Test.Util.Contracts (getWellTypedMichelsonContracts)

test_InterpreterWithReferenceImplementation :: IO TestTree
test_InterpreterWithReferenceImplementation = do
  files <- filter (`notElem` excludedContracts) <$> getWellTypedMichelsonContracts

  filesAndContracts :: [(FilePath, SomeContract)] <-
    forM files $ \file -> do
      someContract <- importSomeContract file
      pure (file, someContract)

  pure $
    whenNetworkEnabled $ \withEnv ->
      testGroup "compare interpreter with reference implementation" $
        withFrozenCallStack $ testContract withEnv <$> filesAndContracts
  where
    -- These constracts are currently excluded from the tests since
    -- our or reference implementations doesn't act in an expected way.
    --
    -- Ideally, we should remove the usage of this list one day :coolstory:
    excludedContracts :: [FilePath]
    excludedContracts =
      [ "../../contracts/tezos_examples/mini_scenarios/replay.tz"
      , "../../contracts/replay_origination.tz"
      , "../../contracts/replay_transfer.tz"
      -- ↑ See [#320] and https://gitlab.com/tezos/tezos/-/issues/897
      , "../../contracts/call_self_several_times.tz"
      , "../../contracts/tezos_examples/attic/cps_fact.tz"
      -- ↑ Due to that fact that 'run_code' doesn't perform internal operations in contrary to morley interpreter
      , "../../contracts/tezos_examples/mini_scenarios/multiple_entrypoints_counter.tz"
      , "../../contracts/tezos_examples/mini_scenarios/multiple_en2.tz"
      -- TODO [#699]: find out why these still fail
      -- ↑ 'SELF' and 'SENDER' instruction return same addresses in 'run_code'
      , "../../contracts/tezos_examples/mini_scenarios/lockup.tz"
      , "../../contracts/tezos_examples/attic/forward.tz"
      -- ↑ Reference 'run_code' implementation does something weird to allow transfers with arbitrary
      -- amounts even when BALANCE + AMOUNT < required amount of money that needs to be transfered.
      -- This probably will be resolved in https://gitlab.com/tezos/tezos/-/issues/897, when 'run_code'
      -- will actually try to apply operation instead of running it inside weird environment.
      -- Or in [#406] if the issue turns to be on our side.
      , "../../contracts/voting_powers.tz"
      , "../../contracts/tezos_examples/opcodes/voting_power.tz"
      -- ↑ We cannot predict the total voting power in a real chain.
      -- There is still a similar @voting_power.tz@ that touches only
      -- @VOTING_POWER@ instruction.
      , "../../contracts/tezos_examples/opcodes/level.tz"
      -- ↑ We cannot predict the actual block level in a real chain.
      -- There is another @../../contracts/level.tz@ contract that checks that we can interpret
      -- the @LEVEL@ instruction, without checking its result.
      , "../../contracts/sapling_annot.tz"
      , "../../contracts/sapling_transaction.tz"
      , "../../contracts/tezos_examples/opcodes/sapling_empty_state.tz"
      -- ↑ Sapling types and instructions are supported for typechecking only and not the actual
      -- implementation.
     ]

test_Regression688 :: IO TestTree
test_Regression688 = do
  let file = "../../contracts/tezos_examples/mini_scenarios/xcat_dapp.tz"
      parameter = VOr @_ @('TOr 'TBytes 'TBytes) $ Left $
        VPair
          (VAddress $ EpAddress
            { eaAddress = KeyAddress $ KeyHash
                { khTag = KeyHashEd25519
                , khBytes = "UK\224\155\204\188\151\163%\183I\194*H\252P\246\&4\192\205"
                }
            , eaEntrypoint = U.DefEpName}
          , VPair (VBytes "", VTimestamp $ Timestamp 1225497600)
          )
      storage = VPair (
        VBigMap @'TBytes @('TPair ('TPair 'TAddress 'TAddress) ('TPair 'TMutez 'TTimestamp))
          Nothing mempty,VUnit)

  contract <- importContract file

  pure $
    whenNetworkEnabled $ \withEnv ->
      testProperty "#688 regression test" $ withTests 1 $ property $
        withFrozenCallStack $ compareWithReference withEnv file contract parameter storage

testContract
  :: HasCallStack
  => (forall a. (NetworkEnv -> IO a) -> IO a)
  -> (FilePath, SomeContract) -> TestTree
testContract withEnv (file, someContract) =
  case someContract of
    SomeContract (contract@Contract {} :: Contract cp st) ->
      -- We run each contract 4 times to check that it behaves the same way on different
      -- inputs.
      testProperty ("compare result with morley interpreter for " <> file) $
        withTests 4 $ property $ do
          parameter <- forAll $ genValue @cp
          storage <- forAll $ genValue @st

          compareWithReference withEnv file contract parameter storage

compareWithReference
  :: forall cp st. (ParameterScope cp, StorageScope st)
  => (forall a. (NetworkEnv -> IO a) -> IO a)
  -> FilePath -> Contract cp st -> Value cp -> Value st -> PropertyT IO ()
compareWithReference withEnv file contract parameter storage = do
  (resReference, resMorley) <-
    evalIO $
      withEnv \env -> do
        resReference <- try @_ @RunCodeErrors $
          runMorleyClientM (neMorleyClientEnv env) $
            Reference.runContract @cp @st RunContractParameters
              { rcpContract = contract
              , rcpParameter = NotRPC parameter
              , rcpStorage = NotRPC storage
              , rcpBalance = 4000000000000
              , rcpAmount = zeroMutez
              , rcpSender = Just genesisAddress
              , rcpSource = Just genesisAddress
              }
        currentTimestamp <- getCurrentTime
        -- Reference implementation sends 0,05 tz to implicit contract for this contract
        let amount = if file == "../../contracts/tezos_examples/opcodes/proxy.tz"
                    then 50000 else minBound
        resMorley <- try @_ @ExecutorError $ silence $
          Morley.runContract (Just currentTimestamp) (Just 0) 100500 4000000000000 ""
          tcOptions (untypeValue storage) (convertContract contract)
          (TxData genesisAddress (TxUntypedParam $ untypeValue parameter) U.DefEpName amount)
          (#verbose :! False)
          (#dryRun :! True)

        pure (resReference, resMorley)

  compareResults resReference resMorley parameter storage

tcOptions :: TypeCheckOptions
tcOptions = def
  { -- Since we test against @tezos-client run@
    tcStrict = False
  }

compareResults
      :: forall cp st m.
         (HasCallStack, StorageScope st, MonadTest m)
      => Either RunCodeErrors (AsRPC (Value st))
      -> Either ExecutorError SomeStorage -> Value cp -> Value st -> m ()
compareResults (Left rpcErr) (Left interpreterErr) _ _ = compareErrors rpcErr interpreterErr
compareResults (Left err) (Right _) parameter storage = do
  failedTest . fromString $
    "Morley interpreter unexpectedly didn't fail.\n Passed parameter: " <> pretty parameter <>
    ".\n Passed storage: " <> pretty storage <> ".\n Reference implementation failed with: " <>
    pretty err
compareResults (Right _) (Left err) parameter storage = do
  failedTest . fromString $
    "Morley interpreter unexpectedly failed.\n " <> "Passed parameter: " <> pretty parameter <>
    ".\n Passed storage: " <> pretty storage <> ".\n Morley interpreter failed with: " <>
    displayException err
compareResults (Right st1) (Right (SomeStorage (st2 :: Value st2))) parameter storage =
  withDict (rpcStorageScopeEvi @st) $ do
    case eqI @st @st2 of
      Nothing ->
        failedTest . fromString $ "Result storages have different types.\n" <>
          "Reference implementation returned storage: " <> pretty st1 <> ".\n" <>
          "Morley returned storage: " <> pretty st2
      Just Refl -> do
        annotate $
          ("Both contracts succeeded, but new storages are different.\n Passed parameter: " <>
            pretty parameter <> ".\n Passed storage: " <> pretty storage <> ".\n" <>
            "Reference implementation returned storage: " <> pretty st1 <> ".\n" <>
            "Morley returned storage: " <> pretty st2
          )
        compareValues st1 st2

-- | Compare values with weaken equality requirements, e.g. we don't
-- check that @VAddress@, @VBytes@, @VChainId@ and @VTimestamp@ have same constructor arguments,
-- since they can be different even when both interpreters were successfully run.
--
-- We also don't check big_map contents or their IDs.
--
-- TODO [#604]: compare big_maps' contents.
compareValues :: forall st m. (HasCallStack, MonadTest m, SingI st) => Value (TAsRPC st) -> Value st -> m ()
compareValues storageReference storageMorley =
  preprocessValue storageReference === preprocessValue (valueAsRPC storageMorley)
  where
    preprocessValue :: Value (TAsRPC st) -> Value (TAsRPC st)
    preprocessValue = dfsMapValue placeStubs . scrubBigMapIDs (sing @st)

    placeStubs :: Value t -> Value t
    placeStubs = \case
      VAddress _ -> VAddress $ EpAddress genesisAddress U.DefEpName
      VBytes _ -> VBytes "kek"
      VChainId _ -> VChainId dummyChainId
      VTimestamp _ -> VTimestamp $ Timestamp 100
      v -> v

    -- | Scrub all big_map IDs from a storage value.
    --
    -- The morley interpreter and the RPC might assign different IDs to each big_map,
    -- so we need to scrub all IDs before checking whether the two storages are equivalent.
    scrubBigMapIDs :: forall t. Sing t -> Value (TAsRPC t) -> Value (TAsRPC t)
    scrubBigMapIDs storageSing storage =
      case (storageSing, storage) of
        (STBigMap{}, VNat _) -> VNat 0
        (STOption vSing, VOption v) -> VOption $ scrubBigMapIDs vSing <$> v
        (STList vSing, VList v) -> VList $ scrubBigMapIDs vSing <$> v
        (STPair lSing rSing, VPair v) -> VPair $ bimap (scrubBigMapIDs lSing) (scrubBigMapIDs rSing) v
        (STOr lSing rSing, VOr v) -> VOr $ bimap (scrubBigMapIDs lSing) (scrubBigMapIDs rSing) v
        (STMap _ vSing, VMap v) -> VMap $ scrubBigMapIDs vSing <$> v
        _ -> storage

assertRpcErrs :: (HasCallStack, MonadTest m) => [RunError] -> (RunError -> Bool) -> String -> m ()
assertRpcErrs errs predicate msg =
  if any predicate errs then pass else do failedTest $ fromString msg

-- Note that error comparison can be extended when the new contracts will be added.
compareErrors :: (HasCallStack, MonadTest m) => RunCodeErrors -> ExecutorError -> m ()
compareErrors rpcErr@(RunCodeErrors errs) interpreterErr = case interpreterErr of
  EEInterpreterFailed _ (InterpretError (mfwsFailed -> runtimeErr, _)) -> case runtimeErr of
    MichelsonFailedWith _ ->
      assertRpcErrs errs
      (\case
          ScriptRejected {} -> True
          _ -> False
      ) $ "Morley interpreter failed with FAILWITH, \
          \however reference interpreter failed with:\n" <> displayException rpcErr
    MichelsonArithError arithErr -> case arithErr of
      ShiftArithError {}->
        assertRpcErrs errs
        (\case
            ScriptOverflow -> True
            _ -> False
        ) $ "Morley interpreter failed with shift overflow, \
            \however reference interpreter failed with:\n" <> displayException rpcErr
      MutezArithError AddOverflow _ _ ->
        assertRpcErrs errs
        (\case
            MutezAdditionOverflow {} -> True
            _ -> False
        ) $ "Morley interpreter failed with mutez addition overflow, \
            \however reference interpreter failed with:\n" <> displayException rpcErr
      MutezArithError MulOverflow _ _ ->
        assertRpcErrs errs
        (\case
            MutezMultiplicationOverflow {} -> True
            ScriptOverflow -> True
            _ -> False
        ) $ "Morley interpreter failed with mutez multiplication overflow, \
            \however reference interpreter failed with:\n" <> displayException rpcErr
    MichelsonGasExhaustion ->
      assertRpcErrs errs
      (\case
          GasExhaustedOperation -> True
          _ -> False
      ) $ "Morley interpreter failed due to gas exhaustion, \
          \however reference interpreter failed with:\n" <> displayException rpcErr
    _ -> do
      failedTest . fromString $ "Unexpected morley runtime failure:\n" <> pretty runtimeErr <>
          "\nReference interpreter failed with:\n" <> displayException rpcErr
  EEIllTypedParameter _ _ ->
    assertRpcErrs errs
    (\case
        InconsistentTypes {} -> True
        BadContractParameter {} -> True
        _ -> False
    ) $ "Morley interpreter failed during parmeter typechecking, \
        \however reference interpreter failed with:\n" <> displayException rpcErr
  _ -> do
    failedTest . fromString $ "Unexpected morley interpreter failure:\n" <> displayException interpreterErr <>
      "\nReference interpreter failed with:\n" <> displayException rpcErr