morley-1.15.0: src/Michelson/Runtime/GState.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
-- | Global blockchain state (emulated).
module Michelson.Runtime.GState
(
-- * Auxiliary types
ContractState (..)
, AddressState (..)
, asBalance
, VotingPowers (..)
, vpPick
, vpTotal
, mkVotingPowers
, mkVotingPowersFromMap
, dummyVotingPowers
, BigMapCounter(..)
, bigMapCounter
-- * GState
, GState (..)
, gsChainIdL
, gsAddressesL
, gsVotingPowersL
, gsCounterL
, gsBigMapCounterL
, genesisAddresses
, genesisKeyHashes
, genesisAddress
-- * More genesisAddresses which can be used in tests
, genesisAddress1
, genesisAddress2
, genesisAddress3
, genesisAddress4
, genesisAddress5
, genesisAddress6
, genesisKeyHash
-- * Genesis secret keys
, genesisSecretKey
, genesisSecrets
, initGState
, readGState
, writeGState
-- * Operations on GState
, GStateUpdate (..)
, GStateUpdateError (..)
, applyUpdate
, applyUpdates
, extractAllContracts
) where
import Control.Lens (at, makeLenses)
import Data.Aeson (FromJSON(..), ToJSON(..), object, withObject, (.:), (.=))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encode.Pretty as Aeson
import Data.Aeson.TH (deriveJSON)
import qualified Data.ByteString.Lazy as LBS
import Data.Default (def)
import Data.List.NonEmpty ((!!))
import qualified Data.Map.Strict as Map
import Data.Type.Equality ((:~:)(..))
import Fmt (Buildable(build), (+|), (|+), (||+))
import System.IO.Error (IOError, isDoesNotExistError)
import Michelson.TypeCheck
(SomeParamType(..), TcOriginatedContracts,
typeCheckContractAndStorage, typeCheckingWith)
import qualified Michelson.Typed as T
import Michelson.Typed.Existential (SomeContractAndStorage(..))
import Michelson.Typed.Scope
import Michelson.Untyped (Contract, Value)
import Tezos.Address (Address(..), ContractHash, GlobalCounter(..))
import Tezos.Core (ChainId, Mutez, divModMutezInt, dummyChainId)
import Tezos.Crypto
import Util.Aeson
import Util.Lens
import Util.Sing (eqI)
-- | State of a contract with code.
data ContractState =
forall cp st. (ParameterScope cp, StorageScope st) => ContractState
{ csBalance :: Mutez
-- ^ Amount of mutez owned by this contract.
, csContract :: T.Contract cp st
-- ^ Contract itself.
, csStorage :: T.Value st
-- ^ Storage value associated with this contract.
}
deriving stock instance Show ContractState
instance ToJSON ContractState where
toJSON ContractState{..} =
object
[ "balance" .= csBalance
, "storage" .= T.untypeValue csStorage
, "contract" .= T.convertContract csContract
]
-- These instance is a bit hacky because it is quite painful to
-- write proper JSON instances for typed `Instr` and `Value` so
-- we typecheck untyped representation instead of parsing.
instance FromJSON ContractState where
parseJSON =
withObject "contractstate" $ \o -> do
(balance :: Mutez) <- o .: "balance"
(uStorage :: Value) <- o .: "storage"
(uContract :: Contract) <- o .: "contract"
case typeCheckingWith def $ typeCheckContractAndStorage uContract uStorage of
Right (SomeContractAndStorage contract storage) ->
pure $ ContractState balance contract storage
Left err -> fail $ "Unable to parse `ContractState`: " <> (show err)
instance Buildable ContractState where
build ContractState{..} =
"Contractstate:\n Balance: " +| csBalance |+
"\n Storage: " +| T.untypeValue csStorage ||+
"\n Contract: " +| T.convertContract csContract ||+ ""
-- | State of an arbitrary address.
data AddressState
= ASSimple Mutez
-- ^ For contracts without code we store only its balance.
| ASContract ContractState
-- ^ For contracts with code we store more state represented by
-- 'ContractState'.
deriving stock (Show, Generic)
instance Buildable AddressState where
build =
\case
ASSimple balance -> "Balance = " +| balance |+ ""
ASContract cs -> build cs
deriveJSON morleyAesonOptions ''AddressState
-- | Extract balance from 'AddressState'.
asBalance :: AddressState -> Mutez
asBalance =
\case
ASSimple b -> b
ASContract cs -> csBalance cs
-- | Distribution of voting power among the contracts.
--
-- Voting power reflects the ability of bakers to accept, deny or pass new
-- proposals for protocol updates. I.e. each baker has its vote weight.
--
-- This datatype definition may change in future, so its internals are not
-- exported.
newtype VotingPowers
= VotingPowers (Map KeyHash Natural)
deriving stock (Show)
deriveJSON morleyAesonOptions ''VotingPowers
-- | Get voting power of the given address.
vpPick :: KeyHash -> VotingPowers -> Natural
vpPick key (VotingPowers distr) = Map.lookup key distr ?: 0
-- | Get total voting power.
vpTotal :: VotingPowers -> Natural
vpTotal (VotingPowers distr) = sum distr
-- | Create voting power distribution from map.
mkVotingPowersFromMap :: Map KeyHash Natural -> VotingPowers
mkVotingPowersFromMap = VotingPowers
-- | Create voting power distribution.
--
-- If some key is encountered multiple times, voting power will be summed up.
mkVotingPowers :: [(KeyHash, Natural)] -> VotingPowers
mkVotingPowers = mkVotingPowersFromMap . Map.fromListWith (+)
-- | All big_maps stored in a chain have a globally unique ID.
--
-- We use this counter to keep track of how many big_maps have been created so far,
-- and to generate new IDs whenever a new big_map is created.
newtype BigMapCounter = BigMapCounter { _bigMapCounter :: Natural }
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData)
deriving newtype (ToJSON, FromJSON, Num, Buildable)
makeLenses ''BigMapCounter
-- | Persistent data passed to Morley contracts which can be updated
-- as result of contract execution.
data GState = GState
{ gsChainId :: ChainId
-- ^ Identifier of chain.
, gsAddresses :: Map Address AddressState
-- ^ All known addresses and their state.
, gsVotingPowers :: VotingPowers
-- ^ Voting power distribution.
, gsCounter :: GlobalCounter
-- ^ Ever increasing operation counter.
, gsBigMapCounter :: BigMapCounter
} deriving stock (Show)
makeLensesWith postfixLFields ''GState
deriveJSON morleyAesonOptions ''GState
-- | Number of genesis addresses.
genesisAddressesNum :: Word
genesisAddressesNum = 10
-- | Secrets from which genesis addresses are derived from.
genesisSecrets :: NonEmpty SecretKey
genesisSecrets = do
i <- 1 :| [2 .. genesisAddressesNum]
let seed = encodeUtf8 (show i :: Text)
return $ detSecretKey seed
-- | KeyHash of genesis address.
genesisKeyHashes :: NonEmpty KeyHash
genesisKeyHashes = hashKey . toPublic <$> genesisSecrets
-- | Initially these addresses have a lot of money.
genesisAddresses :: NonEmpty Address
genesisAddresses = KeyAddress <$> genesisKeyHashes
-- | One of genesis key hashes.
genesisKeyHash :: KeyHash
genesisKeyHash = head genesisKeyHashes
-- | One of genesis addresses.
genesisAddress :: Address
genesisAddress = head genesisAddresses
-- | Secret key assotiated with 'genesisAddress'.
genesisSecretKey :: SecretKey
genesisSecretKey = head genesisSecrets
-- | More genesis addresses
--
-- We know size of @genesisAddresses@, so it is safe to use @!!@
genesisAddress1, genesisAddress2, genesisAddress3 :: Address
genesisAddress4, genesisAddress5, genesisAddress6 :: Address
genesisAddress1 = genesisAddresses !! 1
genesisAddress2 = genesisAddresses !! 2
genesisAddress3 = genesisAddresses !! 3
genesisAddress4 = genesisAddresses !! 4
genesisAddress5 = genesisAddresses !! 5
genesisAddress6 = genesisAddresses !! 6
-- | Dummy 'VotingPowers'. We give all the voting power to two genesis addreses
-- as the addresses holding lot of money. Only two addresses are involved for
-- simplicity.
dummyVotingPowers :: VotingPowers
dummyVotingPowers = case genesisKeyHashes of
k1 :| k2 : _ -> mkVotingPowers [(k1, 50), (k2, 50)]
_ -> error "Insufficient number of genesis addresses"
-- | Initial 'GState'. It's supposed to be used if no 'GState' is
-- provided. It puts plenty of money on each genesis address.
initGState :: GState
initGState =
GState
{ gsChainId = dummyChainId
, gsAddresses = Map.fromList
[ (genesis, ASSimple money)
| let (money, _) = maxBound @Mutez `divModMutezInt` genesisAddressesNum
?: error "Number of genesis addresses is 0"
, genesis <- toList genesisAddresses
]
, gsVotingPowers = dummyVotingPowers
, gsCounter = GlobalCounter 0
, gsBigMapCounter = BigMapCounter 0
}
data GStateParseError =
GStateParseError String
deriving stock (Show)
instance Exception GStateParseError where
displayException (GStateParseError str) = "Failed to parse GState: " <> str
-- | Read 'GState' from a file.
readGState :: FilePath -> IO GState
readGState fp = (LBS.readFile fp >>= parseFile) `catch` onExc
where
parseFile :: LByteString -> IO GState
parseFile lByteString =
if null lByteString
then pure initGState
else (either (throwM . GStateParseError) pure . Aeson.eitherDecode') lByteString
onExc :: IOError -> IO GState
onExc exc
| isDoesNotExistError exc = pure initGState
| otherwise = throwM exc
-- | Write 'GState' to a file.
writeGState :: FilePath -> GState -> IO ()
writeGState fp gs = LBS.writeFile fp (Aeson.encodePretty' config gs)
where
config =
Aeson.defConfig
{ Aeson.confTrailingNewline = True
}
-- | Updates that can be applied to 'GState'.
data GStateUpdate where
GSAddAddress :: Address -> AddressState -> GStateUpdate
GSSetStorageValue :: StorageScope st => Address -> T.Value st -> GStateUpdate
GSSetBalance :: Address -> Mutez -> GStateUpdate
GSIncrementCounter :: GStateUpdate
GSSetBigMapCounter :: BigMapCounter -> GStateUpdate
deriving stock instance Show GStateUpdate
instance Buildable GStateUpdate where
build =
\case
GSAddAddress addr st ->
"Add address " +| addr |+ " with state " +| st |+ ""
GSSetStorageValue addr tVal ->
"Set storage value of address " +| addr |+ " to " +| T.untypeValue tVal |+ ""
GSSetBalance addr balance ->
"Set balance of address " +| addr |+ " to " +| balance |+ ""
GSIncrementCounter ->
"Increment internal counter after operation"
GSSetBigMapCounter inc ->
"Increment internal big_map counter by: " +| build inc
data GStateUpdateError
= GStateAddressExists Address
| GStateUnknownAddress Address
| GStateNotContract Address
| GStateStorageNotMatch Address
deriving stock (Show)
instance Buildable GStateUpdateError where
build =
\case
GStateAddressExists addr -> "Address already exists: " <> build addr
GStateUnknownAddress addr -> "Unknown address: " <> build addr
GStateNotContract addr -> "Address doesn't have contract: " <> build addr
GStateStorageNotMatch addr ->
"Storage type does not match the contract in run-time state\
\ when updating new storage value to address: " <> build addr
-- | Apply 'GStateUpdate' to 'GState'.
applyUpdate :: GStateUpdate -> GState -> Either GStateUpdateError GState
applyUpdate =
\case
GSAddAddress addr st ->
maybeToRight (GStateAddressExists addr) . addAddress addr st
GSSetStorageValue addr newValue ->
setStorageValue addr newValue
GSSetBalance addr newBalance -> setBalance addr newBalance
GSIncrementCounter -> Right . over gsCounterL (+1)
GSSetBigMapCounter bmCounter -> Right . set gsBigMapCounterL bmCounter
-- | Apply a list of 'GStateUpdate's to 'GState'.
applyUpdates :: [GStateUpdate] -> GState -> Either GStateUpdateError GState
applyUpdates = flip (foldM (flip applyUpdate))
-- | Add an address if it hasn't been added before.
addAddress :: Address -> AddressState -> GState -> Maybe GState
addAddress addr st gs
| addr `Map.member` accounts = Nothing
| otherwise = Just (gs {gsAddresses = accounts & at addr .~ Just st})
where
accounts = gsAddresses gs
-- | Update storage value associated with given address.
setStorageValue :: forall st. (StorageScope st) =>
Address -> T.Value st -> GState -> Either GStateUpdateError GState
setStorageValue addr newValue = updateAddressState addr modifier
where
modifier :: AddressState -> Either GStateUpdateError AddressState
modifier (ASSimple _) = Left (GStateNotContract addr)
modifier (ASContract (ContractState b c (_ :: T.Value st') )) = do
case eqI @st @st' of
Just Refl -> Right $ ASContract $ ContractState b c newValue
_ -> Left $ GStateStorageNotMatch addr
-- | Update storage value associated with given address.
setBalance :: Address -> Mutez -> GState -> Either GStateUpdateError GState
setBalance addr newBalance = updateAddressState addr (Right . modifier)
where
modifier (ASSimple _) = ASSimple newBalance
modifier (ASContract cs) = ASContract (cs {csBalance = newBalance})
updateAddressState ::
Address
-> (AddressState -> Either GStateUpdateError AddressState)
-> GState
-> Either GStateUpdateError GState
updateAddressState addr f gs =
case addresses ^. at addr of
Nothing -> Left (GStateUnknownAddress addr)
Just as -> do
newState <- f as
return $ gs { gsAddresses = addresses & at addr .~ Just newState }
where
addresses = gsAddresses gs
-- | Retrieve all contracts stored in GState
extractAllContracts :: GState -> TcOriginatedContracts
extractAllContracts = Map.fromList . mapMaybe extractContract . toPairs . gsAddresses
where
extractContract
:: (Address, AddressState) -> Maybe (ContractHash, SomeParamType)
extractContract =
\case (KeyAddress _, ASSimple {}) -> Nothing
(KeyAddress _, _) -> error "broken GState"
(ContractAddress ca, ASContract (ContractState{..})) ->
Just (ca, SomeParamType sing $ T.cParamNotes $ csContract)
(ContractAddress _, _) -> error "broken GState"