From 5db2971e5db7ecad02339f662a75c06238ec19f3 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Thu, 16 Nov 2023 10:54:43 +0100 Subject: [PATCH 01/20] Preliminary support for Merkle proofs. --- concordium-base | 2 +- concordium-consensus/package.yaml | 25 ++++++++++ .../KonsensusV1/Consensus/Blocks.hs | 5 ++ .../src/Concordium/KonsensusV1/Types.hs | 48 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/concordium-base b/concordium-base index a3a16407c8..b57f5b9c4a 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit a3a16407c82ad49de404b3968ba8507f86314681 +Subproject commit b57f5b9c4af71333f49dc1a17cf0423f0a596c44 diff --git a/concordium-consensus/package.yaml b/concordium-consensus/package.yaml index 91715ab15e..cb82661fa2 100644 --- a/concordium-consensus/package.yaml +++ b/concordium-consensus/package.yaml @@ -202,6 +202,31 @@ executables: - concordium-consensus - clock + merkle: + main: Main.hs + source-dirs: test-runners/merkle + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + - -Wall + - -Wcompat + - -fno-ignore-asserts + when: + - condition: os(windows) + then: + ghc-options: -static + else: + when: + - condition: flag(dynamic) + then: + ghc-options: -dynamic + else: + ghc-options: -static + dependencies: + - concordium-consensus + - concordium-base + database-exporter: main: Main.hs diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs index 4485d80cc7..dafc5078bd 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs @@ -49,6 +49,7 @@ import Concordium.KonsensusV1.TreeState.Implementation import qualified Concordium.KonsensusV1.TreeState.LowLevel as LowLevel import Concordium.KonsensusV1.TreeState.Types import Concordium.KonsensusV1.Types +import qualified Concordium.MerkleProofs as Merkle import Concordium.Scheduler (FilteredTransactions (..)) import Concordium.TimerMonad import Concordium.Types.BakerIdentity @@ -120,6 +121,10 @@ uponReceivingBlock :: PendingBlock -> m BlockResult uponReceivingBlock pendingBlock = do + mp <- Merkle.buildMerkleProof (const False) (sbBlock (pbBlock pendingBlock)) + logEvent Konsensus LLTrace $ show mp + let mr = Merkle.parseMerkleProof (snd Merkle.blockSchema) (fst Merkle.blockSchema) mp + logEvent Konsensus LLTrace $ show mr isShutdown <- use isConsensusShutdown if isShutdown then return BlockResultConsensusShutdown diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs index dd4bb7806d..47a37aaf53 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs @@ -28,6 +28,7 @@ import qualified Concordium.Crypto.VRF as VRF import Concordium.Genesis.Data (Regenesis, firstGenesisBlockHash, regenesisBlockHash, regenesisCoreParametersV1) import Concordium.Genesis.Data.BaseV1 import qualified Concordium.GlobalState.Basic.BlockState.LFMBTree as LFMBT +import qualified Concordium.MerkleProofs as Merkle import Concordium.Types import Concordium.Types.HashableTo import Concordium.Types.Parameters (IsConsensusV1) @@ -334,6 +335,9 @@ instance Serialize QuorumCertificate where instance HashableTo Hash.Hash QuorumCertificate where getHash = Hash.hash . encode +instance (Monad m) => Merkle.MerkleProvable m QuorumCertificate where + buildMerkleProof _ qc = return [Merkle.RawData $ encode qc] + -- | Check that the quorum certificate has: -- -- * Valid signatures from members of the finalization committee. @@ -456,6 +460,10 @@ instance HashableTo Hash.Hash (Option FinalizationEntry) where putWord8 1 put fe +instance (Monad m) => Merkle.MerkleProvable m (Option FinalizationEntry) where + buildMerkleProof _ Absent = return [Merkle.RawData (encode (0 :: Word8))] + buildMerkleProof _ (Present fe) = return [Merkle.RawData . runPut $ putWord8 1 >> put fe] + -- | Check that a finalization entry is valid. This checks the validity of the two quorum -- certificates. Note that the structural invariants on 'FinalizationEntry' enforce the other -- conditions in the definition of validity. @@ -609,6 +617,10 @@ instance HashableTo Hash.Hash (Option TimeoutCertificate) where putWord8 1 put tc +instance (Monad m) => Merkle.MerkleProvable m (Option TimeoutCertificate) where + buildMerkleProof _ Absent = return [Merkle.RawData (encode (0 :: Word8))] + buildMerkleProof _ (Present tc) = return [Merkle.RawData . runPut $ putWord8 1 >> put tc] + -- | Check that the signature on a timeout certificate is correct and that it contains a sufficient -- weight of signatures with respect to the finalization committee of a given epoch (the epoch of -- the quorum certificate that the timeout certificate should be valid with respect to). @@ -1217,6 +1229,42 @@ computeBlockHash bhh bqh = instance HashableTo BlockHash BakedBlock where getHash bb = computeBlockHash (getHash bb) (getHash bb) +instance (Monad m) => Merkle.MerkleProvable m BakedBlock where + buildMerkleProof open BakedBlock{..} = do + let blockHeader = optProof ["header"] . rawMerkle . runPut $ do + put bbRound + put bbEpoch + put (qcBlock bbQuorumCertificate) + let timestampBaker = optProof ["quasi", "meta", "bakerInfo", "timestampBaker"] . rawMerkle . runPut $ do + put bbTimestamp + put bbBaker + let nonce = optProof ["quasi", "meta", "bakerInfo", "nonce"] . rawMerkle . encode $ bbNonce + let bakerInfo = optProof ["quasi", "meta", "bakerInfo"] [timestampBaker, nonce] + let qcPath = ["quasi", "meta", "certificatesHash", "quorumCertificate"] + qcMerkleProof <- Merkle.buildMerkleProof (open . (qcPath ++)) bbQuorumCertificate + let qcHash = optProof qcPath qcMerkleProof + let tfPath = ["quasi", "meta", "certificatesHash", "timeoutFinalization"] + let tcPath = tfPath ++ ["timeoutCertificate"] + tcMerkleProof <- Merkle.buildMerkleProof (open . (tcPath ++)) bbTimeoutCertificate + let tcHash = optProof tcPath tcMerkleProof + let efePath = tfPath ++ ["epochFinalizationEntry"] + efeMerkleProof <- Merkle.buildMerkleProof (open . (efePath ++)) bbEpochFinalizationEntry + let efeHash = optProof efePath efeMerkleProof + let tfHash = optProof tfPath [tcHash, efeHash] + let certificatesHash = optProof ["quasi", "meta", "certificatesHash"] [qcHash, tfHash] + let blockMeta = optProof ["quasi", "meta"] [bakerInfo, certificatesHash] + let transactionsAndOutcomes = optProof ["quasi", "data", "transactionsAndOutcomes"] . rawMerkle . runPut $ do + put $ computeTransactionsHash bbTransactions + put bbTransactionOutcomesHash + let blockData = optProof ["quasi", "data"] [transactionsAndOutcomes, Merkle.RawData (encode bbStateHash)] + let blockQuasi = optProof ["quasi"] [blockMeta, blockData] + return [blockHeader, blockQuasi] + where + rawMerkle = (: []) . Merkle.RawData + optProof path proof + | open path = Merkle.SubProof proof + | otherwise = Merkle.RawData . Hash.hashToByteString . Merkle.toRootHash $ proof + -- | Configuration information stored for the genesis block. data GenesisMetadata = GenesisMetadata { -- | Core genesis parameters. From ab2f3fdbda074765517555e89ebe660e76853224 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Fri, 24 Nov 2023 10:11:18 +0100 Subject: [PATCH 02/20] Update base --- concordium-base | 2 +- .../src/Concordium/KonsensusV1/Consensus/Blocks.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/concordium-base b/concordium-base index b57f5b9c4a..288f27959b 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit b57f5b9c4af71333f49dc1a17cf0423f0a596c44 +Subproject commit 288f27959b98862ca8e5e2b8a574fb7fe1315da7 diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs index dafc5078bd..a3161ad7d9 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs @@ -121,7 +121,7 @@ uponReceivingBlock :: PendingBlock -> m BlockResult uponReceivingBlock pendingBlock = do - mp <- Merkle.buildMerkleProof (const False) (sbBlock (pbBlock pendingBlock)) + mp <- Merkle.buildMerkleProof (const True) (sbBlock (pbBlock pendingBlock)) logEvent Konsensus LLTrace $ show mp let mr = Merkle.parseMerkleProof (snd Merkle.blockSchema) (fst Merkle.blockSchema) mp logEvent Konsensus LLTrace $ show mr From 28a4650fba91489fe3d0a4285289917ea6c4c7f8 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Fri, 1 Dec 2023 13:51:54 +0100 Subject: [PATCH 03/20] Versioning LFMBTree hashing. --- concordium-base | 2 +- .../GlobalState/Basic/BlockState/LFMBTree.hs | 125 ++++++++++++-- .../Basic/BlockState/PoolRewards.hs | 85 +++++----- .../src/Concordium/GlobalState/Block.hs | 7 +- .../src/Concordium/GlobalState/BlockState.hs | 19 ++- .../GlobalState/CapitalDistribution.hs | 57 +++++-- .../GlobalState/Persistent/Accounts.hs | 10 +- .../GlobalState/Persistent/BlobStore.hs | 10 +- .../GlobalState/Persistent/BlockState.hs | 155 +++++++++++++----- .../Persistent/BlockState/Modules.hs | 8 +- .../GlobalState/Persistent/Genesis.hs | 2 +- .../GlobalState/Persistent/LFMBTree.hs | 53 +++++- .../GlobalState/Persistent/PoolRewards.hs | 93 +++++++---- .../src/Concordium/GlobalState/Rewards.hs | 19 ++- .../src/Concordium/GlobalState/TreeState.hs | 1 + .../src/Concordium/KonsensusV1/Consensus.hs | 2 +- .../KonsensusV1/Consensus/Blocks.hs | 35 ++-- .../KonsensusV1/Consensus/CatchUp.hs | 2 +- .../KonsensusV1/Consensus/Finality.hs | 3 + .../KonsensusV1/Consensus/Quorum.hs | 1 + .../src/Concordium/KonsensusV1/Flag.hs | 38 ++--- .../src/Concordium/KonsensusV1/SkovMonad.hs | 2 +- .../src/Concordium/KonsensusV1/TestMonad.hs | 2 +- .../Concordium/KonsensusV1/Transactions.hs | 2 +- .../KonsensusV1/TreeState/Implementation.hs | 2 +- .../KonsensusV1/TreeState/LowLevel.hs | 2 +- .../KonsensusV1/TreeState/LowLevel/LMDB.hs | 4 +- .../Concordium/KonsensusV1/TreeState/Types.hs | 24 +-- .../src/Concordium/KonsensusV1/Types.hs | 103 +++++++----- .../src/Concordium/MultiVersion.hs | 4 +- .../src/Concordium/Queries.hs | 2 +- .../EndToEnd/CredentialDeploymentTests.hs | 14 +- .../TransactionTableIntegrationTest.hs | 8 +- .../ConcordiumTests/KonsensusV1/CatchUp.hs | 6 +- .../ConcordiumTests/KonsensusV1/Common.hs | 19 ++- .../KonsensusV1/Consensus/Blocks.hs | 76 +++++---- .../ConcordiumTests/KonsensusV1/LMDB.hs | 5 +- .../ConcordiumTests/KonsensusV1/Quorum.hs | 2 +- .../ConcordiumTests/KonsensusV1/Timeout.hs | 2 +- .../KonsensusV1/TransactionProcessingTest.hs | 3 +- .../KonsensusV1/TreeStateTest.hs | 34 ++-- .../ConcordiumTests/KonsensusV1/Types.hs | 24 ++- .../tests/consensus/ConcordiumTests/Update.hs | 2 +- .../globalstate/GlobalStateTests/LFMBTree.hs | 19 ++- .../tools/database-exporter/Main.hs | 9 +- concordium-node/Cargo.lock | 2 +- 46 files changed, 736 insertions(+), 363 deletions(-) diff --git a/concordium-base b/concordium-base index 5a8d9bf280..569b37dcd3 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit 5a8d9bf280061555480e014010df35827d5fe3be +Subproject commit 569b37dcd3c43b787103dee97fca7e52a13ed1ba diff --git a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs index 3e321f7cff..a3a5eafa6c 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs @@ -1,5 +1,8 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} @@ -13,6 +16,12 @@ module Concordium.GlobalState.Basic.BlockState.LFMBTree ( LFMBTree, size, + -- * Hash types + LFMBTreeHash' (..), + LFMBTreeHash, + LFMBTreeHashV0, + LFMBTreeHashV1, + -- * Construction empty, @@ -31,8 +40,11 @@ module Concordium.GlobalState.Basic.BlockState.LFMBTree ( toAscList, toAscPairList, fromListChoosingFirst, - hashFromFoldable, - hashAsLFMBT, + hashP1FromFoldable, + hashAsLFMBTV0, + hashAsLFMBTV1, + lfmbtHash, + lfmbtHash', -- * Specialized functions for @Maybe@ lookupMaybe, @@ -46,6 +58,9 @@ module Concordium.GlobalState.Basic.BlockState.LFMBTree ( -- * Helpers setBits, + -- * Auxiliary definitions + emptyTreeHash, + -- * Structure specification -- $specification ) @@ -57,12 +72,16 @@ import Control.Monad (join) import Data.Bits import Data.Coerce (Coercible, coerce) import Data.Foldable (foldl', toList) +import Data.Hashable (Hashable) import Data.Maybe (fromJust) +import qualified Data.Serialize as S import Data.Word import Lens.Micro ((<&>)) import Lens.Micro.Internal (Index, IxValue, Ixed (..)) import Prelude hiding (lookup) +import Concordium.Types + {- ------------------------------------------------------------------------------- Helpers @@ -100,6 +119,27 @@ data T v | Leaf !v deriving (Eq, Show) +-- | Hash representation type for an LFMBTree. This is parametrised by the block hash version. +-- +-- * At 'BlockHashVersion0', the tree is hashed as a Merkle tree where each leaf is a hash +-- (defined by the hashing scheme for the values) and inner nodes are hashed as the hash of +-- the concatenation of the hashes of the two children. The empty tree is represented as the +-- hash of the string "EmptyLFMBTree". +-- +-- * At 'BlockHashVersion1', the tree hash is defined as the hash of the concatenation of the +-- size (as a 64-bit big-endian) and the 'BlockHashVersion0' hash of the tree. +newtype LFMBTreeHash' (bhv :: BlockHashVersion) = LFMBTreeHash {theLFMBTreeHash :: H.Hash} + deriving newtype (Eq, Ord, Show, Read, Hashable, S.Serialize) + +-- | Hash of an LFMBTree parametrised by the protocol version. +type LFMBTreeHash (pv :: ProtocolVersion) = LFMBTreeHash' (BlockHashVersionFor pv) + +-- | Alias for version 0 hash of an LFMBTree. +type LFMBTreeHashV0 = LFMBTreeHash' 'BlockHashVersion0 + +-- | Alias for version 1 hash of an LFMBTree. +type LFMBTreeHashV1 = LFMBTreeHash' 'BlockHashVersion1 + {- ------------------------------------------------------------------------------- Instances @@ -110,11 +150,28 @@ instance (HashableTo H.Hash v) => HashableTo H.Hash (T v) where getHash (Leaf v) = getHash v getHash (Node _ l r) = H.hashOfHashes (getHash l) (getHash r) --- | The hash of a LFMBTree is defined as the hash of the string "EmptyLFMBTree" if it +-- | The hash used to represent an empty LFMBTree. Defined as the hash of the +-- string "EmptyLFMBTree". +emptyTreeHash :: LFMBTreeHashV0 +emptyTreeHash = LFMBTreeHash $ H.hash "EmptyLFMBTree" + +-- | The (P1) hash of an LFMBTree is defined as the hash of the string "EmptyLFMBTree" if it -- is empty or the hash of the tree otherwise. -instance (HashableTo H.Hash v) => HashableTo H.Hash (LFMBTree k v) where - getHash Empty = H.hash "EmptyLFMBTree" - getHash (NonEmpty _ v) = getHash v +instance (HashableTo H.Hash v) => HashableTo LFMBTreeHashV0 (LFMBTree k v) where + getHash Empty = emptyTreeHash + getHash (NonEmpty _ v) = LFMBTreeHash $ getHash v + +-- | The P7 hash of an LFMBTree is the hash of concatenation of the size of the tree (Word64, +-- big-endian) and the P1 hash of the LFMBTree. This hash is more suitable for Merkle proofs, +-- since the size is encoded. +instance (HashableTo H.Hash v) => HashableTo LFMBTreeHashV1 (LFMBTree k v) where + getHash t = LFMBTreeHash $ H.hashLazy $ S.runPutLazy $ do + S.putWord64be sz + S.put $ getHash @(LFMBTreeHashV0) t + where + sz = case t of + Empty -> 0 + NonEmpty s _ -> s type instance Index (LFMBTree k v) = k type instance IxValue (LFMBTree k v) = v @@ -284,21 +341,21 @@ fromAscListMaybes l = fromList $ go l 0 -- | Get the hash of an LFMBTree constructed from a 'Foldable' by inserting each item sequentially -- from index 0. --- prop> hashFromFoldable l == getHash (fromFoldable @Word64 l) +-- prop> hashP1FromFoldable l == getHash (fromFoldable @Word64 l) -- -- TODO: Optimise this implementation. -hashFromFoldable :: (Foldable f, HashableTo H.Hash v) => f v -> H.Hash -hashFromFoldable = getHash . fromFoldable @Word64 +hashP1FromFoldable :: (Foldable f, HashableTo H.Hash v) => f v -> LFMBTreeHashV0 +hashP1FromFoldable = getHash . fromFoldable @Word64 -- | Hash a list of hashes in the LFMBTree format, using the specified hash for the empty tree. -- This avoids building the full tree. -hashAsLFMBT :: +hashAsLFMBTV0 :: -- | Hash to use for empty list H.Hash -> -- | List of hashes to construct into Merkle tree [H.Hash] -> H.Hash -hashAsLFMBT e = go +hashAsLFMBTV0 e = go where go [] = e go [x] = x @@ -306,6 +363,52 @@ hashAsLFMBT e = go f (x : y : xs) = H.hashOfHashes x y : f xs f other = other +-- | Get the hash of an LFMBTree constructed from a 'Foldable'. +-- +-- prop> lfmbtV0Hash l == getHash (fromFoldable @Word64 l) +lfmbtV0Hash :: (Foldable f, HashableTo H.Hash v) => f v -> LFMBTreeHashV0 +{-# INLINE lfmbtV0Hash #-} +lfmbtV0Hash = lfmbtV0Hash' getHash + +-- | Get the hash of an LFMBTree constructed from a 'Foldable' by applying the given hashing +-- function to each element. +lfmbtV0Hash' :: (Foldable f) => (v -> H.Hash) -> f v -> LFMBTreeHashV0 +{-# INLINE lfmbtV0Hash' #-} +lfmbtV0Hash' hsh = LFMBTreeHash . hashAsLFMBTV0 (theLFMBTreeHash emptyTreeHash) . map hsh . toList + +hashAsLFMBTV1 :: H.Hash -> [H.Hash] -> H.Hash +hashAsLFMBTV1 e l = H.hashLazy $! S.runPutLazy $ do + S.putWord64be (fromIntegral $ length l) + S.put $ hashAsLFMBTV0 e l + +-- | Get the hash of an LFMBTree constructed from a 'Foldable'. +-- +-- prop> lfmbtV0Hash l == getHash (fromFoldable @Word64 l) +lfmbtV1Hash :: (Foldable f, HashableTo H.Hash v) => f v -> LFMBTreeHashV1 +{-# INLINE lfmbtV1Hash #-} +lfmbtV1Hash = lfmbtV1Hash' getHash + +-- | Get the hash of an LFMBTree constructed from a 'Foldable' by applying the given hashing +-- function to each element. +lfmbtV1Hash' :: (Foldable f) => (v -> H.Hash) -> f v -> LFMBTreeHashV1 +{-# INLINE lfmbtV1Hash' #-} +lfmbtV1Hash' hsh = LFMBTreeHash . hashAsLFMBTV1 (theLFMBTreeHash emptyTreeHash) . map hsh . toList + +-- | Get the hash of an LFMBTree constructed from a 'Foldable'. +lfmbtHash :: (Foldable f, HashableTo H.Hash v) => SBlockHashVersion bhv -> f v -> LFMBTreeHash' bhv +{-# INLINE lfmbtHash #-} +lfmbtHash sbhv = case sbhv of + SBlockHashVersion0 -> lfmbtV0Hash + SBlockHashVersion1 -> lfmbtV1Hash + +-- | Get the hash of an LFMBTree constructed from a 'Foldable' by applying the given hashing +-- function to each element. +lfmbtHash' :: (Foldable f) => SBlockHashVersion bhv -> (v -> H.Hash) -> f v -> LFMBTreeHash' bhv +{-# INLINE lfmbtHash' #-} +lfmbtHash' sbhv = case sbhv of + SBlockHashVersion0 -> lfmbtV0Hash' + SBlockHashVersion1 -> lfmbtV1Hash' + {- ------------------------------------------------------------------------------- Specification diff --git a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs index 62525285d4..58ca4c4241 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs @@ -1,11 +1,15 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} module Concordium.GlobalState.Basic.BlockState.PoolRewards where import Control.Exception -import Control.Monad import qualified Data.Map.Strict as Map import Data.Serialize +import Data.Singletons import qualified Data.Vector as Vec import Data.Word import Lens.Micro.Platform @@ -57,16 +61,16 @@ instance (Monad m) => MHashableTo m Hash.Hash BakerPoolRewardDetails -- | Details of rewards accruing over the course of a reward period, and details about the capital -- distribution for this reward period and (possibly) the next. -data PoolRewards = PoolRewards +data PoolRewards (bhv :: BlockHashVersion) = PoolRewards { -- | The capital distribution for the next reward period. -- This is updated the epoch before a payday. - nextCapital :: !(Hashed CapitalDistribution), + nextCapital :: !(Hashed' (CapitalDistributionHash' bhv) CapitalDistribution), -- | The capital distribution for the current reward period. - currentCapital :: !(Hashed CapitalDistribution), + currentCapital :: !(Hashed' (CapitalDistributionHash' bhv) CapitalDistribution), -- | The details of rewards accruing to baker pools. -- These are indexed by the index of the baker in the capital distribution (_not_ the BakerId). -- There must be an entry for each baker in 'currentCapital'. - bakerPoolRewardDetails :: !(LFMBT.LFMBTree Word64 BakerPoolRewardDetails), + bakerPoolRewardDetails :: !(Vec.Vector BakerPoolRewardDetails), -- | The transaction reward amount accruing to the passive delegators. passiveDelegationTransactionRewards :: !Amount, -- | The transaction reward fraction accruing to the foundation. @@ -79,7 +83,7 @@ data PoolRewards = PoolRewards deriving (Show) -- | Traversal for accessing the reward details for a particular baker ID. -rewardDetails :: BakerId -> Traversal' PoolRewards BakerPoolRewardDetails +rewardDetails :: BakerId -> Traversal' (PoolRewards bhv) BakerPoolRewardDetails rewardDetails bid f pr | Just (index, _) <- mindex = (\bprd -> pr{bakerPoolRewardDetails = bprd}) @@ -89,32 +93,35 @@ rewardDetails bid f pr mindex = binarySearchI bcBakerId (bakerPoolCapital $ _unhashed $ currentCapital pr) bid -- | Look up the baker capital and reward details for a baker ID. -lookupBakerCapitalAndRewardDetails :: BakerId -> PoolRewards -> Maybe (BakerCapital, BakerPoolRewardDetails) +lookupBakerCapitalAndRewardDetails :: BakerId -> PoolRewards bhv -> Maybe (BakerCapital, BakerPoolRewardDetails) lookupBakerCapitalAndRewardDetails bid PoolRewards{..} = do (index, capital) <- binarySearchI bcBakerId (bakerPoolCapital $ _unhashed currentCapital) bid rds <- bakerPoolRewardDetails ^? ix (fromIntegral index) return (capital, rds) -instance HashableTo PoolRewardsHash PoolRewards where +instance (IsBlockHashVersion bhv) => HashableTo (PoolRewardsHash bhv) (PoolRewards bhv) where getHash PoolRewards{..} = - PoolRewardsHash . Hash.hashOfHashes (getHash nextCapital) $ - Hash.hashOfHashes (getHash currentCapital) $ - Hash.hashOfHashes (getHash bakerPoolRewardDetails) $ + PoolRewardsHash . Hash.hashOfHashes (getCDHash nextCapital) $ + Hash.hashOfHashes (getCDHash currentCapital) $ + Hash.hashOfHashes prdHash $ getHash $ runPut $ put passiveDelegationTransactionRewards <> put foundationTransactionRewards <> put nextPaydayEpoch <> put nextPaydayMintRate + where + getCDHash = theCapitalDistributionHash @bhv . getHash + prdHash = LFMBT.theLFMBTreeHash $ LFMBT.lfmbtHash' (sing @bhv) getHash bakerPoolRewardDetails -- | The empty 'PoolRewards', where there are no bakers, delegators or rewards. -- This is generally not used except as a dummy value for testing. -emptyPoolRewards :: PoolRewards +emptyPoolRewards :: (IsBlockHashVersion bhv) => PoolRewards bhv emptyPoolRewards = PoolRewards { nextCapital = makeHashed emptyCapitalDistribution, currentCapital = makeHashed emptyCapitalDistribution, - bakerPoolRewardDetails = LFMBT.empty, + bakerPoolRewardDetails = Vec.empty, passiveDelegationTransactionRewards = 0, foundationTransactionRewards = 0, nextPaydayEpoch = 0, @@ -124,14 +131,15 @@ emptyPoolRewards = -- | A 'Putter' for 'PoolRewards'. -- The 'bakerPoolRewardDetails' is serialized as a flat list, with the length implied by the -- length of @bakerPoolCapital (_unhashed currentCapital)@. -putPoolRewards :: Putter PoolRewards +putPoolRewards :: Putter (PoolRewards bhv) putPoolRewards PoolRewards{..} = do put (_unhashed nextCapital) put (_unhashed currentCapital) - let bprdList = LFMBT.toAscList bakerPoolRewardDetails - assert (Vec.length (bakerPoolCapital (_unhashed currentCapital)) == length bprdList) $ - mapM_ put $ - LFMBT.toAscList bakerPoolRewardDetails + assert + ( Vec.length (bakerPoolCapital (_unhashed currentCapital)) + == Vec.length bakerPoolRewardDetails + ) + $ mapM_ put bakerPoolRewardDetails put passiveDelegationTransactionRewards put foundationTransactionRewards put nextPaydayEpoch @@ -140,15 +148,14 @@ putPoolRewards PoolRewards{..} = do -- | Deserialize 'PoolRewards'. -- The 'bakerPoolRewardDetails' is serialized as a flat list, with the length implied by the -- length of @bakerPoolCapital (_unhashed currentCapital)@. -getPoolRewards :: Get PoolRewards +getPoolRewards :: (IsBlockHashVersion bhv) => Get (PoolRewards bhv) getPoolRewards = do nextCapital <- makeHashed <$> get currentCapital <- makeHashed <$> get bakerPoolRewardDetails <- - LFMBT.fromList - <$> replicateM - (Vec.length (bakerPoolCapital (_unhashed currentCapital))) - get + Vec.replicateM + (Vec.length (bakerPoolCapital (_unhashed currentCapital))) + get passiveDelegationTransactionRewards <- get foundationTransactionRewards <- get nextPaydayEpoch <- get @@ -156,39 +163,40 @@ getPoolRewards = do return PoolRewards{..} -- | List of baker and number of blocks baked by this baker in the reward period. -bakerBlockCounts :: PoolRewards -> [(BakerId, Word64)] +bakerBlockCounts :: PoolRewards bhv -> [(BakerId, Word64)] bakerBlockCounts PoolRewards{..} = zipWith bc (Vec.toList (bakerPoolCapital (_unhashed currentCapital))) - (LFMBT.toAscPairList bakerPoolRewardDetails) + (Vec.toList bakerPoolRewardDetails) where - bc BakerCapital{..} (_, BakerPoolRewardDetails{..}) = (bcBakerId, blockCount) + bc BakerCapital{..} BakerPoolRewardDetails{..} = (bcBakerId, blockCount) -- | Rotate the capital distribution, so that the current capital distribution is replaced by the -- next one, and set up empty pool rewards. -rotateCapitalDistribution :: PoolRewards -> PoolRewards +rotateCapitalDistribution :: PoolRewards bhv -> PoolRewards bhv rotateCapitalDistribution pr = pr { currentCapital = nextCapital pr, bakerPoolRewardDetails = - LFMBT.fromList $ - replicate - (Vec.length (bakerPoolCapital (_unhashed (nextCapital pr)))) - emptyBakerPoolRewardDetails + Vec.replicate + (Vec.length (bakerPoolCapital (_unhashed (nextCapital pr)))) + emptyBakerPoolRewardDetails } -- | Set the next 'CapitalDistribution'. setNextCapitalDistribution :: + (IsBlockHashVersion bhv) => CapitalDistribution -> - PoolRewards -> - PoolRewards + PoolRewards bhv -> + PoolRewards bhv setNextCapitalDistribution cd pr = pr{nextCapital = makeHashed cd} -- | Construct 'PoolRewards' for migrating from 'P3' to 'P4'. -- This is used to construct the state of the genesis block. makePoolRewardsForMigration :: + (IsBlockHashVersion bhv) => -- | Current epoch bakers and stakes, in ascending order of 'BakerId'. Vec.Vector (BakerId, Amount) -> -- | Next epoch bakers and stakes, in ascending order of 'BakerId'. @@ -199,12 +207,12 @@ makePoolRewardsForMigration :: Epoch -> -- | Mint rate for the next payday MintRate -> - PoolRewards + PoolRewards bhv makePoolRewardsForMigration curBakers nextBakers bakedBlocks npEpoch npMintRate = PoolRewards { nextCapital = makeCD nextBakers, currentCapital = makeCD curBakers, - bakerPoolRewardDetails = LFMBT.fromFoldable (makePRD <$> curBakers), + bakerPoolRewardDetails = makePRD <$> curBakers, passiveDelegationTransactionRewards = 0, foundationTransactionRewards = 0, nextPaydayEpoch = npEpoch, @@ -228,13 +236,14 @@ makePoolRewardsForMigration curBakers nextBakers bakedBlocks npEpoch npMintRate -- | Make initial pool rewards for a genesis block state. makeInitialPoolRewards :: + (IsBlockHashVersion bhv) => -- | Genesis capital distribution CapitalDistribution -> -- | Epoch of next payday Epoch -> -- | Mint rate MintRate -> - PoolRewards + PoolRewards bhv makeInitialPoolRewards cdist npEpoch npMintRate = PoolRewards { nextCapital = initCD, @@ -247,9 +256,9 @@ makeInitialPoolRewards cdist npEpoch npMintRate = } where initCD = makeHashed cdist - bprd = LFMBT.fromList (replicate (length (bakerPoolCapital cdist)) emptyBakerPoolRewardDetails) + bprd = Vec.replicate (length (bakerPoolCapital cdist)) emptyBakerPoolRewardDetails -- | The total capital passively delegated in the current reward period's capital distribution. -currentPassiveDelegationCapital :: PoolRewards -> Amount +currentPassiveDelegationCapital :: PoolRewards bhv -> Amount currentPassiveDelegationCapital = Vec.sum . fmap dcDelegatorCapital . passiveDelegatorsCapital . _unhashed . currentCapital diff --git a/concordium-consensus/src/Concordium/GlobalState/Block.hs b/concordium-consensus/src/Concordium/GlobalState/Block.hs index af5a120a8a..b3d9f83e54 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Block.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Block.hs @@ -23,6 +23,7 @@ import Concordium.Crypto.SHA256 as Hash import Concordium.GlobalState.Parameters import Concordium.Types import Concordium.Types.HashableTo +import Concordium.Types.TransactionOutcomes import Concordium.Types.Transactions import Concordium.GlobalState.Finalization @@ -334,9 +335,9 @@ instance forall pv. (IsProtocolVersion pv) => BlockData (Block pv) where -- move into gendata? blockTransactionOutcomesHash GenesisBlock{} = - case transactionOutcomesVersion @(TransactionOutcomesVersionFor pv) of - STOV0 -> getHash emptyTransactionOutcomesV0 - STOV1 -> emptyTransactionOutcomesHashV1 + toTransactionOutcomesHash $ + emptyTransactionOutcomesHashV $ + transactionOutcomesVersion @(TransactionOutcomesVersionFor pv) blockTransactionOutcomesHash (NormalBlock bb) = blockTransactionOutcomesHash bb blockSignature GenesisBlock{} = Nothing diff --git a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs index f540ddefcd..b76a3087ae 100644 --- a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs @@ -97,11 +97,20 @@ import Concordium.GlobalState.TransactionTable (TransactionTable) import Concordium.ID.Parameters (GlobalContext) import Concordium.ID.Types (AccountCredential) import qualified Concordium.ID.Types as ID +import Concordium.Types.TransactionOutcomes -- | Hash associated with birk parameters. newtype BirkParametersHash (pv :: ProtocolVersion) = BirkParametersHash {birkParamHash :: H.Hash} deriving newtype (Eq, Ord, Show, Serialize) +-- | Hash associated with the accounts table. +newtype AccountsHash (pv :: ProtocolVersion) = AccountsHash {theAccountsHash :: H.Hash} + deriving newtype (Eq, Ord, Show, Serialize) + +-- | Hash associated with the modules table. +newtype ModulesHash (pv :: ProtocolVersion) = ModulesHash {theModulesHash :: H.Hash} + deriving newtype (Eq, Ord, Show, Serialize) + -- | The hashes of the block state components, which are combined -- to produce a 'StateHash'. data BlockStateHashInputs (pv :: ProtocolVersion) = BlockStateHashInputs @@ -109,12 +118,12 @@ data BlockStateHashInputs (pv :: ProtocolVersion) = BlockStateHashInputs bshCryptographicParameters :: H.Hash, bshIdentityProviders :: H.Hash, bshAnonymityRevokers :: H.Hash, - bshModules :: H.Hash, + bshModules :: ModulesHash pv, bshBankStatus :: H.Hash, - bshAccounts :: H.Hash, + bshAccounts :: AccountsHash pv, bshInstances :: H.Hash, bshUpdates :: H.Hash, - bshBlockRewardDetails :: BlockRewardDetailsHash (AccountVersionFor pv) + bshBlockRewardDetails :: BlockRewardDetailsHash pv } deriving (Show) @@ -129,8 +138,8 @@ makeBlockStateHash BlockStateHashInputs{..} = (H.hashOfHashes bshIdentityProviders bshAnonymityRevokers) ) ( H.hashOfHashes - (H.hashOfHashes bshModules bshBankStatus) - (H.hashOfHashes bshAccounts bshInstances) + (H.hashOfHashes (theModulesHash bshModules) bshBankStatus) + (H.hashOfHashes (theAccountsHash bshAccounts) bshInstances) ) ) ( H.hashOfHashes diff --git a/concordium-consensus/src/Concordium/GlobalState/CapitalDistribution.hs b/concordium-consensus/src/Concordium/GlobalState/CapitalDistribution.hs index fbb37547dc..4a95aca2d8 100644 --- a/concordium-consensus/src/Concordium/GlobalState/CapitalDistribution.hs +++ b/concordium-consensus/src/Concordium/GlobalState/CapitalDistribution.hs @@ -1,3 +1,9 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + -- | This module contains common types for representing the capital distribution of bakers and -- delegators. A snapshot of the capital distribution is taken when the bakers for a payday are -- calculated. This is then used to determine how rewards are distributed among the bakers and @@ -5,6 +11,7 @@ module Concordium.GlobalState.CapitalDistribution where import Data.Serialize +import Data.Singletons import qualified Data.Vector as Vec import qualified Concordium.Crypto.SHA256 as Hash @@ -61,14 +68,21 @@ instance Serialize BakerCapital where bcDelegatorCapital <- flip Vec.generateM (const get) =<< getLength return BakerCapital{..} -instance HashableTo Hash.Hash BakerCapital where - getHash BakerCapital{..} = Hash.hash $ - runPut $ do - put bcBakerId - put bcBakerEquityCapital - put $ LFMBT.hashFromFoldable bcDelegatorCapital +newtype BakerCapitalHash' (bhv :: BlockHashVersion) = BakerCapitalHash + { theBakerCapitalHash :: Hash.Hash + } + deriving newtype (Eq, Ord, Show, Read, Serialize) + +type BakerCapitalHash (pv :: ProtocolVersion) = + BakerCapitalHash' (BlockHashVersionFor pv) + +instance (IsBlockHashVersion bhv) => HashableTo (BakerCapitalHash' bhv) BakerCapital where + getHash BakerCapital{..} = BakerCapitalHash . Hash.hash . runPut $ do + put bcBakerId + put bcBakerEquityCapital + put $ LFMBT.lfmbtHash (sing @bhv) bcDelegatorCapital -instance (Monad m) => MHashableTo m Hash.Hash BakerCapital +instance (IsBlockHashVersion bhv, Monad m) => MHashableTo m (BakerCapitalHash' bhv) BakerCapital -- | The total capital delegated to the baker. bcTotalDelegatorCapital :: BakerCapital -> Amount @@ -93,13 +107,30 @@ instance Serialize CapitalDistribution where passiveDelegatorsCapital <- flip Vec.generateM (const get) =<< getLength return CapitalDistribution{..} -instance HashableTo Hash.Hash CapitalDistribution where - getHash CapitalDistribution{..} = - Hash.hashOfHashes - (LFMBT.hashFromFoldable bakerPoolCapital) - (LFMBT.hashFromFoldable passiveDelegatorsCapital) +newtype CapitalDistributionHash' (bhv :: BlockHashVersion) = CapitalDistributionHash + { theCapitalDistributionHash :: Hash.Hash + } + deriving newtype (Eq, Ord, Show, Read, Serialize) -instance (Monad m) => MHashableTo m Hash.Hash CapitalDistribution +type CapitalDistributionHash (pv :: ProtocolVersion) = + CapitalDistributionHash' (BlockHashVersionFor pv) + +instance + (IsBlockHashVersion bhv) => + HashableTo (CapitalDistributionHash' bhv) CapitalDistribution + where + getHash CapitalDistribution{..} = + CapitalDistributionHash $ + Hash.hashOfHashes + (lfmbtHash (theBakerCapitalHash @bhv . getHash) bakerPoolCapital) + (lfmbtHash getHash passiveDelegatorsCapital) + where + lfmbtHash :: (a -> Hash.Hash) -> Vec.Vector a -> Hash.Hash + lfmbtHash gHash = LFMBT.theLFMBTreeHash . LFMBT.lfmbtHash' (sing @bhv) gHash + +instance + (IsBlockHashVersion bhv, Monad m) => + MHashableTo m (CapitalDistributionHash' bhv) CapitalDistribution -- | The empty 'CapitalDistribution', with no bakers or passive delegators. emptyCapitalDistribution :: CapitalDistribution diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Accounts.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Accounts.hs index fac8808211..0440a1c204 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Accounts.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Accounts.hs @@ -61,16 +61,16 @@ -- for keeping track of non persisted accounts for supporting e.g. queries via the ‘AccountAddress'. module Concordium.GlobalState.Persistent.Accounts where -import qualified Concordium.Crypto.SHA256 as H import qualified Concordium.GlobalState.AccountMap as OldMap import qualified Concordium.GlobalState.AccountMap.DifferenceMap as DiffMap import qualified Concordium.GlobalState.AccountMap.LMDB as LMDBAccountMap +import Concordium.GlobalState.BlockState (AccountsHash (..)) import Concordium.GlobalState.Parameters import Concordium.GlobalState.Persistent.Account import Concordium.GlobalState.Persistent.BlobStore import Concordium.GlobalState.Persistent.Cache import Concordium.GlobalState.Persistent.CachedRef -import Concordium.GlobalState.Persistent.LFMBTree (LFMBTree') +import Concordium.GlobalState.Persistent.LFMBTree (LFMBTree', LFMBTreeHash, LFMBTreeHash' (..)) import qualified Concordium.GlobalState.Persistent.LFMBTree as L import qualified Concordium.GlobalState.Persistent.Trie as Trie import Concordium.ID.Parameters @@ -143,8 +143,10 @@ type SupportsPersistentAccount pv m = LMDBAccountMap.MonadAccountMapStore m ) -instance (SupportsPersistentAccount pv m) => MHashableTo m H.Hash (Accounts pv) where - getHashM Accounts{..} = getHashM accountTable +instance (SupportsPersistentAccount pv m) => MHashableTo m (AccountsHash pv) (Accounts pv) where + getHashM Accounts{..} = + AccountsHash . theLFMBTreeHash + <$> getHashM @m @(LFMBTreeHash pv) accountTable -- | Write accounts created for this block or any non-persisted parent block. -- Note that this also empties the difference map for this block. diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs index 953b4e216f..9d10724718 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs @@ -1621,10 +1621,10 @@ migrateHashedBufferedRefKeepHash hb = do -- already. The input reference is uncached, and the new references is flushed -- to disk, as well as cached in memory. migrateHashedBufferedRef :: - (MonadTrans t, MHashableTo (t m) h b, BlobStorable m a, BlobStorable (t m) b) => + (MonadTrans t, MHashableTo (t m) h2 b, BlobStorable m a, BlobStorable (t m) b) => (a -> t m b) -> - HashedBufferedRef' h a -> - t m (HashedBufferedRef' h b) + HashedBufferedRef' h1 a -> + t m (HashedBufferedRef' h2 b) migrateHashedBufferedRef f hb = do !newRef <- refMake =<< f =<< lift (refLoad (bufferedReference hb)) -- compute the hash while the data is in memory. @@ -1639,7 +1639,7 @@ migrateHashedBufferedRef f hb = do type HashedBufferedRef = HashedBufferedRef' H.Hash -- | Created a 'HashedBufferedRef' value from a 'Hashed' value, retaining the hash. -bufferHashed :: (MonadIO m) => Hashed a -> m (HashedBufferedRef a) +bufferHashed :: (MonadIO m) => Hashed' h a -> m (HashedBufferedRef' h a) bufferHashed (Hashed !val !h) = do br <- makeBufferedRef val hashRef <- liftIO $ newIORef (Some h) @@ -1662,7 +1662,7 @@ instance (DirectBlobStorable m a, MHashableTo m h a) => MHashableTo m h (HashedB return h Some h -> return h -instance (Show a) => Show (HashedBufferedRef a) where +instance (Show a) => Show (HashedBufferedRef' h a) where show ref = show (bufferedReference ref) instance (DirectBlobStorable m a) => BlobStorable m (HashedBufferedRef' h a) where diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs index 764bcf0cda..95da937c42 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs @@ -29,7 +29,8 @@ module Concordium.GlobalState.Persistent.BlockState ( emptyPersistentTransactionOutcomes, PersistentBlockStateContext (..), PersistentState, - BlockRewardDetails (..), + BlockRewardDetails' (..), + BlockRewardDetails, PersistentBlockStateMonad (..), withNewAccountCacheAndLMDBAccountMap, cacheState, @@ -81,6 +82,7 @@ import Concordium.Types.HashableTo import qualified Concordium.Types.IdentityProviders as IPS import Concordium.Types.Queries (CurrentPaydayBakerPoolStatus (..), PoolStatus (..), RewardStatus' (..), makePoolPendingChange) import Concordium.Types.SeedState +import qualified Concordium.Types.TransactionOutcomes as TransactionOutcomes import qualified Concordium.Types.Transactions as Transactions import qualified Concordium.Types.UpdateQueues as UQ import Concordium.Types.Updates @@ -557,9 +559,11 @@ putHashedEpochBlocksV0 HashedEpochBlocks{..} = do EpochBlock{..} <- refLoad ebref loadEB (s Seq.|> ebBakerId) ebPrevious -data BlockRewardDetails (av :: AccountVersion) where - BlockRewardDetailsV0 :: !HashedEpochBlocks -> BlockRewardDetails 'AccountV0 - BlockRewardDetailsV1 :: (AVSupportsDelegation av) => !(HashedBufferedRef' Rewards.PoolRewardsHash PoolRewards) -> BlockRewardDetails av +data BlockRewardDetails' (av :: AccountVersion) (bhv :: BlockHashVersion) where + BlockRewardDetailsV0 :: !HashedEpochBlocks -> BlockRewardDetails' 'AccountV0 bhv + BlockRewardDetailsV1 :: (AVSupportsDelegation av) => !(HashedBufferedRef' (Rewards.PoolRewardsHash bhv) (PoolRewards bhv)) -> BlockRewardDetails' av bhv + +type BlockRewardDetails pv = BlockRewardDetails' (AccountVersionFor pv) (BlockHashVersionFor pv) -- | Migrate the block reward details. -- When migrating to a 'P4' or later, this sets the 'nextPaydayEpoch' to the reward period length. @@ -578,8 +582,8 @@ migrateBlockRewardDetails :: OParam 'PTTimeParameters (ChainParametersVersionFor pv) TimeParameters -> -- | The epoch number before the protocol update. Epoch -> - BlockRewardDetails (AccountVersionFor oldpv) -> - t m (BlockRewardDetails (AccountVersionFor pv)) + BlockRewardDetails oldpv -> + t m (BlockRewardDetails pv) migrateBlockRewardDetails StateMigrationParametersTrivial _ _ tp oldEpoch = \case (BlockRewardDetailsV0 heb) -> BlockRewardDetailsV0 <$> migrateHashedEpochBlocks heb (BlockRewardDetailsV1 hbr) -> case tp of @@ -613,24 +617,30 @@ migrateBlockRewardDetails StateMigrationParametersP5ToP6{} _ _ (SomeParam TimePa migrateBlockRewardDetails StateMigrationParametersP6ToP7{} _ _ (SomeParam TimeParametersV1{..}) _ = \case (BlockRewardDetailsV1 hbr) -> BlockRewardDetailsV1 - <$> migrateHashedBufferedRef (migratePoolRewards (rewardPeriodEpochs _tpRewardPeriodLength)) hbr + <$> migrateHashedBufferedRef (migratePoolRewardsChangeHash (rewardPeriodEpochs _tpRewardPeriodLength)) hbr -instance (MonadBlobStore m) => MHashableTo m (Rewards.BlockRewardDetailsHash av) (BlockRewardDetails av) where +instance + (MonadBlobStore m, IsBlockHashVersion bhv) => + MHashableTo m (Rewards.BlockRewardDetailsHash' av bhv) (BlockRewardDetails' av bhv) + where getHashM (BlockRewardDetailsV0 heb) = return $ Rewards.BlockRewardDetailsHashV0 (getHash heb) getHashM (BlockRewardDetailsV1 pr) = Rewards.BlockRewardDetailsHashV1 <$> getHashM pr -instance (IsAccountVersion av, MonadBlobStore m) => BlobStorable m (BlockRewardDetails av) where +instance (IsAccountVersion av, MonadBlobStore m) => BlobStorable m (BlockRewardDetails' av bhv) where storeUpdate (BlockRewardDetailsV0 heb) = fmap (fmap BlockRewardDetailsV0) $ storeUpdate heb storeUpdate (BlockRewardDetailsV1 hpr) = fmap (fmap BlockRewardDetailsV1) $ storeUpdate hpr load = case delegationSupport @av of SAVDelegationNotSupported -> fmap (fmap BlockRewardDetailsV0) load SAVDelegationSupported -> fmap (fmap BlockRewardDetailsV1) load -instance (MonadBlobStore m) => Cacheable m (BlockRewardDetails av) where +instance (MonadBlobStore m, IsBlockHashVersion bhv) => Cacheable m (BlockRewardDetails' av bhv) where cache (BlockRewardDetailsV0 heb) = BlockRewardDetailsV0 <$> cache heb cache (BlockRewardDetailsV1 hpr) = BlockRewardDetailsV1 <$> cache hpr -putBlockRewardDetails :: (MonadBlobStore m, MonadPut m) => BlockRewardDetails av -> m () +putBlockRewardDetails :: + (MonadBlobStore m, MonadPut m, IsBlockHashVersion bhv) => + BlockRewardDetails' av bhv -> + m () putBlockRewardDetails (BlockRewardDetailsV0 heb) = putHashedEpochBlocksV0 heb putBlockRewardDetails (BlockRewardDetailsV1 hpr) = refLoad hpr >>= putPoolRewards @@ -638,16 +648,16 @@ putBlockRewardDetails (BlockRewardDetailsV1 hpr) = refLoad hpr >>= putPoolReward consBlockRewardDetails :: (MonadBlobStore m) => BakerId -> - BlockRewardDetails 'AccountV0 -> - m (BlockRewardDetails 'AccountV0) + BlockRewardDetails' 'AccountV0 bhv -> + m (BlockRewardDetails' 'AccountV0 bhv) consBlockRewardDetails bid (BlockRewardDetailsV0 heb) = do BlockRewardDetailsV0 <$> consEpochBlock bid heb -- | The empty 'BlockRewardDetails'. emptyBlockRewardDetails :: - forall av m. - (MonadBlobStore m, IsAccountVersion av) => - m (BlockRewardDetails av) + forall av bhv m. + (MonadBlobStore m, IsAccountVersion av, IsBlockHashVersion bhv) => + m (BlockRewardDetails' av bhv) emptyBlockRewardDetails = case delegationSupport @av of SAVDelegationNotSupported -> return $ BlockRewardDetailsV0 emptyHashedEpochBlocks @@ -688,21 +698,36 @@ emptyMerkleTransactionOutcomes = -- the hashing scheme is not a hash list but a merkle tree, so it is the root hash that is -- used in the final 'BlockHash'. data PersistentTransactionOutcomes (tov :: TransactionOutcomesVersion) where - PTOV0 :: Transactions.TransactionOutcomes -> PersistentTransactionOutcomes 'TOV0 + PTOV0 :: TransactionOutcomes.TransactionOutcomes -> PersistentTransactionOutcomes 'TOV0 PTOV1 :: MerkleTransactionOutcomes -> PersistentTransactionOutcomes 'TOV1 + PTOV2 :: MerkleTransactionOutcomes -> PersistentTransactionOutcomes 'TOV2 -- | Create an empty persistent transaction outcome emptyPersistentTransactionOutcomes :: forall tov. (IsTransactionOutcomesVersion tov) => PersistentTransactionOutcomes tov emptyPersistentTransactionOutcomes = case transactionOutcomesVersion @tov of - STOV0 -> PTOV0 Transactions.emptyTransactionOutcomesV0 + STOV0 -> PTOV0 TransactionOutcomes.emptyTransactionOutcomesV0 STOV1 -> PTOV1 emptyMerkleTransactionOutcomes + STOV2 -> PTOV2 emptyMerkleTransactionOutcomes -instance (BlobStorable m TransactionSummaryV1) => MHashableTo m Transactions.TransactionOutcomesHash (PersistentTransactionOutcomes tov) where +instance + (BlobStorable m TransactionSummaryV1) => + MHashableTo m (TransactionOutcomes.TransactionOutcomesHashV tov) (PersistentTransactionOutcomes tov) + where getHashM (PTOV0 bto) = return (getHash bto) getHashM (PTOV1 MerkleTransactionOutcomes{..}) = do - out <- getHashM mtoOutcomes - special <- getHashM mtoSpecials - return $! Transactions.TransactionOutcomesHash (H.hashShort ("TransactionOutcomesHashV1" <> H.hashToShortByteString out <> H.hashToShortByteString special)) + out <- getHashM @_ @(LFMBT.LFMBTreeHash' 'BlockHashVersion0) mtoOutcomes + special <- getHashM @_ @(LFMBT.LFMBTreeHash' 'BlockHashVersion0) mtoSpecials + return $! + TransactionOutcomes.TransactionOutcomesHashV . H.hashLazy . runPutLazy $ do + putShortByteString "TransactionOutcomesHashV1" + put out + put special + getHashM (PTOV2 MerkleTransactionOutcomes{..}) = do + out <- getHashM @_ @(LFMBT.LFMBTreeHash' 'BlockHashVersion1) mtoOutcomes + special <- getHashM @_ @(LFMBT.LFMBTreeHash' 'BlockHashVersion1) mtoSpecials + return $! + TransactionOutcomes.TransactionOutcomesHashV $ + H.hashOfHashes (LFMBT.theLFMBTreeHash out) (LFMBT.theLFMBTreeHash special) instance ( TransactionOutcomesVersionFor (MPV m) ~ tov, @@ -711,16 +736,20 @@ instance ) => BlobStorable m (PersistentTransactionOutcomes tov) where - storeUpdate out@(PTOV0 bto) = return (Transactions.putTransactionOutcomes bto, out) - storeUpdate (PTOV1 MerkleTransactionOutcomes{..}) = do - (pout, mtoOutcomes') <- storeUpdate mtoOutcomes - (pspecial, mtoSpecials') <- storeUpdate mtoSpecials - return (pout <> pspecial, PTOV1 MerkleTransactionOutcomes{mtoOutcomes = mtoOutcomes', mtoSpecials = mtoSpecials'}) + storeUpdate out@(PTOV0 bto) = return (TransactionOutcomes.putTransactionOutcomes bto, out) + storeUpdate out = case out of + PTOV1 mto -> (_2 %~ PTOV1) <$> inner mto + PTOV2 mto -> (_2 %~ PTOV2) <$> inner mto + where + inner MerkleTransactionOutcomes{..} = do + (pout, mtoOutcomes') <- storeUpdate mtoOutcomes + (pspecial, mtoSpecials') <- storeUpdate mtoSpecials + return (pout <> pspecial, MerkleTransactionOutcomes{mtoOutcomes = mtoOutcomes', mtoSpecials = mtoSpecials'}) load = do case transactionOutcomesVersion @(TransactionOutcomesVersionFor (MPV m)) of STOV0 -> do - out <- PTOV0 <$!> Transactions.getTransactionOutcomes (protocolVersion @(MPV m)) + out <- PTOV0 <$!> TransactionOutcomes.getTransactionOutcomes (protocolVersion @(MPV m)) pure . pure $! out STOV1 -> do mout <- load @@ -729,6 +758,13 @@ instance mtoOutcomes <- mout mtoSpecials <- mspecials return $! PTOV1 MerkleTransactionOutcomes{..} + STOV2 -> do + mout <- load + mspecials <- load + return $! do + mtoOutcomes <- mout + mtoSpecials <- mspecials + return $! PTOV2 MerkleTransactionOutcomes{..} -- | Create an empty 'PersistentTransactionOutcomes' based on the 'ProtocolVersion'. emptyTransactionOutcomes :: @@ -737,8 +773,9 @@ emptyTransactionOutcomes :: Proxy pv -> PersistentTransactionOutcomes (TransactionOutcomesVersionFor pv) emptyTransactionOutcomes Proxy = case transactionOutcomesVersion @(TransactionOutcomesVersionFor pv) of - STOV0 -> PTOV0 Transactions.emptyTransactionOutcomesV0 + STOV0 -> PTOV0 TransactionOutcomes.emptyTransactionOutcomesV0 STOV1 -> PTOV1 emptyMerkleTransactionOutcomes + STOV2 -> PTOV2 emptyMerkleTransactionOutcomes -- | References to the components that make up the block state. -- @@ -750,7 +787,7 @@ emptyTransactionOutcomes Proxy = case transactionOutcomesVersion @(TransactionOu data BlockStatePointers (pv :: ProtocolVersion) = BlockStatePointers { bspAccounts :: !(Accounts.Accounts pv), bspInstances :: !(Instances.Instances pv), - bspModules :: !(HashedBufferedRef Modules.Modules), + bspModules :: !(HashedBufferedRef' (ModulesHash pv) Modules.Modules), bspBank :: !(Hashed Rewards.BankStatus), bspIdentityProviders :: !(HashedBufferedRef IPS.IdentityProviders), bspAnonymityRevokers :: !(HashedBufferedRef ARS.AnonymityRevokers), @@ -761,7 +798,7 @@ data BlockStatePointers (pv :: ProtocolVersion) = BlockStatePointers bspTransactionOutcomes :: !(PersistentTransactionOutcomes (TransactionOutcomesVersionFor pv)), -- | Details of bakers that baked blocks in the current epoch. This is -- used for rewarding bakers at the end of epochs. - bspRewardDetails :: !(BlockRewardDetails (AccountVersionFor pv)) + bspRewardDetails :: !(BlockRewardDetails pv) } -- | Lens for accessing the birk parameters of a 'BlockStatePointers' structure. @@ -876,13 +913,17 @@ instance (SupportsPersistentState pv m) => BlobStorable m (BlockStatePointers pv return $! BlockStatePointers{..} -- | Accessor for getting the pool rewards when supported by the protocol version. -bspPoolRewards :: (PVSupportsDelegation pv) => BlockStatePointers pv -> HashedBufferedRef' Rewards.PoolRewardsHash PoolRewards +bspPoolRewards :: + (PVSupportsDelegation pv, bhv ~ BlockHashVersionFor pv) => + BlockStatePointers pv -> + HashedBufferedRef' (Rewards.PoolRewardsHash bhv) (PoolRewards bhv) bspPoolRewards bsp = case bspRewardDetails bsp of BlockRewardDetailsV1 pr -> pr -- | An initial 'HashedPersistentBlockState', which may be used for testing purposes. {-# WARNING initialPersistentState "should only be used for testing" #-} initialPersistentState :: + forall pv m. (SupportsPersistentState pv m) => SeedState (SeedStateVersionFor pv) -> CryptographicParameters -> @@ -2708,24 +2749,41 @@ doGetTransactionOutcome pbs transHash = do bsp <- loadPBS pbs case bspTransactionOutcomes bsp of PTOV0 bto -> return $! bto ^? ix transHash - PTOV1 bto -> do - fmap _transactionSummaryV1 <$> LFMBT.lookup transHash (mtoOutcomes bto) + PTOV1 bto -> fmap _transactionSummaryV1 <$> LFMBT.lookup transHash (mtoOutcomes bto) + PTOV2 bto -> fmap _transactionSummaryV1 <$> LFMBT.lookup transHash (mtoOutcomes bto) -doGetTransactionOutcomesHash :: forall pv m. (SupportsPersistentState pv m) => PersistentBlockState pv -> m Transactions.TransactionOutcomesHash +doGetTransactionOutcomesHash :: + forall pv m. + (SupportsPersistentState pv m) => + PersistentBlockState pv -> + m TransactionOutcomes.TransactionOutcomesHash doGetTransactionOutcomesHash pbs = do bsp <- loadPBS pbs - getHashM (bspTransactionOutcomes bsp) + TransactionOutcomes.toTransactionOutcomesHash @(TransactionOutcomesVersionFor pv) + <$> getHashM (bspTransactionOutcomes bsp) doSetTransactionOutcomes :: forall pv m. (SupportsPersistentState pv m) => PersistentBlockState pv -> [TransactionSummary] -> m (PersistentBlockState pv) doSetTransactionOutcomes pbs transList = do bsp <- loadPBS pbs case bspTransactionOutcomes bsp of PTOV0 _ -> - storePBS pbs bsp{bspTransactionOutcomes = PTOV0 (Transactions.transactionOutcomesV0FromList transList)} + storePBS + pbs + bsp + { bspTransactionOutcomes = + PTOV0 (TransactionOutcomes.transactionOutcomesV0FromList transList) + } PTOV1 _ -> do - mtoOutcomes <- LFMBT.fromAscList . map TransactionSummaryV1 $ transList - let mtoSpecials = LFMBT.empty - storePBS pbs bsp{bspTransactionOutcomes = PTOV1 MerkleTransactionOutcomes{..}} + mto <- makeMTO + storePBS pbs bsp{bspTransactionOutcomes = PTOV1 mto} + PTOV2 _ -> do + mto <- makeMTO + storePBS pbs bsp{bspTransactionOutcomes = PTOV2 mto} + where + makeMTO :: m MerkleTransactionOutcomes + makeMTO = do + mtoOutcomes <- LFMBT.fromAscList . map TransactionSummaryV1 $ transList + return MerkleTransactionOutcomes{mtoSpecials = LFMBT.empty, ..} doNotifyEncryptedBalanceChange :: (SupportsPersistentState pv m) => PersistentBlockState pv -> AmountDelta -> m (PersistentBlockState pv) doNotifyEncryptedBalanceChange pbs amntDiff = do @@ -2736,25 +2794,34 @@ doGetSpecialOutcomes :: (SupportsPersistentState pv m, MonadProtocolVersion m) = doGetSpecialOutcomes pbs = do bsp <- loadPBS pbs case bspTransactionOutcomes bsp of - PTOV0 bto -> return (bto ^. Transactions.outcomeSpecial) + PTOV0 bto -> return (bto ^. TransactionOutcomes.outcomeSpecial) PTOV1 bto -> Seq.fromList <$> LFMBT.toAscList (mtoSpecials bto) + PTOV2 bto -> Seq.fromList <$> LFMBT.toAscList (mtoSpecials bto) doGetOutcomes :: (SupportsPersistentState pv m, MonadProtocolVersion m) => PersistentBlockState pv -> m (Vec.Vector TransactionSummary) doGetOutcomes pbs = do bsp <- loadPBS pbs case bspTransactionOutcomes bsp of - PTOV0 bto -> return (Transactions.outcomeValues bto) + PTOV0 bto -> return (TransactionOutcomes.outcomeValues bto) PTOV1 bto -> Vec.fromList . map _transactionSummaryV1 <$> LFMBT.toAscList (mtoOutcomes bto) + PTOV2 bto -> Vec.fromList . map _transactionSummaryV1 <$> LFMBT.toAscList (mtoOutcomes bto) doAddSpecialTransactionOutcome :: (SupportsPersistentState pv m, MonadProtocolVersion m) => PersistentBlockState pv -> Transactions.SpecialTransactionOutcome -> m (PersistentBlockState pv) doAddSpecialTransactionOutcome pbs !o = do bsp <- loadPBS pbs case bspTransactionOutcomes bsp of PTOV0 bto -> - storePBS pbs $! bsp{bspTransactionOutcomes = PTOV0 (bto & Transactions.outcomeSpecial %~ (Seq.|> o))} + storePBS pbs $! + bsp + { bspTransactionOutcomes = + PTOV0 (bto & TransactionOutcomes.outcomeSpecial %~ (Seq.|> o)) + } PTOV1 bto -> do (_, newSpecials) <- LFMBT.append o (mtoSpecials bto) storePBS pbs $! bsp{bspTransactionOutcomes = PTOV1 (bto{mtoSpecials = newSpecials})} + PTOV2 bto -> do + (_, newSpecials) <- LFMBT.append o (mtoSpecials bto) + storePBS pbs $! bsp{bspTransactionOutcomes = PTOV2 (bto{mtoSpecials = newSpecials})} doGetElectionDifficulty :: ( SupportsPersistentState pv m, @@ -3004,7 +3071,7 @@ doMarkFinalizationAwakeBakers pbs bids = do bpc <- bakerPoolCapital <$> refLoad (currentCapital pr) newBPRs <- foldM (markFinalizerAwake bpc) bprs bids newBlockRewardDetails <- BlockRewardDetailsV1 <$> refMake pr{bakerPoolRewardDetails = newBPRs} - newHash :: (Rewards.BlockRewardDetailsHash (AccountVersionFor pv)) <- + newHash :: (Rewards.BlockRewardDetailsHash pv) <- getHashM newBlockRewardDetails oldHash <- getHashM (bspRewardDetails bsp) if newHash == oldHash diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs index b2812d6846..bc2350e8e5 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs @@ -33,6 +33,7 @@ module Concordium.GlobalState.Persistent.BlockState.Modules ( ) where import Concordium.Crypto.SHA256 +import Concordium.GlobalState.BlockState (ModulesHash (..)) import Concordium.GlobalState.Persistent.BlobStore import Concordium.GlobalState.Persistent.Cache import Concordium.GlobalState.Persistent.CachedRef @@ -282,8 +283,11 @@ data Modules = Modules makeLenses ''Modules -- | The hash of the collection of modules is the hash of the tree. -instance (SupportsPersistentModule m) => MHashableTo m Hash Modules where - getHashM = getHashM . _modulesTable +instance (SupportsPersistentModule m, IsBlockHashVersion (BlockHashVersionFor pv)) => MHashableTo m (ModulesHash pv) Modules where + getHashM = + fmap (ModulesHash . LFMB.theLFMBTreeHash @(BlockHashVersionFor pv)) + . getHashM + . _modulesTable instance (SupportsPersistentModule m) => BlobStorable m Modules where load = do diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs index 6e3c3d1861..6ba09baa9d 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs @@ -186,7 +186,7 @@ buildGenesisBlockState vcgp GenesisData.GenesisState{..} = do Types.SAVDelegationSupported -> case Types.delegationChainParameters @pv of Types.DelegationChainParameters -> do - capRef :: Blob.HashedBufferedRef CapDist.CapitalDistribution <- + capRef :: Blob.HashedBufferedRef' (CapDist.CapitalDistributionHash pv) CapDist.CapitalDistribution <- Blob.refMakeFlushed CapDist.CapitalDistribution { bakerPoolCapital = agsBakerCapitals, diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs index 882a0e2ca0..30ccb32d7c 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs @@ -1,8 +1,10 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} @@ -12,6 +14,12 @@ -- -- An implementation of a Left-full Merkle Binary Tree. module Concordium.GlobalState.Persistent.LFMBTree ( + -- * Hash types + LFMBTreeHash' (..), + LFMBTreeHash, + LFMBTreeHashV0, + LFMBTreeHashV1, + -- * Tree type LFMBTree, LFMBTree', @@ -57,8 +65,16 @@ module Concordium.GlobalState.Persistent.LFMBTree ( where import qualified Concordium.Crypto.SHA256 as H -import Concordium.GlobalState.Basic.BlockState.LFMBTree (setBits) +import Concordium.GlobalState.Basic.BlockState.LFMBTree ( + LFMBTreeHash, + LFMBTreeHash' (..), + LFMBTreeHashV0, + LFMBTreeHashV1, + emptyTreeHash, + setBits, + ) import Concordium.GlobalState.Persistent.BlobStore +import Concordium.Types import Concordium.Types.HashableTo import Control.Monad import Control.Monad.Trans @@ -66,6 +82,7 @@ import Data.Bits import Data.Coerce (Coercible, coerce) import Data.Kind import Data.Serialize +import Data.Singletons import Data.Word import Prelude hiding (lookup) @@ -133,16 +150,38 @@ instance hr <- getHashM r return $ H.hashOfHashes hl hr --- | The hash of a LFMBTree is defined as the hash of the string "EmptyLFMBTree" if it --- is empty or the hash of the tree otherwise. +-- | Compute the version 0 hash of an LFMBTree. +toHashV0 :: + (MHashableTo m H.Hash v, MHashableTo m H.Hash (ref (T ref v))) => + LFMBTree' k ref v -> + m LFMBTreeHashV0 +toHashV0 Empty = return emptyTreeHash +toHashV0 (NonEmpty _ v) = LFMBTreeHash <$> getHashM v + +-- | Compute the version 1 hash of an LFMBTree. +toHashV1 :: + (MHashableTo m H.Hash v, MHashableTo m H.Hash (ref (T ref v))) => + LFMBTree' k ref v -> + m LFMBTreeHashV1 +toHashV1 t = do + preHash <- toHashV0 t + let sz = case t of + Empty -> 0 + NonEmpty s _ -> s + return $ LFMBTreeHash $ H.hashLazy $ runPutLazy $ do + putWord64be sz + put preHash + instance ( MHashableTo m H.Hash v, -- values must be hashable - MHashableTo m H.Hash (ref (T ref v)) -- references to nodes must be hashable + MHashableTo m H.Hash (ref (T ref v)), -- references to nodes must be hashable + IsBlockHashVersion bhv ) => - MHashableTo m H.Hash (LFMBTree' k ref v) + MHashableTo m (LFMBTreeHash' bhv) (LFMBTree' k ref v) where - getHashM Empty = return $ H.hash "EmptyLFMBTree" - getHashM (NonEmpty _ v) = getHashM v + getHashM = case sing @bhv of + SBlockHashVersion0 -> toHashV0 + SBlockHashVersion1 -> toHashV1 -- | Constraints that ensures a monad @m@ can store an LFMBTree (that holds references -- of type @ref@ to values of type @v@) in references of type @ref@. diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs index d5d44d19f4..d7691e6d61 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs @@ -1,8 +1,12 @@ {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} module Concordium.GlobalState.Persistent.PoolRewards ( module Concordium.GlobalState.Basic.BlockState.PoolRewards, + CapitalDistributionRef, PoolRewards (..), emptyPoolRewards, makerPersistentPoolRewards, @@ -14,6 +18,7 @@ module Concordium.GlobalState.Persistent.PoolRewards ( lookupBakerCapitalAndRewardDetails, migratePoolRewardsP1, migratePoolRewards, + migratePoolRewardsChangeHash, ) where import Control.Exception (assert) @@ -40,18 +45,21 @@ import Concordium.GlobalState.CapitalDistribution import Concordium.GlobalState.Persistent.BlobStore import qualified Concordium.GlobalState.Persistent.LFMBTree as LFMBT -import Concordium.Utils.Serialization.Put +import Concordium.Utils.Serialization.Put (MonadPut (liftPut)) + +type CapitalDistributionRef (bhv :: BlockHashVersion) = + HashedBufferedRef' (CapitalDistributionHash' bhv) CapitalDistribution -- | Details of rewards accruing over the course of a reward period, and details about the capital -- distribution for this reward period and (possibly) the next. Note, 'currentCapital' and -- 'nextCapital' are the same except in the epoch before a payday, where 'nextCapital' is updated -- to record the capital distribution for the next reward period. -data PoolRewards = PoolRewards +data PoolRewards (bhv :: BlockHashVersion) = PoolRewards { -- | The capital distribution for the next reward period. -- This is updated the epoch before a payday. - nextCapital :: !(HashedBufferedRef CapitalDistribution), + nextCapital :: !(CapitalDistributionRef bhv), -- | The capital distribution for the current reward period. - currentCapital :: !(HashedBufferedRef CapitalDistribution), + currentCapital :: !(CapitalDistributionRef bhv), -- | The details of rewards accruing to baker pools. -- These are indexed by the index of the baker in the capital distribution (_not_ the BakerId). bakerPoolRewardDetails :: !(LFMBT.LFMBTree Word64 BufferedRef BakerPoolRewardDetails), @@ -68,12 +76,13 @@ data PoolRewards = PoolRewards -- | Migrate pool rewards from @m@ to the new backing store @t m@. -- This takes the new next payday epoch as a parameter, since this should always be updated on --- a protocol update. +-- a protocol update. This does not allow the hashing scheme to change in the migration, and +-- thus can reuse hashes. migratePoolRewards :: (SupportMigration m t) => Epoch -> - PoolRewards -> - t m PoolRewards + PoolRewards bhv -> + t m (PoolRewards bhv) migratePoolRewards newNextPayday PoolRewards{..} = do nextCapital' <- migrateHashedBufferedRefKeepHash nextCapital currentCapital' <- migrateHashedBufferedRefKeepHash currentCapital @@ -87,12 +96,34 @@ migratePoolRewards newNextPayday PoolRewards{..} = do .. } +-- | Migrate pool rewards from @m@ to the new backing store @t m@. +-- This takes the new next payday epoch as a parameter, since this should always be updated on +-- a protocol update. Compared to 'migratePoolRewards', this implementation supports +-- changing the hashing scheme used for the pool rewards. +migratePoolRewardsChangeHash :: + (SupportMigration m t, IsBlockHashVersion bhv1) => + Epoch -> + PoolRewards bhv0 -> + t m (PoolRewards bhv1) +migratePoolRewardsChangeHash newNextPayday PoolRewards{..} = do + nextCapital' <- migrateHashedBufferedRef return nextCapital + currentCapital' <- migrateHashedBufferedRef return currentCapital + bakerPoolRewardDetails' <- LFMBT.migrateLFMBTree (migrateReference return) bakerPoolRewardDetails + return + PoolRewards + { nextCapital = nextCapital', + currentCapital = currentCapital', + bakerPoolRewardDetails = bakerPoolRewardDetails', + nextPaydayEpoch = newNextPayday, + .. + } + -- the remaining fields are flat, so migration is copying -- | Migrate pool rewards from the format before delegation to the P4 format. migratePoolRewardsP1 :: - forall m. - (MonadBlobStore m) => + forall m bhv. + (MonadBlobStore m, IsBlockHashVersion bhv) => -- | Current epoch bakers and stakes, in ascending order of 'BakerId'. [(BakerId, Amount)] -> -- | Next epoch bakers and stakes, in ascending order of 'BakerId'. @@ -103,7 +134,7 @@ migratePoolRewardsP1 :: Epoch -> -- | Mint rate for the next payday MintRate -> - m PoolRewards + m (PoolRewards bhv) migratePoolRewardsP1 curBakers nextBakers blockCounts npEpoch npMintRate = do (nextCapital, _) <- refFlush =<< bufferHashed (makeCD nextBakers) (currentCapital, _) <- refFlush =<< bufferHashed (makeCD curBakers) @@ -135,9 +166,9 @@ migratePoolRewardsP1 curBakers nextBakers blockCounts npEpoch npMintRate = do -- | Look up the baker capital and reward details for a baker ID. lookupBakerCapitalAndRewardDetails :: - (MonadBlobStore m) => + (MonadBlobStore m, IsBlockHashVersion bhv) => BakerId -> - PoolRewards -> + PoolRewards bhv -> m (Maybe (BakerCapital, BakerPoolRewardDetails)) lookupBakerCapitalAndRewardDetails bid PoolRewards{..} = do cdistr <- refLoad currentCapital @@ -146,7 +177,7 @@ lookupBakerCapitalAndRewardDetails bid PoolRewards{..} = do Just (index, capital) -> fmap (capital,) <$> LFMBT.lookup (fromIntegral index) bakerPoolRewardDetails -instance (MonadBlobStore m) => BlobStorable m PoolRewards where +instance (MonadBlobStore m) => BlobStorable m (PoolRewards bhv) where storeUpdate pr0 = do (pNextCapital, nextCapital) <- storeUpdate (nextCapital pr0) (pCurrentCapital, currentCapital) <- storeUpdate (currentCapital pr0) @@ -188,7 +219,7 @@ instance (MonadBlobStore m) => BlobStorable m PoolRewards where -- | Serialize 'PoolRewards'. -- The 'bakerPoolRewardDetails' is serialized as a flat list, with the length implied by the -- length of 'bakerPoolCapital' of 'currentCapital'. -putPoolRewards :: (MonadBlobStore m, MonadPut m) => PoolRewards -> m () +putPoolRewards :: (MonadBlobStore m, MonadPut m, IsBlockHashVersion bhv) => PoolRewards bhv -> m () putPoolRewards PoolRewards{..} = do nxtCapital <- refLoad nextCapital curCapital <- refLoad currentCapital @@ -203,15 +234,15 @@ putPoolRewards PoolRewards{..} = do put nextPaydayEpoch put nextPaydayMintRate -instance (MonadBlobStore m) => MHashableTo m PoolRewardsHash PoolRewards where +instance (MonadBlobStore m, IsBlockHashVersion bhv) => MHashableTo m (PoolRewardsHash bhv) (PoolRewards bhv) where getHashM PoolRewards{..} = do hNextCapital <- getHashM nextCapital hCurrentCapital <- getHashM currentCapital hBakerPoolRewardDetails <- getHashM bakerPoolRewardDetails return $! - PoolRewardsHash . Hash.hashOfHashes hNextCapital $ - Hash.hashOfHashes hCurrentCapital $ - Hash.hashOfHashes hBakerPoolRewardDetails $ + PoolRewardsHash . Hash.hashOfHashes (theCapitalDistributionHash @bhv hNextCapital) $ + Hash.hashOfHashes (theCapitalDistributionHash @bhv hCurrentCapital) $ + Hash.hashOfHashes (BasicLFMBT.theLFMBTreeHash @bhv hBakerPoolRewardDetails) $ getHash $ runPut $ put passiveDelegationTransactionRewards @@ -219,7 +250,7 @@ instance (MonadBlobStore m) => MHashableTo m PoolRewardsHash PoolRewards where <> put nextPaydayEpoch <> put nextPaydayMintRate -instance (MonadBlobStore m) => Cacheable m PoolRewards where +instance (MonadBlobStore m, IsBlockHashVersion bhv) => Cacheable m (PoolRewards bhv) where cache pr@PoolRewards{nextPaydayEpoch = nextPaydayEpoch, nextPaydayMintRate = nextPaydayMintRate} = do nextCapital <- cache (nextCapital pr) currentCapital <- cache (currentCapital pr) @@ -228,11 +259,11 @@ instance (MonadBlobStore m) => Cacheable m PoolRewards where foundationTransactionRewards <- cache (foundationTransactionRewards pr) return PoolRewards{..} -makerPersistentPoolRewards :: (MonadBlobStore m) => BasicPoolRewards.PoolRewards -> m PoolRewards +makerPersistentPoolRewards :: (MonadBlobStore m, IsBlockHashVersion bhv) => BasicPoolRewards.PoolRewards bhv -> m (PoolRewards bhv) makerPersistentPoolRewards bpr = do nc <- refMake (_unhashed (BasicPoolRewards.nextCapital bpr)) cc <- refMake (_unhashed (BasicPoolRewards.currentCapital bpr)) - bprd <- LFMBT.fromAscList $ BasicLFMBT.toAscList $ BasicPoolRewards.bakerPoolRewardDetails bpr + bprd <- LFMBT.fromAscList $ Vec.toList $ BasicPoolRewards.bakerPoolRewardDetails bpr return PoolRewards { nextCapital = nc, @@ -245,11 +276,11 @@ makerPersistentPoolRewards bpr = do } -- | The empty 'PoolRewards'. -emptyPoolRewards :: (MonadBlobStore m) => m PoolRewards +emptyPoolRewards :: (MonadBlobStore m, IsBlockHashVersion bhv) => m (PoolRewards bhv) emptyPoolRewards = makerPersistentPoolRewards BasicPoolRewards.emptyPoolRewards -- | List of baker and number of blocks baked by this baker in the reward period. -bakerBlockCounts :: (MonadBlobStore m) => PoolRewards -> m [(BakerId, Word64)] +bakerBlockCounts :: (MonadBlobStore m, IsBlockHashVersion bhv) => PoolRewards bhv -> m [(BakerId, Word64)] bakerBlockCounts PoolRewards{..} = do cc <- refLoad currentCapital rds <- LFMBT.toAscPairList bakerPoolRewardDetails @@ -264,9 +295,9 @@ bakerBlockCounts PoolRewards{..} = do -- | Rotate the capital distribution, so that the current capital distribution is replaced by the -- next one, and set up empty pool rewards. rotateCapitalDistribution :: - (MonadBlobStore m, Reference m ref PoolRewards) => - ref PoolRewards -> - m (ref PoolRewards) + (MonadBlobStore m, Reference m ref (PoolRewards bhv), IsBlockHashVersion bhv) => + ref (PoolRewards bhv) -> + m (ref (PoolRewards bhv)) rotateCapitalDistribution oldPoolRewards = do pr <- refLoad oldPoolRewards nextCap <- refLoad (nextCapital pr) @@ -282,11 +313,11 @@ rotateCapitalDistribution oldPoolRewards = do } setNextCapitalDistribution :: - (MonadBlobStore m, Reference m ref PoolRewards) => + (MonadBlobStore m, Reference m ref (PoolRewards bhv), IsBlockHashVersion bhv) => [(BakerId, Amount, [(DelegatorId, Amount)])] -> [(DelegatorId, Amount)] -> - ref PoolRewards -> - m (ref PoolRewards) + ref (PoolRewards bhv) -> + m (ref (PoolRewards bhv)) setNextCapitalDistribution bakers passive oldPoolRewards = do let bakerPoolCapital = Vec.fromList $ map mkBakCap bakers let passiveDelegatorsCapital = Vec.fromList $ map mkDelCap passive @@ -302,8 +333,8 @@ setNextCapitalDistribution bakers passive oldPoolRewards = do -- | The total capital passively delegated in the current reward period capital distribution. currentPassiveDelegationCapital :: - (MonadBlobStore m) => - PoolRewards -> + (MonadBlobStore m, IsBlockHashVersion bhv) => + PoolRewards bhv -> m Amount currentPassiveDelegationCapital PoolRewards{..} = Vec.sum . fmap dcDelegatorCapital . passiveDelegatorsCapital <$> refLoad currentCapital diff --git a/concordium-consensus/src/Concordium/GlobalState/Rewards.hs b/concordium-consensus/src/Concordium/GlobalState/Rewards.hs index 40ecaffae7..9aca165ad6 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Rewards.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Rewards.hs @@ -114,21 +114,24 @@ epochBlockHash bid h = <> encode bid <> H.hashToByteString (ebHash h) -newtype PoolRewardsHash = PoolRewardsHash {prHash :: H.Hash} +newtype PoolRewardsHash (bhv :: BlockHashVersion) = PoolRewardsHash {prHash :: H.Hash} deriving newtype (Eq, Ord, Show, Serialize) -- | Hash of block reward details. -data BlockRewardDetailsHash (av :: AccountVersion) where - BlockRewardDetailsHashV0 :: !EpochBlocksHash -> BlockRewardDetailsHash 'AccountV0 +data BlockRewardDetailsHash' (av :: AccountVersion) (bhv :: BlockHashVersion) where + BlockRewardDetailsHashV0 :: !EpochBlocksHash -> BlockRewardDetailsHash' 'AccountV0 bhv BlockRewardDetailsHashV1 :: (AVSupportsDelegation av) => - !PoolRewardsHash -> - BlockRewardDetailsHash av + !(PoolRewardsHash bhv) -> + BlockRewardDetailsHash' av bhv -deriving instance Show (BlockRewardDetailsHash av) -deriving instance Eq (BlockRewardDetailsHash av) +deriving instance Show (BlockRewardDetailsHash' av bhv) +deriving instance Eq (BlockRewardDetailsHash' av bhv) + +type BlockRewardDetailsHash (pv :: ProtocolVersion) = + BlockRewardDetailsHash' (AccountVersionFor pv) (BlockHashVersionFor pv) -- | SHA256 hash of 'BlockRewardDetailsHash'. -brdHash :: BlockRewardDetailsHash av -> H.Hash +brdHash :: BlockRewardDetailsHash' av bhv -> H.Hash brdHash (BlockRewardDetailsHashV0 eb) = ebHash eb brdHash (BlockRewardDetailsHashV1 ha) = prHash ha diff --git a/concordium-consensus/src/Concordium/GlobalState/TreeState.hs b/concordium-consensus/src/Concordium/GlobalState/TreeState.hs index d0323d934d..e98fd2086c 100644 --- a/concordium-consensus/src/Concordium/GlobalState/TreeState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/TreeState.hs @@ -38,6 +38,7 @@ import Concordium.Types.Updates hiding (getUpdateKeysCollection) import qualified Concordium.GlobalState.Block as B import Concordium.Scheduler.Types (FilteredTransactions) import qualified Concordium.TransactionVerification as TVer +import Concordium.Types.TransactionOutcomes data BlockStatus bp pb = BlockAlive !bp diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus.hs index e4e605dd80..7c195cd0c5 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus.hs @@ -49,7 +49,7 @@ class MonadBroadcast m where sendQuorumMessage :: QuorumMessage -> m () -- | Broadcast a 'SignedBlock'. - sendBlock :: SignedBlock -> m () + sendBlock :: SignedBlock (MPV m) -> m () -- | This class provides event handlers for consensus events. A runner should implement this to -- handle these events. diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs index bedc8e8d16..483e27f2e1 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs @@ -57,9 +57,9 @@ import Concordium.Types.Option -- | A block that has passed initial verification, but must still be executed, added to the state, -- and (potentially) signed as a finalizer. -data VerifiedBlock = VerifiedBlock +data VerifiedBlock (pv :: ProtocolVersion) = VerifiedBlock { -- | The block that has passed initial verification. - vbBlock :: !PendingBlock, + vbBlock :: !(PendingBlock pv), -- | The bakers and finalizers for the epoch of this block. vbBakersAndFinalizers :: !BakersAndFinalizers, -- | The baker info for the block's own baker. @@ -69,8 +69,8 @@ data VerifiedBlock = VerifiedBlock } deriving (Eq, Show) -instance BlockData VerifiedBlock where - type BakedBlockDataType VerifiedBlock = SignedBlock +instance BlockData (VerifiedBlock pv) where + type BakedBlockDataType (VerifiedBlock pv) = SignedBlock pv blockRound = blockRound . vbBlock blockEpoch = blockEpoch . vbBlock blockTimestamp = blockTimestamp . vbBlock @@ -82,12 +82,12 @@ instance BlockData VerifiedBlock where -- * Receiving blocks -- | The result type for 'uponReceivingBlock'. -data BlockResult +data BlockResult pv = -- | The block was successfully received, but not yet executed. - BlockResultSuccess !VerifiedBlock + BlockResultSuccess !(VerifiedBlock pv) | -- | The baker also signed another block in the same round, but the block was otherwise -- successfully received, but not yet executed. - BlockResultDoubleSign !VerifiedBlock + BlockResultDoubleSign !(VerifiedBlock pv) | -- | The block contains data that is not valid with respect to the chain. BlockResultInvalid | -- | The block is too old to be added to the chain. @@ -113,14 +113,15 @@ data BlockResult -- 'executeBlock' to complete the procedure (as necessary). uponReceivingBlock :: ( IsConsensusV1 (MPV m), + MonadProtocolVersion m, LowLevel.MonadTreeStateStore m, MonadState (SkovData (MPV m)) m, BlockStateStorage m, BlockState m ~ HashedPersistentBlockState (MPV m), MonadLogger m ) => - PendingBlock -> - m BlockResult + PendingBlock (MPV m) -> + m (BlockResult (MPV m)) uponReceivingBlock pendingBlock = do mp <- Merkle.buildMerkleProof (const True) (sbBlock (pbBlock pendingBlock)) logEvent Konsensus LLTrace $ show mp @@ -193,8 +194,8 @@ receiveBlockKnownParent :: MonadLogger m ) => BlockPointer (MPV m) -> - PendingBlock -> - m BlockResult + PendingBlock (MPV m) -> + m (BlockResult (MPV m)) receiveBlockKnownParent parent pendingBlock = do logEvent Konsensus LLInfo $ "Block " <> show pbHash <> " received." let nominalTime = timestampToUTCTime $ blockTimestamp pendingBlock @@ -335,8 +336,8 @@ receiveBlockUnknownParent :: MonadState (SkovData (MPV m)) m, MonadLogger m ) => - PendingBlock -> - m BlockResult + PendingBlock pv -> + m (BlockResult (MPV m)) receiveBlockUnknownParent pendingBlock = do earlyThreshold <- rpEarlyBlockThreshold <$> use runtimeParameters if blockTimestamp pendingBlock @@ -381,7 +382,7 @@ getMinBlockTime b = do addBlock :: (TimeMonad m, MonadState (SkovData (MPV m)) m, MonadConsensusEvent m, MonadLogger m) => -- | Block to add - PendingBlock -> + PendingBlock (MPV m) -> -- | Block state HashedPersistentBlockState (MPV m) -> -- | Parent pointer @@ -447,7 +448,7 @@ processBlock :: -- | Parent block (@parent@) BlockPointer (MPV m) -> -- | Block being processed (@pendingBlock@) - VerifiedBlock -> + VerifiedBlock (MPV m) -> m (Maybe (BlockPointer (MPV m))) processBlock parent VerifiedBlock{vbBlock = pendingBlock, ..} -- Check that the QC is consistent with the parent block round. @@ -988,7 +989,7 @@ executeBlock :: MonadConsensusEvent m, MonadLogger m ) => - VerifiedBlock -> + VerifiedBlock (MPV m) -> m () executeBlock verifiedBlock = do isShutdown <- use isConsensusShutdown @@ -1161,7 +1162,7 @@ bakeBlock :: MonadLogger m ) => BakeBlockInputs (MPV m) -> - m SignedBlock + m (SignedBlock (MPV m)) bakeBlock BakeBlockInputs{..} = do curTimestamp <- utcTimeToTimestamp <$> currentTime minBlockTime <- getMinBlockTime bbiParent diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/CatchUp.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/CatchUp.hs index cea82c00f2..00dfd00c56 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/CatchUp.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/CatchUp.hs @@ -76,7 +76,7 @@ data CatchUpPartialResponse m = -- | The next block in the stream. CatchUpPartialResponseBlock { -- | Next block. - cuprNextBlock :: SignedBlock, + cuprNextBlock :: SignedBlock (MPV m), -- | Continuation for getting any further blocks. cuprContinue :: m (CatchUpPartialResponse m), -- | Continuation that gets the terminal data in the case where there are no further diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Finality.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Finality.hs index 6ba984f089..43bf26e048 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Finality.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Finality.hs @@ -1,5 +1,6 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} @@ -45,6 +46,7 @@ import Concordium.Types.Option -- -- This function incorporates the functionality of @checkFinality@ from the bluepaper. processCertifiedBlock :: + forall m. ( MonadState (SkovData (MPV m)) m, TimeMonad m, MonadIO m, @@ -54,6 +56,7 @@ processCertifiedBlock :: MonadThrow m, MonadConsensusEvent m, MonadLogger m, + MonadProtocolVersion m, IsConsensusV1 (MPV m), HasCallStack ) => diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Quorum.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Quorum.hs index 26434de6e2..386e9751d4 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Quorum.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Quorum.hs @@ -269,6 +269,7 @@ makeQuorumCertificate qcBlockPointer sd@SkovData{..} = do -- 'Concordium.KonsensusV1.Consensus.Blocks'.) processQuorumMessage :: ( IsConsensusV1 (MPV m), + MonadProtocolVersion m, MonadThrow m, MonadIO m, BlockStateStorage m, diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Flag.hs b/concordium-consensus/src/Concordium/KonsensusV1/Flag.hs index 316489cadb..d77f7ceb77 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Flag.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Flag.hs @@ -18,58 +18,58 @@ data FlaggableOffense (pv :: ProtocolVersion) DuplicateBlock !BlockSignatureWitness !BlockSignatureWitness | -- | The 'Round' of the 'QuorumCertificate' is not consistent with the -- 'Round' of the parent block. Witnessed by the block received. - BlockQCRoundInconsistent !SignedBlock + BlockQCRoundInconsistent !(SignedBlock pv) | -- | The 'Epoch' of the 'QuorumCertificate' is not consistent with the -- 'Epoch' of the parent block. Witnessed by the block received. - BlockQCEpochInconsistent !SignedBlock + BlockQCEpochInconsistent !(SignedBlock pv) | -- | The round is not greater than the parent block. Witnessed by -- the block received. - BlockRoundInconsistent !SignedBlock + BlockRoundInconsistent !(SignedBlock pv) | -- | The epoch is not the current or current + 1 epoch. Witnessed -- by the block received. - BlockEpochInconsistent !SignedBlock + BlockEpochInconsistent !(SignedBlock pv) | -- | The block received contains an invalid 'QuorumCertificate'. -- Witnessed by the block received. - BlockInvalidQC !SignedBlock + BlockInvalidQC !(SignedBlock pv) | -- | The 'TimeoutCertificate' is missing and the 'Round' of the block is -- not sequentially the next 'Round'. Witnessed by the block received. - BlockTCMissing !SignedBlock + BlockTCMissing !(SignedBlock pv) | -- | The 'Round' of the 'TimeoutCertificate' is inconsistent. -- Witnessed by the block received. - BlockTCRoundInconsistent !SignedBlock + BlockTCRoundInconsistent !(SignedBlock pv) | -- | The 'QuorumCertificate' is inconsistent -- with the 'TimeoutCertificate' of the block. -- Witnessed by the block received. - BlockQCInconsistentWithTC !SignedBlock + BlockQCInconsistentWithTC !(SignedBlock pv) | -- | The previous round did not timeout, but there is a -- 'TimeoutCertificate' present in the block. -- Witnessed by the block received. - BlockUnexpectedTC !SignedBlock + BlockUnexpectedTC !(SignedBlock pv) | -- | The 'TimeoutCertificate' is invalid. -- Witnessed by the block received. - BlockInvalidTC !SignedBlock + BlockInvalidTC !(SignedBlock pv) | -- | The 'SignedBlock' is too close to its parent @Block pv@. - BlockTooFast !SignedBlock !(Block pv) + BlockTooFast !(SignedBlock pv) !(Block pv) | -- | The block nonce is invalid. Witnessed by the block received. - BlockNonceIncorrect !SignedBlock + BlockNonceIncorrect !(SignedBlock pv) | -- | The block is in a new 'Epoch', but it is missing the finalization entry. -- Witnessed by the block received. - BlockEpochFinalizationMissing !SignedBlock - | -- | The block was not in a new 'Epoch', but a finalization entry is presnet. + BlockEpochFinalizationMissing !(SignedBlock pv) + | -- | The block was not in a new 'Epoch', but a finalization entry is present. -- Witnessed by the block received. - BlockUnexpectedEpochFinalization !SignedBlock + BlockUnexpectedEpochFinalization !(SignedBlock pv) | -- | The block is in a new 'Epoch' but the finalization entry is deemed invalid. -- Witnessed by the block received. - BlockInvalidEpochFinalization !SignedBlock + BlockInvalidEpochFinalization !(SignedBlock pv) | -- | Execution of the block failed. -- Witnessed by the block received. - BlockExecutionFailure !SignedBlock + BlockExecutionFailure !(SignedBlock pv) | -- | Execution of the block resulted in an unexpected outcome. -- Witnessed by the block received and the parent block. - BlockInvalidTransactionOutcomesHash !SignedBlock !(Block pv) + BlockInvalidTransactionOutcomesHash !(SignedBlock pv) !(Block pv) | -- | Execution of the block resulted in an unexpected state. -- Witnessed by the block received and the parent block. - BlockInvalidStateHash !SignedBlock !(Block pv) + BlockInvalidStateHash !(SignedBlock pv) !(Block pv) | -- | An invalid block was signed by the 'QuorumMessage'. -- Witnessed by the 'QuorumMessage' received. SignedInvalidBlock !QuorumMessage diff --git a/concordium-consensus/src/Concordium/KonsensusV1/SkovMonad.hs b/concordium-consensus/src/Concordium/KonsensusV1/SkovMonad.hs index ada9e290c7..eed52f5ee4 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/SkovMonad.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/SkovMonad.hs @@ -149,7 +149,7 @@ data HandlerContext (pv :: ProtocolVersion) m = HandlerContext -- | Handler to broadcast a quorum message. _sendQuorumHandler :: QuorumMessage -> m (), -- | Handler to broadcast a block. - _sendBlockHandler :: SignedBlock -> m (), + _sendBlockHandler :: SignedBlock pv -> m (), -- | An event handler called when a block becomes live. _onBlockHandler :: BlockPointer pv -> m (), -- | An event handler called per finalization. It is called with the diff --git a/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs b/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs index 2d5838f2ca..c7334515ab 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs @@ -101,7 +101,7 @@ data TestEvent (pv :: ProtocolVersion) | -- | Implements 'sendQuorumMessage' of 'MonadBroadcast'. SendQuorumMessage !QuorumMessage | -- | Implements 'sendBlock' of 'MonadBroadcast'. - SendBlock !SignedBlock + SendBlock !(SignedBlock pv) | -- | Implements 'onBlock' of 'MonadConsensusEvent'. OnBlock !(Block pv) | -- | Implements 'onFinalize' of 'MonadConsensusEvent'. diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Transactions.hs b/concordium-consensus/src/Concordium/KonsensusV1/Transactions.hs index 799a45c608..351f132833 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Transactions.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Transactions.hs @@ -274,7 +274,7 @@ processBlockItems :: GSTypes.BlockState m ~ PBS.HashedPersistentBlockState (MPV m) ) => -- | The baked block - BakedBlock -> + BakedBlock pv -> -- | Pointer to the parent block. BlockPointer pv -> -- | Return 'True' only if all transactions were diff --git a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Implementation.hs b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Implementation.hs index 19427a7f47..6f855faa4a 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Implementation.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Implementation.hs @@ -463,7 +463,7 @@ getFirstFinalizedBlockOfEpoch epochOrBlock sd makeLiveBlock :: (MonadState (SkovData pv) m) => -- | Pending block to make live - PendingBlock -> + PendingBlock pv -> -- | Block state associated with the block PBS.HashedPersistentBlockState pv -> BlockHeight -> diff --git a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel.hs b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel.hs index e1abbca3f1..eaf208cc9c 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel.hs @@ -51,7 +51,7 @@ instance (IsProtocolVersion pv) => Serialize (StoredBlock pv) where v -> fail $ "Unsupported StoredBlock version: " ++ show v instance BlockData (StoredBlock pv) where - type BakedBlockDataType (StoredBlock pv) = BakedBlockDataType SignedBlock + type BakedBlockDataType (StoredBlock pv) = SignedBlock pv blockRound = blockRound . stbBlock blockEpoch = blockEpoch . stbBlock blockTimestamp = blockTimestamp . stbBlock diff --git a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel/LMDB.hs b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel/LMDB.hs index 4b1b271100..0675d914cd 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel/LMDB.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/LowLevel/LMDB.hs @@ -880,7 +880,9 @@ rollBackBlocksUntil checkState = do { feFinalizedQuorumCertificate = blockQuorumCertificate block, feSuccessorQuorumCertificate = finQC, - feSuccessorProof = getHash (sbBlock block) + feSuccessorProof = + makeSuccessorProof @(BlockHashVersionFor pv) $ + getHash (sbBlock block) } return (c + 1, finHash : hashes, parent) loadRecord txn (dbh ^. latestFinalizationEntryStore) CSKLatestFinalizationEntry diff --git a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Types.hs b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Types.hs index 7746b43cd4..1f890f12c2 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Types.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/TreeState/Types.hs @@ -136,7 +136,7 @@ instance Eq (BlockPointer pv) where (==) = on (==) (getHash @BlockHash) instance BlockData (BlockPointer pv) where - type BakedBlockDataType (BlockPointer pv) = SignedBlock + type BakedBlockDataType (BlockPointer pv) = SignedBlock pv blockRound = blockRound . bpBlock blockEpoch = blockEpoch . bpBlock blockTimestamp = blockTimestamp . bpBlock @@ -159,19 +159,19 @@ instance HasBlockMetadata (BlockPointer pv) where blockMetadata = bpInfo -- | A block that is pending its parent. -data PendingBlock = PendingBlock +data PendingBlock (pv :: ProtocolVersion) = PendingBlock { -- | The block itself. - pbBlock :: !SignedBlock, + pbBlock :: !(SignedBlock pv), -- | The time that the block was received by the consensus. pbReceiveTime :: !UTCTime } deriving (Eq, Show) -instance HashableTo BlockHash PendingBlock where +instance HashableTo BlockHash (PendingBlock pv) where getHash PendingBlock{..} = getHash pbBlock -instance BlockData PendingBlock where - type BakedBlockDataType PendingBlock = SignedBlock +instance BlockData (PendingBlock pv) where + type BakedBlockDataType (PendingBlock pv) = SignedBlock pv blockRound = blockRound . pbBlock blockEpoch = blockEpoch . pbBlock blockTimestamp = blockTimestamp . pbBlock @@ -180,7 +180,7 @@ instance BlockData PendingBlock where blockTransactionCount = blockTransactionCount . pbBlock blockStateHash = blockStateHash . pbBlock -instance BakedBlockData PendingBlock where +instance BakedBlockData (PendingBlock pv) where blockQuorumCertificate = blockQuorumCertificate . pbBlock blockParent = blockParent . pbBlock blockBaker = blockBaker . pbBlock @@ -190,9 +190,13 @@ instance BakedBlockData PendingBlock where blockSignature = blockSignature . pbBlock blockTransactionOutcomesHash = blockTransactionOutcomesHash . pbBlock -deserializeExactVersionedPendingBlock :: SProtocolVersion pv -> BS.ByteString -> UTCTime -> Either String PendingBlock -deserializeExactVersionedPendingBlock spv blockBS recTime = - case runGet (getSignedBlock spv (utcTimeToTransactionTime recTime)) blockBS of +deserializeExactVersionedPendingBlock :: + (IsProtocolVersion pv) => + BS.ByteString -> + UTCTime -> + Either String (PendingBlock pv) +deserializeExactVersionedPendingBlock blockBS recTime = + case runGet (getSignedBlock (utcTimeToTransactionTime recTime)) blockBS of Left err -> Left $ "Block deserialization failed: " ++ err Right signedBlock -> Right $ PendingBlock signedBlock recTime diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs index b8c70aff0b..bebf5a8cef 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs @@ -36,6 +36,9 @@ import Concordium.Types.Transactions import Concordium.Utils.BinarySearch import Concordium.Utils.Serialization +import Concordium.Types.TransactionOutcomes +import Data.Singletons + -- | The message that is signed by a finalizer to certify a block. data QuorumSignatureMessage = QuorumSignatureMessage { -- | Hash of the genesis block. @@ -346,7 +349,12 @@ quorumCertificateSigningBakers finalizers qc = <$> committeeFinalizers finalizers Vector.!? fromIntegral (theFinalizerIndex finIndex) -- | A Merkle proof that one block is the successor of another. -type SuccessorProof = BlockQuasiHash +newtype SuccessorProof = SuccessorProof {theSuccessorProof :: Hash.Hash} + deriving (Eq, Ord, Show, Serialize) + +-- | Construct a 'SuccessorProof' from a 'BlockQuasiHash'. +makeSuccessorProof :: BlockQuasiHash' bhv -> SuccessorProof +makeSuccessorProof (BlockQuasiHash hsh) = SuccessorProof hsh -- | Compute the 'BlockHash' of a block that is the successor of another block. successorBlockHash :: @@ -355,7 +363,7 @@ successorBlockHash :: -- | Successor proof SuccessorProof -> BlockHash -successorBlockHash bh = computeBlockHash bhh +successorBlockHash bh = computeBlockHash' bhh . theSuccessorProof where bhh = getHash bh @@ -863,7 +871,7 @@ class BlockData b where blockStateHash :: b -> StateHash -- | A 'BakedBlock' consists of a non-genesis block, excluding the block signature. -data BakedBlock = BakedBlock +data BakedBlock (pv :: ProtocolVersion) = BakedBlock { -- | Block round number. Must be non-zero. bbRound :: !Round, -- | Block epoch number. @@ -896,7 +904,7 @@ data BakedBlockFlags = BakedBlockFlags } -- | Get the 'BakedBlockFlags' associated with a 'BakedBlock'. -bakedBlockFlags :: BakedBlock -> BakedBlockFlags +bakedBlockFlags :: BakedBlock pv -> BakedBlockFlags bakedBlockFlags BakedBlock{..} = BakedBlockFlags { bbfTimeoutCertificate = isPresent bbTimeoutCertificate, @@ -919,7 +927,7 @@ instance Serialize BakedBlockFlags where } -- | Serialize a 'BakedBlock'. -putBakedBlock :: Putter BakedBlock +putBakedBlock :: Putter (BakedBlock pv) putBakedBlock bb@BakedBlock{..} = do put bbRound put bbEpoch @@ -937,7 +945,7 @@ putBakedBlock bb@BakedBlock{..} = do -- | Deserialize a 'BakedBlock'. The protocol version is used to determine which transaction -- types are allowed in the block. -getBakedBlock :: SProtocolVersion pv -> TransactionTime -> Get BakedBlock +getBakedBlock :: SProtocolVersion pv -> TransactionTime -> Get (BakedBlock pv) getBakedBlock spv tt = label "BakedBlock" $ do bbRound <- get bbEpoch <- get @@ -976,9 +984,9 @@ getBakedBlock spv tt = label "BakedBlock" $ do -- | A baked block, together with the block hash and block signature. -- -- Invariant: @sbHash == getHash sbBlock@. -data SignedBlock = SignedBlock +data SignedBlock (pv :: ProtocolVersion) = SignedBlock { -- | The block contents. - sbBlock :: !BakedBlock, + sbBlock :: !(BakedBlock pv), -- | The hash of the block. sbHash :: !BlockHash, -- | Signature of the baker on the block. @@ -986,7 +994,7 @@ data SignedBlock = SignedBlock } deriving (Eq, Show) -instance BakedBlockData SignedBlock where +instance BakedBlockData (SignedBlock pv) where blockQuorumCertificate = bbQuorumCertificate . sbBlock blockBaker = bbBaker . sbBlock blockTimeoutCertificate = bbTimeoutCertificate . sbBlock @@ -995,8 +1003,8 @@ instance BakedBlockData SignedBlock where blockSignature = sbSignature blockTransactionOutcomesHash = bbTransactionOutcomesHash . sbBlock -instance BlockData SignedBlock where - type BakedBlockDataType SignedBlock = SignedBlock +instance BlockData (SignedBlock pv) where + type BakedBlockDataType (SignedBlock pv) = SignedBlock pv blockRound = bbRound . sbBlock blockEpoch = bbEpoch . sbBlock blockTimestamp = bbTimestamp . sbBlock @@ -1006,22 +1014,22 @@ instance BlockData SignedBlock where blockTransactionCount = Vector.length . bbTransactions . sbBlock blockStateHash = bbStateHash . sbBlock -instance HashableTo BlockHash SignedBlock where +instance HashableTo BlockHash (SignedBlock pv) where getHash = sbHash -instance (Monad m) => MHashableTo m BlockHash SignedBlock +instance (Monad m) => MHashableTo m BlockHash (SignedBlock pv) -- | Serialize a 'SignedBlock', including the signature. -putSignedBlock :: Putter SignedBlock +putSignedBlock :: Putter (SignedBlock pv) putSignedBlock SignedBlock{..} = do putBakedBlock sbBlock put sbSignature -- | Deserialize a 'SignedBlock'. The protocol version is used to determine which transactions types -- are permitted. -getSignedBlock :: SProtocolVersion pv -> TransactionTime -> Get SignedBlock -getSignedBlock spv tt = do - sbBlock <- getBakedBlock spv tt +getSignedBlock :: forall pv. (IsProtocolVersion pv) => TransactionTime -> Get (SignedBlock pv) +getSignedBlock tt = do + sbBlock <- getBakedBlock (protocolVersion @pv) tt let sbHash = getHash sbBlock sbSignature <- get return SignedBlock{..} @@ -1072,14 +1080,15 @@ signBlockHash privKey genesisHash bh = BlockSig.sign privKey (blockSignatureMess -- | Sign a block as a baker. signBlock :: + (IsProtocolVersion pv) => -- | The key to use for signing BakerSignPrivateKey -> -- | The genesis hash BlockHash -> -- | The baked block - BakedBlock -> + BakedBlock pv -> -- | The resulting signed block. - SignedBlock + SignedBlock pv signBlock privKey genesisHash sbBlock = SignedBlock{..} where sbHash = getHash sbBlock @@ -1113,7 +1122,7 @@ data BlockHeader = BlockHeader deriving (Eq) -- | The block header for a 'BakedBlock'. -bbBlockHeader :: BakedBlock -> BlockHeader +bbBlockHeader :: BakedBlock pv -> BlockHeader bbBlockHeader BakedBlock{..} = BlockHeader { bhRound = bbRound, @@ -1132,22 +1141,26 @@ instance HashableTo BlockHeaderHash BlockHeader where put bhEpoch put bhParent -instance HashableTo BlockHeaderHash BakedBlock where +instance HashableTo BlockHeaderHash (BakedBlock pv) where getHash = getHash . bbBlockHeader -- | Hash of a block's contents. This is combined with the 'BlockHeaderHash' to produce a -- 'BlockHash'. -newtype BlockQuasiHash = BlockQuasiHash {theBlockQuasiHash :: Hash.Hash} +newtype BlockQuasiHash' (bhv :: BlockHashVersion) = BlockQuasiHash {theBlockQuasiHash :: Hash.Hash} deriving (Eq, Ord, Show, Serialize) --- | Compute the hash from a list of transactions. -computeTransactionsHash :: Vector.Vector BlockItem -> Hash.Hash -computeTransactionsHash bis = - LFMBT.hashAsLFMBT - (Hash.hash "") - (v0TransactionHash . getHash <$> Vector.toList bis) +type BlockQuasiHash (pv :: ProtocolVersion) = BlockQuasiHash' (BlockHashVersionFor pv) -instance HashableTo BlockQuasiHash BakedBlock where +-- | Compute the hash from a list of transactions. +computeTransactionsHash :: SBlockHashVersion bhv -> Vector.Vector BlockItem -> Hash.Hash +computeTransactionsHash sbhv bis = case sbhv of + SBlockHashVersion0 -> + LFMBT.hashAsLFMBTV0 + (Hash.hash "") + (v0TransactionHash . getHash <$> Vector.toList bis) + SBlockHashVersion1 -> LFMBT.theLFMBTreeHash $ LFMBT.lfmbtHash' SBlockHashVersion1 (v0TransactionHash . getHash) bis + +instance (IsBlockHashVersion bhv) => HashableTo (BlockQuasiHash' bhv) (BakedBlock pv) where getHash BakedBlock{..} = BlockQuasiHash $ Hash.hashOfHashes metaHash dataHash where metaHash = Hash.hashOfHashes bakerInfoHash certificatesHash @@ -1169,22 +1182,30 @@ instance HashableTo BlockQuasiHash BakedBlock where where transactionsAndOutcomesHash = Hash.hashOfHashes transactionsHash outcomesHash where - transactionsHash = computeTransactionsHash bbTransactions + transactionsHash = computeTransactionsHash (sing @bhv) bbTransactions outcomesHash = tohGet bbTransactionOutcomesHash stateHash = v0StateHash bbStateHash +instance (IsProtocolVersion pv) => HashableTo SuccessorProof (BakedBlock pv) where + getHash = makeSuccessorProof @(BlockHashVersionFor pv) . getHash + -- | Compute the block hash from the header hash and quasi-hash. -computeBlockHash :: BlockHeaderHash -> BlockQuasiHash -> BlockHash -computeBlockHash bhh bqh = +computeBlockHash' :: BlockHeaderHash -> Hash.Hash -> BlockHash +{-# INLINE computeBlockHash' #-} +computeBlockHash' bhh bqh = BlockHash $ Hash.hashOfHashes (theBlockHeaderHash bhh) - (theBlockQuasiHash bqh) + bqh + +-- | Compute the block hash from the header hash and quasi-hash. +computeBlockHash :: BlockHeaderHash -> BlockQuasiHash' bhv -> BlockHash +computeBlockHash bhh bqh = computeBlockHash' bhh (theBlockQuasiHash bqh) -instance HashableTo BlockHash BakedBlock where - getHash bb = computeBlockHash (getHash bb) (getHash bb) +instance (IsProtocolVersion pv) => HashableTo BlockHash (BakedBlock pv) where + getHash bb = computeBlockHash @(BlockHashVersionFor pv) (getHash bb) (getHash bb) -instance (Monad m) => Merkle.MerkleProvable m BakedBlock where +instance (Monad m, IsProtocolVersion pv) => Merkle.MerkleProvable m (BakedBlock pv) where buildMerkleProof open BakedBlock{..} = do let blockHeader = optProof ["header"] . rawMerkle . runPut $ do put bbRound @@ -1209,7 +1230,7 @@ instance (Monad m) => Merkle.MerkleProvable m BakedBlock where let certificatesHash = optProof ["quasi", "meta", "certificatesHash"] [qcHash, tfHash] let blockMeta = optProof ["quasi", "meta"] [bakerInfo, certificatesHash] let transactionsAndOutcomes = optProof ["quasi", "data", "transactionsAndOutcomes"] . rawMerkle . runPut $ do - put $ computeTransactionsHash bbTransactions + put $ computeTransactionsHash (sing @(BlockHashVersionFor pv)) bbTransactions put bbTransactionOutcomesHash let blockData = optProof ["quasi", "data"] [transactionsAndOutcomes, Merkle.RawData (encode bbStateHash)] let blockQuasi = optProof ["quasi"] [blockMeta, blockData] @@ -1267,11 +1288,11 @@ instance Serialize GenesisMetadata where -- 'StateHash', which abstract from the genesis data. data Block (pv :: ProtocolVersion) = GenesisBlock !GenesisMetadata - | NormalBlock !SignedBlock + | NormalBlock !(SignedBlock pv) deriving (Eq, Show) instance BlockData (Block pv) where - type BakedBlockDataType (Block pv) = SignedBlock + type BakedBlockDataType (Block pv) = SignedBlock pv blockRound GenesisBlock{} = 0 blockRound (NormalBlock b) = blockRound b blockEpoch GenesisBlock{} = 0 @@ -1314,7 +1335,7 @@ getBlock ts = do (_ :: Round) <- get GenesisBlock <$> get _ -> do - NormalBlock <$> getSignedBlock (protocolVersion @pv) ts + NormalBlock <$> getSignedBlock ts -- | Deserialize a 'Block' where we already know the block hash. This behaves the same as 'getBlock', -- but avoids having to recompute the block hash. @@ -1339,7 +1360,7 @@ newtype BlockSignatureWitness = BlockSignatureWitness {bswBlockHash :: BlockHash deriving (Eq, Show) -- | Derive a 'BlockSignatureWitness' from a signed block. -toBlockSignatureWitness :: SignedBlock -> BlockSignatureWitness +toBlockSignatureWitness :: SignedBlock pv -> BlockSignatureWitness toBlockSignatureWitness = BlockSignatureWitness . getHash -- | A proof that contains the 'Epoch' for a 'QuorumCertificate' diff --git a/concordium-consensus/src/Concordium/MultiVersion.hs b/concordium-consensus/src/Concordium/MultiVersion.hs index fbbfde3b5d..231f077a91 100644 --- a/concordium-consensus/src/Concordium/MultiVersion.hs +++ b/concordium-consensus/src/Concordium/MultiVersion.hs @@ -1664,7 +1664,7 @@ receiveBlock gi blockBS = withLatestExpectedVersion gi $ \case (EVersionedConfigurationV1 (vc :: VersionedConfigurationV1 finconf pv)) -> do MVR $ \mvr -> do now <- currentTime - case SkovV1.deserializeExactVersionedPendingBlock (protocolVersion @pv) blockBS now of + case SkovV1.deserializeExactVersionedPendingBlock @pv blockBS now of Left err -> do mvLog mvr Runner LLDebug err return (Skov.ResultSerializationFail, Nothing) @@ -2039,7 +2039,7 @@ receiveExecuteBlock gi blockBS = withLatestExpectedVersion_ gi $ \case Right block -> runSkovV0Transaction vc (Skov.receiveExecuteBlock block) EVersionedConfigurationV1 (vc :: VersionedConfigurationV1 finconf pv) -> do now <- currentTime - case SkovV1.deserializeExactVersionedPendingBlock (protocolVersion @pv) blockBS now of + case SkovV1.deserializeExactVersionedPendingBlock @pv blockBS now of Left err -> do logEvent Runner LLDebug err return Skov.ResultSerializationFail diff --git a/concordium-consensus/src/Concordium/Queries.hs b/concordium-consensus/src/Concordium/Queries.hs index 59a18dea7d..9d487345da 100644 --- a/concordium-consensus/src/Concordium/Queries.hs +++ b/concordium-consensus/src/Concordium/Queries.hs @@ -1629,7 +1629,7 @@ getBlockCertificates = liftSkovQueryBHI (\_ -> return $ Left BlockCertificatesIn QueriesKonsensusV1.EpochFinalizationEntry { efeFinalizedQC = mkQuorumCertificateOut committee feFinalizedQuorumCertificate, efeSuccessorQC = mkQuorumCertificateOut committee feSuccessorQuorumCertificate, - efeSuccessorProof = QueriesKonsensusV1.SuccessorProof $ SkovV1.theBlockQuasiHash feSuccessorProof + efeSuccessorProof = QueriesKonsensusV1.SuccessorProof $ SkovV1.theSuccessorProof feSuccessorProof } -- | Error type for querying 'BakerRewardPeriodInfo' for some block. diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs index 22acf3b5e9..ab622a2e2f 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs @@ -76,7 +76,7 @@ credBi3 = tt = utcTimeToTransactionTime testTime -- | Valid block for round 1 with 1 credential deployment. -testBB1 :: BakedBlock +testBB1 :: BakedBlock PV testBB1 = BakedBlock { bbRound = 1, @@ -96,7 +96,7 @@ testBB1 = -- | Valid block for round 2. -- This block carries a QC for 'testBB1' thus certifying it. -testBB2 :: BakedBlock +testBB2 :: BakedBlock PV testBB2 = BakedBlock { bbRound = 2, @@ -116,7 +116,7 @@ testBB2 = -- | Valid block for round 3, finalizes 'testBB1' as this block -- carries a QC for 'testBB2'. -testBB3 :: BakedBlock +testBB3 :: BakedBlock PV testBB3 = BakedBlock { bbRound = 3, @@ -154,7 +154,7 @@ testDeployCredential = runTestMonad noBaker testTime genesisData $ do -- | Valid block for round 2. -- This block has one credential deployment. -- This block carries a QC for 'testBB1' thus certifying it. -testBB2' :: BakedBlock +testBB2' :: BakedBlock PV testBB2' = BakedBlock { bbRound = 2, @@ -174,7 +174,7 @@ testBB2' = -- | Valid block for round 3, carries a TC for round 2. -- This block has one credential deployment. -testBB3' :: BakedBlock +testBB3' :: BakedBlock PV testBB3' = BakedBlock { bbRound = 3, @@ -192,7 +192,7 @@ testBB3' = where bakerId = 4 -testBB4 :: BakedBlock +testBB4 :: BakedBlock PV testBB4 = BakedBlock { bbRound = 4, @@ -210,7 +210,7 @@ testBB4 = where bakerId = 3 -testBB5 :: BakedBlock +testBB5 :: BakedBlock PV testBB5 = BakedBlock { bbRound = 5, diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs index dcf4a2b255..dbd8c2de1f 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs @@ -45,7 +45,7 @@ transfer2 :: BlockItem transfer2 = normalTransaction $ addMetadata (\x -> NormalTransaction{biTransaction = x}) 1001 (biTransaction $ mkTransferTransaction 2) -- | Valid block for round 1 with 1 normal transfer -testBB1 :: BakedBlock +testBB1 :: BakedBlock PV testBB1 = BakedBlock { bbRound = 1, @@ -65,7 +65,7 @@ testBB1 = -- | Valid block for round 2. -- This block carries a QC for 'testBB1' thus certifying it. -testBB2 :: BakedBlock +testBB2 :: BakedBlock PV testBB2 = BakedBlock { bbRound = 2, @@ -85,7 +85,7 @@ testBB2 = -- | Valid block for round 3, finalizes 'testBB1' as this block -- carries a QC for 'testBB2'. -testBB3 :: BakedBlock +testBB3 :: BakedBlock PV testBB3 = BakedBlock { bbRound = 3, @@ -104,7 +104,7 @@ testBB3 = bakerId = 4 -- | Valid block for round 4 with 1 normal transfer -testBB4 :: BakedBlock +testBB4 :: BakedBlock PV testBB4 = BakedBlock { bbRound = 4, diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs index c984b8ccb6..391c55790f 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs @@ -39,7 +39,7 @@ assertCatchupResponse :: -- | The expected terminal data. CatchUpTerminalData -> -- | The expected blocks to be served. - [SignedBlock] -> + [SignedBlock (MPV m)] -> -- | The response. CatchUpPartialResponse m -> m () @@ -124,7 +124,7 @@ testQuorumMessage finIndex rnd e ptr = -- | Create a finalization entry where the @QuorumCertificate@ denotes the block being finalized, -- and the @BakedBlock@ is the successor block (which has a QC for the finalized block) and thus finalizing it. -testFinalizationEntry :: BakedBlock -> BakedBlock -> FinalizationEntry +testFinalizationEntry :: (IsProtocolVersion pv) => BakedBlock pv -> BakedBlock pv -> FinalizationEntry testFinalizationEntry finalizedBlock sucBlock = FinalizationEntry { feFinalizedQuorumCertificate = bbQuorumCertificate finalizedBlock, @@ -133,7 +133,7 @@ testFinalizationEntry finalizedBlock sucBlock = } -- | Timeout the provided round with a pointer to the proved block. -mkTimeout :: Round -> BakedBlock -> TestMonad 'P6 () +mkTimeout :: Round -> BakedBlock 'P6 -> TestMonad 'P6 () mkTimeout rnd bb = mapM_ ( \bid -> diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs index 3dedffdb00..dd82ab3dec 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs @@ -14,7 +14,7 @@ import Concordium.KonsensusV1.TreeState.Types import Concordium.KonsensusV1.Types import Concordium.Types import Concordium.Types.Option -import Concordium.Types.Transactions +import Concordium.Types.TransactionOutcomes import ConcordiumTests.KonsensusV1.TreeStateTest hiding (tests) -- | Just an arbitrary chosen block hash used for testing. @@ -38,13 +38,26 @@ someBlockPointer bh r e = } where -- A dummy block pointer with no meaningful state. - bakedBlock = BakedBlock r e 0 0 (dummyQuorumCertificate $ BlockHash minBound) Absent Absent dummyBlockNonce Vec.empty emptyTransactionOutcomesHashV1 (StateHashV0 $ Hash.hash "empty state hash") + bakedBlock = + BakedBlock + { bbRound = r, + bbEpoch = e, + bbTimestamp = 0, + bbBaker = 0, + bbQuorumCertificate = dummyQuorumCertificate $ BlockHash minBound, + bbTimeoutCertificate = Absent, + bbEpochFinalizationEntry = Absent, + bbNonce = dummyBlockNonce, + bbTransactions = Vec.empty, + bbTransactionOutcomesHash = toTransactionOutcomesHash emptyTransactionOutcomesHashV1, + bbStateHash = StateHashV0 $ Hash.hash "empty state hash" + } -- | A block pointer with 'myBlockHash' as block hash. myBlockPointer :: Round -> Epoch -> BlockPointer 'P6 myBlockPointer = someBlockPointer myBlockHash --- | A key pair created from the provided seeed. +-- | A key pair created from the provided seed. sigKeyPair' :: Int -> Sig.KeyPair sigKeyPair' seed = fst $ Dummy.randomBlockKeyPair $ mkStdGen seed diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs index f96864dd21..01505d5b1c 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} @@ -34,7 +35,7 @@ import qualified Concordium.Types.Transactions as Transactions import qualified Concordium.Genesis.Data.P6 as P6 import Concordium.GlobalState.BakerInfo -import Concordium.GlobalState.Basic.BlockState.LFMBTree (hashAsLFMBT) +import Concordium.GlobalState.Basic.BlockState.LFMBTree (hashAsLFMBTV0) import Concordium.GlobalState.BlockState (TransactionSummaryV1) import qualified Concordium.GlobalState.DummyData as Dummy import Concordium.KonsensusV1.Consensus @@ -48,6 +49,9 @@ import Concordium.KonsensusV1.Types import Concordium.Startup import Concordium.TimerMonad import Concordium.Types.Option +import qualified Concordium.Types.TransactionOutcomes as TransactionOutcomes + +type PV = 'P6 maxBaker :: (Integral a) => a maxBaker = 5 @@ -122,7 +126,7 @@ theFinalizers = [0 .. maxBaker] allFinalizers :: FinalizerSet allFinalizers = finalizerSet $ FinalizerIndex <$> [0 .. maxBaker] -validQCFor :: BakedBlock -> QuorumCertificate +validQCFor :: (IsProtocolVersion pv) => BakedBlock pv -> QuorumCertificate validQCFor bb = QuorumCertificate { qcSignatories = allFinalizers, @@ -142,10 +146,10 @@ validQCFor bb = } sig = fold [signQuorumSignatureMessage qsm (bakerAggKey i) | i <- theFinalizers] -validSignBlock :: BakedBlock -> SignedBlock +validSignBlock :: (IsProtocolVersion pv) => BakedBlock pv -> SignedBlock pv validSignBlock bb = signBlock (bakerKey (bbBaker bb)) genesisHash bb -invalidSignBlock :: BakedBlock -> SignedBlock +invalidSignBlock :: (IsProtocolVersion pv) => BakedBlock pv -> SignedBlock pv invalidSignBlock bb = signBlock (bakerKey (bbBaker bb)) (getHash bb) bb -- | Create a valid timeout message given a QC and a round. @@ -203,25 +207,25 @@ timeoutMessagesFor qc curRound curEpoch = mkTm <$> bakers transactionOutcomesHash :: [TransactionSummaryV1] -> [Transactions.SpecialTransactionOutcome] -> - Transactions.TransactionOutcomesHash + TransactionOutcomes.TransactionOutcomesHash transactionOutcomesHash outcomes specialOutcomes = - Transactions.TransactionOutcomesHash $ + TransactionOutcomes.TransactionOutcomesHash $ H.hashShort $ "TransactionOutcomesHashV1" <> H.hashToShortByteString out <> H.hashToShortByteString special where lfmbHash :: (HashableTo H.Hash a) => [a] -> H.Hash - lfmbHash = hashAsLFMBT (H.hash "EmptyLFMBTree") . fmap getHash + lfmbHash = hashAsLFMBTV0 (H.hash "EmptyLFMBTree") . fmap getHash out = lfmbHash outcomes special = lfmbHash specialOutcomes -- | Compute the transaction outcomes hash for a block with no transactions. -emptyBlockTOH :: BakerId -> Transactions.TransactionOutcomesHash +emptyBlockTOH :: BakerId -> TransactionOutcomes.TransactionOutcomesHash emptyBlockTOH bid = transactionOutcomesHash [] [BlockAccrueReward 0 0 0 0 0 0 bid] -- | Valid block for round 1. -testBB1 :: BakedBlock +testBB1 :: BakedBlock PV testBB1 = BakedBlock { bbRound = 1, @@ -240,7 +244,7 @@ testBB1 = bakerId = 2 -- | Valid block for round 2, descended from 'testBB1'. -testBB2 :: BakedBlock +testBB2 :: BakedBlock PV testBB2 = BakedBlock { bbRound = 2, @@ -259,7 +263,7 @@ testBB2 = bakerId = 4 -- | Valid block for round 3, descended from 'testBB2'. -testBB3 :: BakedBlock +testBB3 :: BakedBlock PV testBB3 = BakedBlock { bbRound = 3, @@ -278,7 +282,7 @@ testBB3 = bakerId = 4 -- | A valid block for round 2 where round 1 timed out. -testBB2' :: BakedBlock +testBB2' :: BakedBlock PV testBB2' = testBB2 { bbQuorumCertificate = genQC, @@ -289,7 +293,7 @@ testBB2' = genQC = genesisQuorumCertificate genesisHash -- | A valid block for round 3 descended from 'testBB2''. -testBB3' :: BakedBlock +testBB3' :: BakedBlock PV testBB3' = testBB3 { bbQuorumCertificate = validQCFor testBB2', @@ -297,7 +301,7 @@ testBB3' = } -- | A valid block for round 4 descended from 'testBB3''. -testBB4' :: BakedBlock +testBB4' :: BakedBlock PV testBB4' = BakedBlock { bbRound = 4, @@ -316,7 +320,7 @@ testBB4' = bakerId = 3 -- | A valid block for round 3 descended from the genesis block with a timeout for round 2. -testBB3'' :: BakedBlock +testBB3'' :: BakedBlock PV testBB3'' = testBB3 { bbQuorumCertificate = genQC, @@ -328,7 +332,7 @@ testBB3'' = -- | Valid block for round 1. -- This should be past the epoch transition trigger time. -testBB1E :: BakedBlock +testBB1E :: BakedBlock PV testBB1E = BakedBlock { bbRound = 1, @@ -347,7 +351,7 @@ testBB1E = bakerId = 2 -- | Valid block for round 2. Descends from 'testBB1E'. -testBB2E :: BakedBlock +testBB2E :: BakedBlock PV testBB2E = BakedBlock { bbRound = 2, @@ -368,7 +372,7 @@ testBB2E = -- | A block that is valid for round 3, descending from 'testBB2E', but which should not be validated -- by a finalizer because it is in epoch 0. With the QC for 'testBB2E', a finalizer should move into -- epoch 1, and thus refuse to validate this block. -testBB3EX :: BakedBlock +testBB3EX :: BakedBlock PV testBB3EX = BakedBlock { bbRound = 3, @@ -403,7 +407,7 @@ testEpochLEN = nonceForNewEpoch genesisFullBakers $ upd testBB1E genesisSeedStat -- | Valid block for round 3, epoch 1. Descends from 'testBB2E'. The finalization entry is -- 'testEpochFinEntry'. -testBB3E :: BakedBlock +testBB3E :: BakedBlock PV testBB3E = BakedBlock { bbRound = 3, @@ -425,7 +429,7 @@ testBB3E = -- 'testEpochFinEntry'. The block contains a valid timeout certificate for round 2. -- The block is not valid, because the highest round in the finalization entry is lower than the -- round of the parent block. -testBB3E' :: BakedBlock +testBB3E' :: BakedBlock PV testBB3E' = testBB3E { bbQuorumCertificate = validQCFor testBB1E, @@ -433,7 +437,7 @@ testBB3E' = } -- | Valid block for round 3, epoch 1. Descends from 'testBB3E'. -testBB4E :: BakedBlock +testBB4E :: BakedBlock PV testBB4E = BakedBlock { bbRound = 4, @@ -453,7 +457,7 @@ testBB4E = -- | Valid block for round 4 epoch 1. Descends from 'testBB2E', with finalization entry -- 'testEpochFinEntry'. The block contains a valid timeout for round 3. -testBB4E' :: BakedBlock +testBB4E' :: BakedBlock PV testBB4E' = testBB4E { bbQuorumCertificate = validQCFor testBB2E, @@ -464,7 +468,7 @@ testBB4E' = -- | Valid block for round 5, epoch 1. Descends from 'testBB3E'. The timeout certificate for round -- 4 spans epoch 0 and 1. -testBB5E' :: BakedBlock +testBB5E' :: BakedBlock PV testBB5E' = BakedBlock { bbRound = rnd, @@ -505,7 +509,7 @@ testBB5E' = finsA = take 3 [0 .. maxBaker] finsB = drop 3 [0 .. maxBaker] -testBB2Ex :: BakedBlock +testBB2Ex :: BakedBlock PV testBB2Ex = testBB2E { bbQuorumCertificate = genQC, @@ -521,7 +525,7 @@ testEpochLENx = nonceForNewEpoch genesisFullBakers $ upd testBB2Ex genesisSeedSt where upd b = updateSeedStateForBlock (bbTimestamp b) (bbNonce b) False -testBB3Ex :: BakedBlock +testBB3Ex :: BakedBlock PV testBB3Ex = BakedBlock { bbRound = 3, @@ -540,7 +544,7 @@ testBB3Ex = bakerId = 2 -- | Valid block in round 3 descended from 'testBB1E' with a timeout. -testBB3EA :: BakedBlock +testBB3EA :: BakedBlock PV testBB3EA = BakedBlock { bbRound = 3, @@ -560,7 +564,7 @@ testBB3EA = -- | Valid block in round 4, epoch 1, descended from 'testBB3EA', with a finalization proof based on -- 'testBB2E'. -testBB4EA :: BakedBlock +testBB4EA :: BakedBlock PV testBB4EA = BakedBlock { bbRound = 4, @@ -578,7 +582,7 @@ testBB4EA = where bakerId = 1 -succeedReceiveBlock :: PendingBlock -> TestMonad 'P6 () +succeedReceiveBlock :: PendingBlock 'P6 -> TestMonad 'P6 () succeedReceiveBlock pb = do res <- uponReceivingBlock pb case res of @@ -597,7 +601,7 @@ succeedReceiveBlock pb = do _ -> liftIO . assertFailure $ "Expected OnBlock event on executeBlock, but saw: " ++ show events _ -> liftIO . assertFailure $ "Expected BlockResultSuccess after uponReceivingBlock, but found: " ++ show res ++ "\n" ++ show pb -duplicateReceiveBlock :: PendingBlock -> TestMonad 'P6 () +duplicateReceiveBlock :: PendingBlock 'P6 -> TestMonad 'P6 () duplicateReceiveBlock pb = do res <- uponReceivingBlock pb case res of @@ -609,7 +613,7 @@ duplicateReceiveBlock pb = do _ -> liftIO . assertFailure $ "Expected BlockAlive after executeBlock, but found: " ++ show status ++ "\n" ++ show pb _ -> liftIO . assertFailure $ "Expected BlockResultDoubleSign after uponReceivingBlock, but found: " ++ show res ++ "\n" ++ show pb -succeedReceiveBlockFailExecute :: PendingBlock -> TestMonad 'P6 () +succeedReceiveBlockFailExecute :: PendingBlock 'P6 -> TestMonad 'P6 () succeedReceiveBlockFailExecute pb = do res <- uponReceivingBlock pb case res of @@ -622,7 +626,7 @@ succeedReceiveBlockFailExecute pb = do _ -> liftIO . assertFailure $ "Expected BlockUnknown or BlockDead after executeBlock, but found: " ++ show status ++ "\n" ++ show pb _ -> liftIO . assertFailure $ "Expected BlockResultSuccess after uponReceivingBlock, but found: " ++ show res ++ "\n" ++ show pb -duplicateReceiveBlockFailExecute :: PendingBlock -> TestMonad 'P6 () +duplicateReceiveBlockFailExecute :: PendingBlock 'P6 -> TestMonad 'P6 () duplicateReceiveBlockFailExecute pb = do res <- uponReceivingBlock pb case res of @@ -635,24 +639,24 @@ duplicateReceiveBlockFailExecute pb = do _ -> liftIO . assertFailure $ "Expected BlockUnknown or BlockDead after executeBlock, but found: " ++ show status ++ "\n" ++ show pb _ -> liftIO . assertFailure $ "Expected BlockResultDoubleSign after uponReceivingBlock, but found: " ++ show res ++ "\n" ++ show pb -pendingReceiveBlock :: PendingBlock -> TestMonad 'P6 () +pendingReceiveBlock :: PendingBlock 'P6 -> TestMonad 'P6 () pendingReceiveBlock pb = do res <- uponReceivingBlock pb case res of BlockResultPending -> return () _ -> liftIO . assertFailure $ "Expected BlockResultPending after uponReceivingBlock, but found: " ++ show res -staleReceiveBlock :: PendingBlock -> TestMonad 'P6 () +staleReceiveBlock :: PendingBlock 'P6 -> TestMonad 'P6 () staleReceiveBlock sb = do res <- uponReceivingBlock sb liftIO $ res `shouldBe` BlockResultStale -invalidReceiveBlock :: PendingBlock -> TestMonad 'P6 () +invalidReceiveBlock :: PendingBlock 'P6 -> TestMonad 'P6 () invalidReceiveBlock sb = do res <- uponReceivingBlock sb liftIO $ res `shouldBe` BlockResultInvalid -earlyReceiveBlock :: PendingBlock -> TestMonad 'P6 () +earlyReceiveBlock :: PendingBlock 'P6 -> TestMonad 'P6 () earlyReceiveBlock sb = do res <- uponReceivingBlock sb liftIO $ res `shouldBe` BlockResultEarly @@ -696,7 +700,7 @@ checkDead b = where bh = getHash b -signedPB :: BakedBlock -> PendingBlock +signedPB :: (IsProtocolVersion pv) => BakedBlock pv -> PendingBlock pv signedPB bb = PendingBlock { pbReceiveTime = timestampToUTCTime $ bbTimestamp bb, diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/LMDB.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/LMDB.hs index b7fc4d2912..2dbee1c39a 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/LMDB.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/LMDB.hs @@ -32,6 +32,7 @@ import Concordium.Logger import Concordium.Types import Concordium.Types.HashableTo import Concordium.Types.Option +import Concordium.Types.TransactionOutcomes import Concordium.Types.Transactions -- | A dummy UTCTime used for tests where the actual value is not significant. @@ -86,7 +87,7 @@ dummyBlockSig = Block.sign dummyKP "someMessage" -- | A helper function for creating a 'BakedBlock' given a round. Used by 'dummyBlock' to create blocks. -- The block is well-formed and contains the supplied transactions and is for the specified round. -- Beyond that, there should be no expectation on the data in the block. -dummyBakedBlock :: Round -> Vector.Vector BlockItem -> BakedBlock +dummyBakedBlock :: Round -> Vector.Vector BlockItem -> BakedBlock 'P6 dummyBakedBlock n ts = BakedBlock { bbRound = n, @@ -173,7 +174,7 @@ dummyStoredBlocks = -- | A FinalizationEntry. Both used by 'writeBlocks' and used when testing 'lookupLatestFinalizationEntry'. dummyFinalizationEntry :: FinalizationEntry dummyFinalizationEntry = - let feSuccessorProof = BlockQuasiHash dummyHash + let feSuccessorProof = SuccessorProof dummyHash feFinalizedQuorumCertificate = dummyQC succRound = qcRound feFinalizedQuorumCertificate + 1 feSuccessorQuorumCertificate = diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Quorum.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Quorum.hs index a0d7283b99..eee835ca2e 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Quorum.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Quorum.hs @@ -207,7 +207,7 @@ testReceiveQuorumMessage = describe "Receive quorum message" $ do & lastFinalized .~ finalizedBlockPointer (Round 0) 1 -- Run the 'receiveQuorumMessage' action. receiveAndCheck skovData qm expect = do - resultCode <- runTestLLDB (lldbWithGenesis @'P6) $ receiveQuorumMessage qm skovData + resultCode <- runTestLLDB lldbWithGenesis $ receiveQuorumMessage qm skovData resultCode `shouldBe` expect tests :: Spec diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs index df41f647ee..9bcf543727 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs @@ -491,7 +491,7 @@ testReceiveTimeoutMessage = describe "Receive timeout message" $ do & currentTimeoutMessages .~ Present (TimeoutMessages 0 (Map.singleton (FinalizerIndex 1) duplicateMessage) Map.empty) -- A low level database which consists of a finalized block for height 0 otherwise empty. lldb = - let myLLDB = lldbWithGenesis @'P6 + let myLLDB = lldbWithGenesis in myLLDB{lldbBlocks = HM.singleton someOldFinalizedBlockHash $ toStoredBlock (dummyBlock 20)} -- receive the timeout message in the provided tree state context and -- check that the result is as expected. diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs index e046f24afe..514dc9c422 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs @@ -63,6 +63,7 @@ import Concordium.Types.HashableTo import Concordium.Types.IdentityProviders import Concordium.Types.Option import Concordium.Types.Parameters +import Concordium.Types.TransactionOutcomes import Concordium.Types.Transactions import Concordium.GlobalState.Transactions @@ -455,7 +456,7 @@ testProcessBlockItems = describe "processBlockItems" $ do -- This block is not valid or makes much sense in the context -- of a chain. But it does have transactions and that is what we care -- about in this test. - blockToProcess :: [BlockItem] -> BakedBlock + blockToProcess :: [BlockItem] -> BakedBlock 'P6 blockToProcess txs = let bbRound = 1 bbEpoch = 0 diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TreeStateTest.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TreeStateTest.hs index c96e4fea5e..731281e576 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TreeStateTest.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TreeStateTest.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-} @@ -87,6 +88,7 @@ import Concordium.Scheduler.DummyData import Concordium.Types import Concordium.Types.Execution import Concordium.Types.HashableTo +import Concordium.Types.TransactionOutcomes import Concordium.Types.Transactions -- konsensus v1 related imports. @@ -167,7 +169,7 @@ dummyBakedBlock :: -- | The timestamp of the block Timestamp -> -- | The empty baked block - BakedBlock + BakedBlock pv dummyBakedBlock parentHash bbRound bbTimestamp = BakedBlock{..} where bbEpoch = 0 @@ -183,6 +185,7 @@ dummyBakedBlock parentHash bbRound bbTimestamp = BakedBlock{..} -- | Create a 'SignedBlock' by signing the -- 'dummyBakedBlock' with 'dummySignKeys' dummySignedBlock :: + (IsProtocolVersion pv) => -- | 'BlockHash' of the parent BlockHash -> -- | 'Round' of the block @@ -190,18 +193,19 @@ dummySignedBlock :: -- | Timestamp of the block Timestamp -> -- | The signed block - SignedBlock + SignedBlock pv dummySignedBlock parentHash rnd = signBlock dummySignKeys dummyGenesisBlockHash . dummyBakedBlock parentHash rnd -- | Construct a 'PendingBlock' for the provided 'Round' where the -- parent is indicated by the provided 'BlockHash'. dummyPendingBlock :: + (IsProtocolVersion pv) => -- | Parent 'BlockHash' BlockHash -> -- | The 'Timestamp' of the block Timestamp -> -- | The resulting 'PendingBlock' - PendingBlock + PendingBlock pv dummyPendingBlock parentHash ts = PendingBlock { pbBlock = dummySignedBlock parentHash 1 ts, @@ -210,6 +214,7 @@ dummyPendingBlock parentHash ts = -- | A 'BlockPointer' referrring to the 'dummySignedBlock' for the provided 'Round' dummyBlock :: + (IsProtocolVersion pv) => -- | 'Round' of the block that the created 'BlockPointer' should point to. Round -> -- | The 'BlockPointer' @@ -325,15 +330,15 @@ runTestLLDB initDB a = do -- Test values -genB :: BlockPointer pv +genB :: BlockPointer 'P6 genB = dummyBlock 0 -lastFin :: BlockPointer pv +lastFin :: BlockPointer 'P6 lastFin = dummyBlock 20 -testB :: BlockPointer pv +testB :: BlockPointer 'P6 testB = dummyBlock 21 -focusB :: BlockPointer pv +focusB :: BlockPointer 'P6 focusB = dummyBlock 22 -pendingB :: PendingBlock +pendingB :: PendingBlock 'P6 pendingB = dummyPendingBlock (BlockHash minBound) 33 deadH :: BlockHash deadH = BlockHash (Hash.hash "DeadBlock") @@ -354,9 +359,8 @@ toStoredBlock BlockPointer{..} = -- -- * 'testB' (alive) -- * 'focusB' (parent is 'testB') --- * 'pendingB' (pending) -- * the block indicated by 'deadH' has added to the dead cache. -skovDataWithTestBlocks :: SkovData pv +skovDataWithTestBlocks :: SkovData 'P6 skovDataWithTestBlocks = dummyInitialSkovData & lastFinalized .~ lastFin @@ -372,7 +376,7 @@ skovDataWithTestBlocks = ) -- | A test 'LowLevelDB' with the genesis block. -lldbWithGenesis :: LowLevelDB pv +lldbWithGenesis :: LowLevelDB 'P6 lldbWithGenesis = initialLowLevelDB sb @@ -406,7 +410,7 @@ testGetBlockStatus = describe "getBlockStatus" $ do it "unknown block" $ getStatus unknownH BlockUnknown where getStatus bh expect = do - s <- runTestLLDB (lldbWithGenesis @'P6) $ getBlockStatus bh sd + s <- runTestLLDB lldbWithGenesis $ getBlockStatus bh sd s `shouldBe` expect sd = skovDataWithTestBlocks @@ -424,7 +428,7 @@ testGetRecentBlockStatus = describe "getRecentBlockStatus" $ do it "unknown block" $ getStatus unknownH $ RecentBlock BlockUnknown where getStatus bh expect = do - s <- runTestLLDB (lldbWithGenesis @'P6) $ getRecentBlockStatus bh sd + s <- runTestLLDB lldbWithGenesis $ getRecentBlockStatus bh sd s `shouldBe` expect sd = skovDataWithTestBlocks @@ -612,7 +616,7 @@ testLookupTransaction = describe "lookupTransaction" $ do %~ addTrans 2 . addTrans 3 db = - (lldbWithGenesis @'P6) + lldbWithGenesis { lldbTransactions = HM.fromList [(txHash 1, FinalizedTransactionStatus 1 0)] } lookupAndCheck hsh expectedOutcome = do @@ -621,7 +625,7 @@ testLookupTransaction = describe "lookupTransaction" $ do -- | Testing 'getNonFinalizedAccountTransactions' -- This test ensures that: --- * An existing non finalized account transction can be looked up +-- * An existing non finalized account transaction can be looked up -- * Looking up with an unknown transaction hash will result in a 'Nothing' result. testGetNonFinalizedAccountTransactions :: Spec testGetNonFinalizedAccountTransactions = describe "getNonFinalizedAccountTransactions" $ do diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs index 69486ceb5f..395982039b 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs @@ -1,3 +1,7 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE TypeApplications #-} + -- | Testing of 'Concordium.KonsensusV1.Types' and 'Concordium.KonsensusV1.TreeState.Types' modules. module ConcordiumTests.KonsensusV1.Types where @@ -20,12 +24,12 @@ import qualified Concordium.Crypto.VRF as VRF import Concordium.Types import qualified Concordium.Types.DummyData as Dummy import Concordium.Types.Transactions -import qualified Concordium.Types.Transactions as Transactions import qualified Data.FixedByteString as FBS import Concordium.KonsensusV1.TreeState.Types import Concordium.KonsensusV1.Types import Concordium.Types.Option +import qualified Concordium.Types.TransactionOutcomes as TransactionOutcomes -- | Generate a 'FinalizerSet'. The size parameter determines the size of the committee that -- the finalizers are (nominally) sampled from. @@ -101,7 +105,7 @@ genFinalizationEntry :: Gen FinalizationEntry genFinalizationEntry = do feFinalizedQuorumCertificate <- genQuorumCertificate preQC <- genQuorumCertificate - feSuccessorProof <- BlockQuasiHash . Hash.Hash . FBS.pack <$> vector 32 + feSuccessorProof <- SuccessorProof . Hash.Hash . FBS.pack <$> vector 32 let succRound = qcRound feFinalizedQuorumCertificate + 1 let sqcEpoch = qcEpoch feFinalizedQuorumCertificate let feSuccessorQuorumCertificate = @@ -224,7 +228,7 @@ genTransactions = Vector.fromList <$> listOf trans -- | Generate an arbitrary baked block with no transactions. -- The baker of the block is number 42. -genBakedBlock :: Gen BakedBlock +genBakedBlock :: Gen (BakedBlock pv) genBakedBlock = do bbRound <- genRound bbEpoch <- genEpoch @@ -237,7 +241,9 @@ genBakedBlock = do BakedBlock { bbTimeoutCertificate = Absent, bbEpochFinalizationEntry = Absent, - bbTransactionOutcomesHash = Transactions.emptyTransactionOutcomesHashV1, + bbTransactionOutcomesHash = + TransactionOutcomes.toTransactionOutcomesHash + TransactionOutcomes.emptyTransactionOutcomesHashV1, bbBaker = 42, .. } @@ -246,7 +252,7 @@ genBakedBlock = do -- The signer of the block is chosen among the arbitrary signers. -- The baker of the block is number 42. -- This generator is suitable for testing serialization. -genSignedBlock :: Gen SignedBlock +genSignedBlock :: (IsProtocolVersion pv) => Gen (SignedBlock pv) genSignedBlock = do kp <- genBlockKeyPair bBlock <- genBakedBlock @@ -295,8 +301,8 @@ propSerializeBakedBlock = -- | Test that serializing then deserializing a signed block is the identity. propSerializeSignedBlock :: Property propSerializeSignedBlock = - forAll genSignedBlock $ \sb -> - case runGet (getSignedBlock SP6 (TransactionTime 42)) $! runPut (putSignedBlock sb) of + forAll (genSignedBlock @'P6) $ \sb -> + case runGet (getSignedBlock (TransactionTime 42)) $! runPut (putSignedBlock sb) of Left _ -> False Right sb' -> sb == sb' @@ -385,14 +391,14 @@ propSignQuorumSignatureMessageDiffBody = propSignBakedBlock :: Property propSignBakedBlock = - forAll genBakedBlock $ \bb -> + forAll (genBakedBlock @'P6) $ \bb -> forAll genBlockHash $ \genesisHash -> forAll genBlockKeyPair $ \kp@(Sig.KeyPair _ pk) -> (verifyBlockSignature pk genesisHash (signBlock kp genesisHash bb)) propSignBakedBlockDiffKey :: Property propSignBakedBlockDiffKey = - forAll genBakedBlock $ \bb -> + forAll (genBakedBlock @'P6) $ \bb -> forAll genBlockHash $ \genesisHash -> forAll genBlockKeyPair $ \kp -> forAll genBlockKeyPair $ \(Sig.KeyPair _ pk1) -> diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs b/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs index 5176a4e981..7a0e25a2ea 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs @@ -35,7 +35,7 @@ import Concordium.GlobalState.Block import qualified Concordium.GlobalState.BlockPointer as BS import Concordium.GlobalState.Parameters import Concordium.Types.IdentityProviders -import Concordium.Types.Transactions +import Concordium.Types.TransactionOutcomes import Concordium.Logger diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs index e7f62ae731..127bee1d73 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs @@ -66,9 +66,15 @@ testingFunction2 = do liftIO $ testElements'' `shouldBe` map Just ["A", "B", "Correct"] ++ [Nothing] ) -testHashAsLFMBT :: Property -testHashAsLFMBT = forAll (fmap BS.pack <$> listOf (vector 10)) $ \bs -> - LFMBT.hashAsLFMBT (H.hash "EmptyLFMBTree") (getHash <$> bs) === getHash (LFMBT.fromFoldable @Word64 bs) +testHashAsLFMBTV0 :: Property +testHashAsLFMBTV0 = forAll (fmap BS.pack <$> listOf (vector 10)) $ \bs -> + LFMBT.hashAsLFMBTV0 (H.hash "EmptyLFMBTree") (getHash <$> bs) + === LFMBT.theLFMBTreeHash @'BlockHashVersion0 getHash (LFMBT.fromFoldable @Word64 bs) + +testHashAsLFMBTV1 :: Property +testHashAsLFMBTV1 = forAll (fmap BS.pack <$> listOf (vector 10)) $ \bs -> + LFMBT.hashAsLFMBTV1 (H.hash "EmptyLFMBTree") (getHash <$> bs) + === LFMBT.theLFMBTreeHash @'BlockHashVersion1 getHash (LFMBT.fromFoldable @Word64 bs) tests :: Spec tests = @@ -80,5 +86,8 @@ tests = "Using BufferedRef" testingFunction2 it - "testHashAsLFMBT" - testHashAsLFMBT + "testHashAsLFMBTV0" + testHashAsLFMBTV0 + it + "testHashAsLFMBTV1" + testHashAsLFMBTV1 diff --git a/concordium-consensus/tools/database-exporter/Main.hs b/concordium-consensus/tools/database-exporter/Main.hs index 3125a5be78..660a996152 100644 --- a/concordium-consensus/tools/database-exporter/Main.hs +++ b/concordium-consensus/tools/database-exporter/Main.hs @@ -1,4 +1,7 @@ --- | This tools provides functionality for exporting a node database for use with the out-of-band +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | This tool provides functionality for exporting a node database for use with the out-of-band -- catch up mechanism. It also provides functionality for checking that such an exported set of -- blocks is correctly serialized. module Main where @@ -37,7 +40,7 @@ checkDatabase filepath = do logm _ lvl s = putStrLn $ show lvl ++ ": " ++ s handleImport :: (MonadLogger m) => UTCTime -> ImportData -> m (ImportResult a ()) handleImport t (ImportBlock pv gi bs) = case promoteProtocolVersion pv of - SomeProtocolVersion spv -> case consensusVersionFor spv of + SomeProtocolVersion (spv :: SProtocolVersion pv) -> case consensusVersionFor spv of ConsensusV0 -> case deserializeExactVersionedPendingBlock spv bs t of Left err -> do logEvent External LLError $ "Deserialization failed for consensus v0 block: " <> err @@ -45,7 +48,7 @@ checkDatabase filepath = do Right pb -> do logEvent External LLInfo $ "GenesisIndex: " ++ show gi ++ " block: " ++ show (pbHash pb) ++ " slot: " ++ show (blockSlot pb) return $ Right () - ConsensusV1 -> case SkovV1.deserializeExactVersionedPendingBlock spv bs t of + ConsensusV1 -> case SkovV1.deserializeExactVersionedPendingBlock @pv bs t of Left err -> do logEvent External LLError $ "Deserialization failed for consensus v1 block: " <> err return $ Left ImportSerializationFail diff --git a/concordium-node/Cargo.lock b/concordium-node/Cargo.lock index ca3cfcbd1b..22bc6b29b9 100644 --- a/concordium-node/Cargo.lock +++ b/concordium-node/Cargo.lock @@ -627,7 +627,7 @@ dependencies = [ [[package]] name = "concordium_base" -version = "3.1.1" +version = "3.2.0" dependencies = [ "aes", "anyhow", From 47bef93f9dcba68316b4e99ddbc9a79fc3dfb517 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Mon, 4 Dec 2023 14:08:49 +0100 Subject: [PATCH 04/20] Remove unused package stanza. --- concordium-consensus/package.yaml | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/concordium-consensus/package.yaml b/concordium-consensus/package.yaml index cb82661fa2..20945cd66a 100644 --- a/concordium-consensus/package.yaml +++ b/concordium-consensus/package.yaml @@ -202,32 +202,6 @@ executables: - concordium-consensus - clock - merkle: - main: Main.hs - source-dirs: test-runners/merkle - ghc-options: - - -threaded - - -rtsopts - - -with-rtsopts=-N - - -Wall - - -Wcompat - - -fno-ignore-asserts - when: - - condition: os(windows) - then: - ghc-options: -static - else: - when: - - condition: flag(dynamic) - then: - ghc-options: -dynamic - else: - ghc-options: -static - dependencies: - - concordium-consensus - - concordium-base - - database-exporter: main: Main.hs source-dirs: tools/database-exporter From 9c8a68107cfb49fd63dd9c3079952beccd715141 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Mon, 4 Dec 2023 17:16:12 +0100 Subject: [PATCH 05/20] Fixing tests. --- .../Concordium/Types/TransactionOutcomes.hs | 147 ++++++++++++++++++ .../tests/globalstate/Basic/AccountTable.hs | 138 ---------------- .../tests/globalstate/Basic/Accounts.hs | 52 ++++--- .../globalstate/GlobalStateTests/Accounts.hs | 11 +- .../globalstate/GlobalStateTests/BlockHash.hs | 11 +- .../globalstate/GlobalStateTests/LFMBTree.hs | 27 +++- .../GlobalStateTests/PersistentTreeState.hs | 30 +++- 7 files changed, 236 insertions(+), 180 deletions(-) create mode 100644 concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs delete mode 100644 concordium-consensus/tests/globalstate/Basic/AccountTable.hs diff --git a/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs b/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs new file mode 100644 index 0000000000..2a37d53d9a --- /dev/null +++ b/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs @@ -0,0 +1,147 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Concordium.Types.TransactionOutcomes where + +import qualified Data.Aeson as AE +import Data.Hashable +import qualified Data.Sequence as Seq +import qualified Data.Serialize as S +import qualified Data.Vector as Vec +import Lens.Micro.Internal +import Lens.Micro.Platform + +import qualified Concordium.Crypto.SHA256 as H +import Concordium.Types +import Concordium.Types.Execution +import Concordium.Types.HashableTo +import Concordium.Types.Transactions +import Concordium.Utils.Serialization + +-- | Outcomes of transactions. The vector of outcomes must have the same size as the +-- number of transactions in the block, and ordered in the same way. +data TransactionOutcomes = TransactionOutcomes + { outcomeValues :: !(Vec.Vector TransactionSummary), + _outcomeSpecial :: !(Seq.Seq SpecialTransactionOutcome) + } + +makeLenses ''TransactionOutcomes + +instance Show TransactionOutcomes where + show (TransactionOutcomes v s) = "Normal transactions: " ++ show (Vec.toList v) ++ ", special transactions: " ++ show s + +putTransactionOutcomes :: S.Putter TransactionOutcomes +putTransactionOutcomes TransactionOutcomes{..} = do + putListOf putTransactionSummary (Vec.toList outcomeValues) + S.put _outcomeSpecial + +getTransactionOutcomes :: SProtocolVersion pv -> S.Get TransactionOutcomes +getTransactionOutcomes spv = TransactionOutcomes <$> (Vec.fromList <$> getListOf (getTransactionSummary spv)) <*> S.get + +instance HashableTo (TransactionOutcomesHashV 'TOV0) TransactionOutcomes where + getHash transactionoutcomes = + TransactionOutcomesHashV . H.hash . S.runPut $ + putTransactionOutcomes transactionoutcomes + +-- | A simple wrapper around a 'H.Hash', that represents the hash of transaction outcomes in a +-- block. The type does not indicate how the hash is derived, which varies over versions. +-- (See 'TransactionOutcomesHashV'.) +-- +-- * If the 'TransactionOutcomesVersion' is 'TOV0', then the hash is the hash of a serialized +-- 'TransactionOutcomes' structure. (The 'BlockHashVersion' is irrelevant in this case.) +-- +-- * If the 'TransactionOutcomesVersion' is 'TOV1', then the hash is the hash of the string +-- @("TransactionOutcomesHashV1" <> outcomes <> special)@, where @outcomes@ and @special@ +-- are the version 0 LFMBTree hashes of the normal and special transaction outcomes +-- respectively. +-- +-- * If the 'TransactionOutcomesVersion' is 'TOV2', then the hash is the hash of the string +-- @(outcomes <> special)@, where @outcomes@ and @special@ are the version 1 LFMBTree hashes +-- of the normal and special transaction outcomes respectively. +newtype TransactionOutcomesHash = TransactionOutcomesHash {tohGet :: H.Hash} + deriving newtype (Eq, Ord, Show, S.Serialize, AE.ToJSON, AE.FromJSON, AE.FromJSONKey, AE.ToJSONKey, Read, Hashable) + +-- | A wrapper around a 'H.Hash', representing the hash of transaction outcomes in +-- a block. The type parameter indicates the hashing scheme used to derive the hash. +-- +-- * If the 'TransactionOutcomesVersion' is 'TOV0', then the hash is the hash of a serialized +-- 'TransactionOutcomes' structure. (The 'BlockHashVersion' is irrelevant in this case.) +-- +-- * If the 'TransactionOutcomesVersion' is 'TOV1', then the hash is the hash of the string +-- @("TransactionOutcomesHashV1" <> outcomes <> special)@, where @outcomes@ and @special@ +-- are the version 0 LFMBTree hashes of the normal and special transaction outcomes +-- respectively. +-- +-- * If the 'TransactionOutcomesVersion' is 'TOV2', then the hash is the hash of the string +-- @(outcomes <> special)@, where @outcomes@ and @special@ are the version 1 LFMBTree hashes +-- of the normal and special transaction outcomes respectively. +newtype TransactionOutcomesHashV (tov :: TransactionOutcomesVersion) = TransactionOutcomesHashV + { theTransactionOutcomesHashV :: H.Hash + } + deriving newtype (Eq, Ord, Show, S.Serialize, AE.ToJSON, AE.FromJSON, AE.FromJSONKey, AE.ToJSONKey, Read, Hashable) + +-- | Convert a 'TransactionOutcomesHashV' to a 'TransactionOutcomesHash'. This erases the +-- type-level information about the transaction outcomes hashing version used. +toTransactionOutcomesHash :: TransactionOutcomesHashV tov -> TransactionOutcomesHash +toTransactionOutcomesHash = TransactionOutcomesHash . theTransactionOutcomesHashV + +emptyTransactionOutcomesV0 :: TransactionOutcomes +emptyTransactionOutcomesV0 = TransactionOutcomes Vec.empty Seq.empty + +-- | Hash of the empty V0 transaction outcomes structure. This transaction outcomes +-- structure is used in protocol versions 1-5. +emptyTransactionOutcomesHashV0 :: TransactionOutcomesHashV 'TOV0 +emptyTransactionOutcomesHashV0 = getHash emptyTransactionOutcomesV0 + +-- | Hash of the empty V1 transaction outcomes structure. This transaction outcomes +-- structure is used in protocol versions 5 and 6. +emptyTransactionOutcomesHashV1 :: TransactionOutcomesHashV 'TOV1 +{-# NOINLINE emptyTransactionOutcomesHashV1 #-} +emptyTransactionOutcomesHashV1 = + TransactionOutcomesHashV $ + H.hashShort + ( "TransactionOutcomesHashV1" + <> H.hashToShortByteString (H.hash "EmptyLFMBTree") + <> H.hashToShortByteString (H.hash "EmptyLFMBTree") + ) + +-- | Hash of the empty V1 transaction outcomes structure. This transaction outcomes +-- structure is used starting in protocol version 7. +emptyTransactionOutcomesHashV2 :: TransactionOutcomesHashV 'TOV2 +{-# NOINLINE emptyTransactionOutcomesHashV2 #-} +emptyTransactionOutcomesHashV2 = + TransactionOutcomesHashV $ H.hashOfHashes emptyHash emptyHash + where + emptyHash = H.hash $ S.runPut $ do + S.putWord64be 0 + S.put (H.hash "EmptyLFMBTree") + +emptyTransactionOutcomesHashV :: STransactionOutcomesVersion tov -> TransactionOutcomesHashV tov +emptyTransactionOutcomesHashV stov = case stov of + STOV0 -> emptyTransactionOutcomesHashV0 + STOV1 -> emptyTransactionOutcomesHashV1 + STOV2 -> emptyTransactionOutcomesHashV2 + +transactionOutcomesV0FromList :: [TransactionSummary] -> TransactionOutcomes +transactionOutcomesV0FromList l = + let outcomeValues = Vec.fromList l + _outcomeSpecial = Seq.empty + in TransactionOutcomes{..} + +type instance Index TransactionOutcomes = TransactionIndex +type instance IxValue TransactionOutcomes = TransactionSummary + +instance Ixed TransactionOutcomes where + ix idx f outcomes@TransactionOutcomes{..} = + let x = fromIntegral idx + in if x >= length outcomeValues + then pure outcomes + else ix x f outcomeValues <&> (\ov -> TransactionOutcomes{outcomeValues = ov, ..}) diff --git a/concordium-consensus/tests/globalstate/Basic/AccountTable.hs b/concordium-consensus/tests/globalstate/Basic/AccountTable.hs deleted file mode 100644 index 7be9eb05d2..0000000000 --- a/concordium-consensus/tests/globalstate/Basic/AccountTable.hs +++ /dev/null @@ -1,138 +0,0 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE ViewPatterns #-} - -module Basic.AccountTable where - -import qualified Concordium.Crypto.SHA256 as H -import Concordium.GlobalState.Basic.BlockState.Account -import Concordium.Types -import Concordium.Types.HashableTo -import Data.Bits -import Data.Word -import Lens.Micro.Internal (Index, IxValue, Ixed) -import Lens.Micro.Platform -import Prelude hiding (lookup) - -data AccountTable (av :: AccountVersion) = Empty | Tree !(AT av) - -instance HashableTo H.Hash (AccountTable av) where - getHash Empty = H.hash "EmptyLFMBTree" -- this is the definition in the persistent implementation, I think it is acceptable to define it this way in the basic one - getHash (Tree t) = getHash t - -data AT (av :: AccountVersion) - = Branch !Word8 !Bool !H.Hash !(AT av) !(AT av) - | Leaf !(AccountHash av) (Account av) - -instance HashableTo H.Hash (AT av) where - getHash (Branch _ _ h _ _) = h - getHash (Leaf (AccountHash h) _) = h - -nextLevel :: AT av -> Word8 -nextLevel (Branch lvl _ _ _ _) = lvl + 1 -nextLevel (Leaf _ _) = 0 - -full :: AT av -> Bool -full (Branch _ f _ _ _) = f -full (Leaf _ _) = True - -mkLeaf :: (IsAccountVersion av) => Account av -> AT av -mkLeaf acct = Leaf (getHash acct) acct -{-# INLINE mkLeaf #-} - -mkBranch :: Word8 -> Bool -> AT av -> AT av -> AT av -mkBranch lvl f l r = Branch lvl f (H.hashShort $ H.hashToShortByteString (getHash l) <> H.hashToShortByteString (getHash r)) l r -{-# INLINE mkBranch #-} - -empty :: AccountTable av -empty = Empty - -lookup' :: AccountIndex -> AT av -> Maybe (Account av) -lookup' 0 (Leaf _ acct) = Just acct -lookup' _ (Leaf _ _) = Nothing -lookup' x (Branch (fromIntegral -> branchBit) _ _ l r) - | testBit x branchBit = lookup' (clearBit x branchBit) r - | otherwise = lookup' x l - -lookup :: AccountIndex -> AccountTable av -> Maybe (Account av) -lookup _ Empty = Nothing -lookup x (Tree t) = lookup' x t - -append :: forall av. (IsAccountVersion av) => Account av -> AccountTable av -> (AccountIndex, AccountTable av) -append acct Empty = (0, Tree (Leaf (getHash acct) acct)) -append acct (Tree t) = append' t & _2 %~ Tree - where - append' :: AT av -> (AccountIndex, AT av) - append' l@(Leaf h _) = (1, Branch 0 True branchHash l newLeaf) - where - branchHash = - H.hashShort $ - H.hashToShortByteString (theAccountHash h) - <> H.hashToShortByteString (theAccountHash newHash) - append' b@(Branch lvl True _ _ _) = (bit (fromIntegral lvl + 1), mkBranch (lvl + 1) False b newLeaf) - append' (Branch lvl False _ l r) = - let (i', r') = append' r - in ( setBit i' (fromIntegral lvl), - mkBranch lvl (full r' && nextLevel r' == lvl) l r' - ) - newLeaf = Leaf newHash acct - newHash = getHash acct - --- | Get the size of an 'AccountTable'. -size :: AccountTable av -> Word64 -size Empty = 0 -size (Tree t) = size' t - where - size' (Leaf _ _) = 1 - size' (Branch lvl True _ _ _) = bit (fromIntegral lvl + 1) - size' (Branch lvl False _ _ r) = setBit (size' r) (fromIntegral lvl) - -type instance Index (AT av) = AccountIndex -type instance IxValue (AT av) = Account av - -instance (IsAccountVersion av) => Ixed (AT av) where - ix i upd l@(Leaf _ acct) - | i == 0 = mkLeaf <$> upd acct - | otherwise = pure l - ix i upd (Branch lvl f _ l r) - | testBit i (fromIntegral lvl) = mkBranch lvl f l <$> ix (clearBit i (fromIntegral lvl)) upd r - | otherwise = (\l' -> mkBranch lvl f l' r) <$> ix i upd l - -type instance Index (AccountTable av) = AccountIndex -type instance IxValue (AccountTable av) = Account av - -instance (IsAccountVersion av) => Ixed (AccountTable av) where - ix _ _ Empty = pure Empty - ix i upd a@(Tree t) - | i < bit (fromIntegral (nextLevel t)) = Tree <$> ix i upd t - | otherwise = pure a - -toList :: AccountTable av -> [(AccountIndex, Account av)] -toList Empty = [] -toList (Tree t) = toL 0 t - where - toL o (Leaf _ a) = [(o, a)] - toL o (Branch lvl _ _ t1 t2) = toL o t1 ++ toL (setBit o (fromIntegral lvl)) t2 - --- | Convert the account table to a list of accounts with their hashes. --- The accounts are in ascending index order. -toHashedList :: AccountTable av -> [Hashed' (AccountHash av) (Account av)] -toHashedList Empty = [] -toHashedList (Tree t) = toHL t - where - toHL (Leaf h a) = [Hashed a h] - toHL (Branch _ _ _ t1 t2) = toHL t1 ++ toHL t2 - --- | Strict fold over the account table in increasing order of account index. -foldl' :: (a -> Account av -> a) -> a -> AccountTable av -> a -foldl' _ a Empty = a -foldl' f !a0 (Tree t) = ft a0 t - where - ft a (Leaf _ acct) = f a acct - ft a (Branch _ _ _ l r) = - let !a1 = ft a l - !a2 = ft a1 r - in a2 diff --git a/concordium-consensus/tests/globalstate/Basic/Accounts.hs b/concordium-consensus/tests/globalstate/Basic/Accounts.hs index 3a51cc50b2..2a0769f548 100644 --- a/concordium-consensus/tests/globalstate/Basic/Accounts.hs +++ b/concordium-consensus/tests/globalstate/Basic/Accounts.hs @@ -1,15 +1,18 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} module Basic.Accounts where -import qualified Concordium.Crypto.SHA256 as H import Concordium.Genesis.Data import qualified Concordium.GlobalState.AccountMap as AccountMap import Concordium.GlobalState.Basic.BlockState.Account import Concordium.GlobalState.Basic.BlockState.AccountReleaseSchedule +import qualified Concordium.GlobalState.Basic.BlockState.LFMBTree as LFMBTree +import Concordium.GlobalState.BlockState (AccountsHash (..)) import Concordium.ID.Parameters import qualified Concordium.ID.Types as ID import Concordium.Types @@ -18,13 +21,12 @@ import Concordium.Types.HashableTo import Control.Monad import Data.Foldable import qualified Data.Map.Strict as Map +import qualified Data.Sequence as Seq import Data.Serialize import GHC.Stack (HasCallStack) import Lens.Micro.Internal (Index, IxValue, Ixed) import Lens.Micro.Platform -import qualified Basic.AccountTable as AT - -- | Representation of the set of accounts on the chain. -- Each account has an 'AccountIndex' which is the order -- in which it was created. @@ -46,7 +48,7 @@ data Accounts (pv :: ProtocolVersion) = Accounts { -- | Unique index of accounts by 'AccountAddress' accountMap :: !(AccountMap.PureAccountMap pv), -- | Hashed Merkle-tree of the accounts. - accountTable :: !(AT.AccountTable (AccountVersionFor pv)), + accountTable :: !(Seq.Seq (Account (AccountVersionFor pv))), -- | A mapping of 'ID.CredentialRegistrationID's to accounts on which they are used. accountRegIds :: !(Map.Map ID.RawCredentialRegistrationID AccountIndex) } @@ -54,11 +56,12 @@ data Accounts (pv :: ProtocolVersion) = Accounts instance (IsProtocolVersion pv) => Show (Accounts pv) where show Accounts{..} = "Accounts {\n" ++ (concatMap showAcct . AccountMap.toListPure $ accountMap) ++ "accountRegIds = " ++ show accountRegIds ++ "\n}" where - showAcct (addr, ind) = show addr ++ " => " ++ maybe "MISSING" show (accountTable ^? ix ind) ++ "\n" + showAcct (addr, ind) = + show addr ++ " => " ++ maybe "MISSING" show (accountTable ^? ix (fromIntegral ind)) ++ "\n" -- | An 'Accounts' with no accounts. emptyAccounts :: Accounts pv -emptyAccounts = Accounts AccountMap.empty AT.Empty Map.empty +emptyAccounts = Accounts AccountMap.empty Seq.Empty Map.empty -- | Add or modify a given account. -- If an account matching the given account's address does not exist, @@ -78,10 +81,11 @@ putAccount !acct = snd . putAccountWithIndex acct putAccountWithIndex :: (IsProtocolVersion pv) => Account (AccountVersionFor pv) -> Accounts pv -> (AccountIndex, Accounts pv) putAccountWithIndex !acct Accounts{..} = case AccountMap.lookupPure addr accountMap of - Nothing -> - let (i, newAccountTable) = AT.append acct accountTable - in (i, Accounts (AccountMap.insertPure addr i accountMap) newAccountTable accountRegIds) - Just i -> (i, Accounts accountMap (accountTable & ix i .~ acct) accountRegIds) + Nothing -> (i, Accounts (AccountMap.insertPure addr i accountMap) newAccountTable accountRegIds) + where + i = fromIntegral $ Seq.length accountTable + newAccountTable = accountTable Seq.|> acct + Just i -> (i, Accounts accountMap (accountTable & ix (fromIntegral i) .~ acct) accountRegIds) where addr = acct ^. accountAddress @@ -109,7 +113,7 @@ exists addr Accounts{..} = AccountMap.isAddressAssignedPure addr accountMap getAccount :: (IsProtocolVersion pv) => AccountAddress -> Accounts pv -> Maybe (Account (AccountVersionFor pv)) getAccount addr Accounts{..} = case AccountMap.lookupPure addr accountMap of Nothing -> Nothing - Just i -> accountTable ^? ix i + Just i -> accountTable ^? ix (fromIntegral i) getAccountIndex :: (IsProtocolVersion pv) => AccountAddress -> Accounts pv -> Maybe AccountIndex getAccountIndex addr Accounts{..} = AccountMap.lookupPure addr accountMap @@ -119,11 +123,11 @@ getAccountIndex addr Accounts{..} = AccountMap.lookupPure addr accountMap getAccountWithIndex :: (IsProtocolVersion pv) => AccountAddress -> Accounts pv -> Maybe (AccountIndex, Account (AccountVersionFor pv)) getAccountWithIndex addr Accounts{..} = case AccountMap.lookupPure addr accountMap of Nothing -> Nothing - Just i -> (i,) <$> accountTable ^? ix i + Just i -> (i,) <$> accountTable ^? ix (fromIntegral i) -- | Traversal for accessing the account at a given index. indexedAccount :: (IsProtocolVersion pv) => AccountIndex -> Traversal' (Accounts pv) (Account (AccountVersionFor pv)) -indexedAccount ai = lens accountTable (\a v -> a{accountTable = v}) . ix ai +indexedAccount ai = lens accountTable (\a v -> a{accountTable = v}) . ix (fromIntegral ai) -- | Lens for accessing the account at a given index, assuming the account exists. -- If the account does not exist, this throws an error. @@ -163,7 +167,7 @@ updateAccount !upd = unsafeGetAccount :: (IsProtocolVersion pv) => AccountAddress -> Accounts pv -> Account (AccountVersionFor pv) unsafeGetAccount addr Accounts{..} = case AccountMap.lookupPure addr accountMap of Nothing -> error $ "unsafeGetAccount: Account " ++ show addr ++ " does not exist." - Just i -> accountTable ^?! ix i + Just i -> accountTable ^?! ix (fromIntegral i) -- | Check whether the given address would clash with any existing addresses in -- the accounts structure. The meaning of this depends on the protocol version. @@ -189,8 +193,12 @@ recordRegIds rids accs = accs{accountRegIds = Map.union (accountRegIds accs) (Ma -- since credentials can only be used on one account the union is well-defined, the maps should be disjoint. -instance HashableTo H.Hash (Accounts pv) where - getHash Accounts{..} = getHash accountTable +instance (IsProtocolVersion pv) => HashableTo (AccountsHash pv) (Accounts pv) where + getHash = + AccountsHash + . LFMBTree.theLFMBTreeHash + . LFMBTree.lfmbtHash (sBlockHashVersionFor (protocolVersion @pv)) + . accountTable type instance Index (Accounts pv) = AccountAddress type instance IxValue (Accounts pv) = (Account (AccountVersionFor pv)) @@ -199,21 +207,21 @@ instance (IsProtocolVersion pv) => Ixed (Accounts pv) where ix addr f acc@Accounts{..} = case AccountMap.lookupPure addr accountMap of Nothing -> pure acc - Just i -> (\atable -> acc{accountTable = atable}) <$> ix i f accountTable + Just i -> (\atable -> acc{accountTable = atable}) <$> ix (fromIntegral i) f accountTable -- | Convert an 'Accounts' to a list of 'Account's with their indexes. accountList :: Accounts pv -> [(AccountIndex, Account (AccountVersionFor pv))] -accountList = AT.toList . accountTable +accountList = zip [0 ..] . toList . accountTable -- | Fold over the account table in ascending order of account index. foldAccounts :: (a -> Account (AccountVersionFor pv) -> a) -> a -> Accounts pv -> a -foldAccounts f a = AT.foldl' f a . accountTable +foldAccounts f a = foldl' f a . accountTable -- | Serialize 'Accounts' in V0 format. serializeAccounts :: (IsProtocolVersion pv) => GlobalContext -> Putter (Accounts pv) serializeAccounts cryptoParams Accounts{..} = do - putWord64be $ AT.size accountTable - forM_ (AT.toList accountTable) $ \(_, acct) -> serializeAccount cryptoParams acct + putWord64be $ fromIntegral $ Seq.length accountTable + forM_ accountTable $ \acct -> serializeAccount cryptoParams acct -- | Deserialize 'Accounts'. The serialization format may depend on the protocol version. -- The state migration determines how do construct an 'Accounts' at a new protocol version 'pv' @@ -245,7 +253,7 @@ deserializeAccounts migration cryptoParams = do (i + 1) Accounts { accountMap = AccountMap.insertPure (acct ^. accountAddress) acctId accountMap, - accountTable = snd (AT.append acct accountTable), + accountTable = accountTable Seq.|> acct, accountRegIds = newRegIds } | otherwise = return accts diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs index 489773cf49..3a035983f5 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs @@ -5,16 +5,15 @@ module GlobalStateTests.Accounts where -import qualified Basic.AccountTable as BAT import qualified Basic.Accounts as B import Concordium.Crypto.DummyData import Concordium.Crypto.FFIDataTypes -import qualified Concordium.Crypto.SHA256 as H import qualified Concordium.Crypto.SignatureScheme as Sig import qualified Concordium.GlobalState.AccountMap as AccountMap import qualified Concordium.GlobalState.AccountMap.DifferenceMap as DiffMap import qualified Concordium.GlobalState.AccountMap.LMDB as LMDBAccountMap import Concordium.GlobalState.Basic.BlockState.Account as BA +import Concordium.GlobalState.BlockState (AccountsHash) import Concordium.GlobalState.DummyData import qualified Concordium.GlobalState.Persistent.Account as PA import qualified Concordium.GlobalState.Persistent.Accounts as P @@ -73,13 +72,13 @@ checkEquivalent :: (P.SupportsPersistentAccount PV m, av ~ AccountVersionFor PV) checkEquivalent ba pa = do addrsAndIndices <- P.allAccounts pa checkBinary (==) (AccountMap.toMapPure (B.accountMap ba)) (Map.fromList addrsAndIndices) "==" "Basic account map" "Persistent account map" - let bat = BAT.toList (B.accountTable ba) + let bat = B.accountList ba pat <- L.toAscPairList (P.accountTable pa) bpat <- mapM (_2 PA.toTransientAccount) pat checkBinary (==) bat bpat "==" "Basic account table (as list)" "Persistent account table (as list)" - let bath = getHash (B.accountTable ba) :: H.Hash - path <- getHashM (P.accountTable pa) - checkBinary (==) bath path "==" "Basic account table hash" "Persistent account table hash" + let bath = getHash ba :: AccountsHash PV + path <- getHashM pa + checkBinary (==) bath path "==" "Basic accounts hash" "Persistent accounts hash" pregids <- P.loadRegIds pa checkBinary (==) (B.accountRegIds ba) pregids "==" "Basic registration ids" "Persistent registration ids" diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/BlockHash.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/BlockHash.hs index 11f0e54ab2..436748f99b 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/BlockHash.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/BlockHash.hs @@ -35,6 +35,7 @@ import Concordium.GlobalState.Persistent.BlobStore import Concordium.GlobalState.Persistent.BlockState (emptyPersistentTransactionOutcomes) import Concordium.Types.DummyData import Concordium.Types.HashableTo +import Concordium.Types.TransactionOutcomes import Control.Monad.IO.Class import System.Random @@ -196,9 +197,13 @@ tests = do defaultHash `shouldNotBe` hash' specify "Hash of emptyPersistentTransactionOutcomes (TOV0) is hash of emptyTransactionOutcomesV0" $ - runDummyHashMonad (getHashM @_ @TransactionOutcomesHash (emptyPersistentTransactionOutcomes @'TOV0)) - `shouldBe` getHash emptyTransactionOutcomesV0 + runDummyHashMonad (getHashM (emptyPersistentTransactionOutcomes @'TOV0)) + `shouldBe` getHash @(TransactionOutcomesHashV 'TOV0) emptyTransactionOutcomesV0 specify "Hash of emptyPersistentTransactionOutcomes (TOV1) is emptyTransactionOutcomesHashV1" $ - runDummyHashMonad (getHashM @_ @TransactionOutcomesHash (emptyPersistentTransactionOutcomes @'TOV1)) + runDummyHashMonad (getHashM (emptyPersistentTransactionOutcomes @'TOV1)) `shouldBe` emptyTransactionOutcomesHashV1 + + specify "Hash of emptyPersistentTransactionOutcomes (TOV2) is emptyTransactionOutcomesHashV2" $ + runDummyHashMonad (getHashM (emptyPersistentTransactionOutcomes @'TOV2)) + `shouldBe` emptyTransactionOutcomesHashV2 diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs index 127bee1d73..b0a623b8ea 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -16,17 +17,27 @@ import qualified Concordium.GlobalState.Basic.BlockState.LFMBTree as LFMBT import Concordium.GlobalState.Persistent.BlobStore import Concordium.GlobalState.Persistent.LFMBTree import Concordium.Types.HashableTo +import Concordium.Types.ProtocolVersion import Control.Monad import Control.Monad.IO.Class import qualified Data.ByteString as BS +import qualified Data.Serialize as S import Data.Word import Test.Hspec import Test.QuickCheck import Prelude hiding (lookup) -abcHash, abcorrectHash :: H.Hash -abcHash = H.hashOfHashes (H.hashOfHashes (H.hash "A") (H.hash "B")) (H.hash "C") -- "dbe11e36aa89a963103de7f8ad09c1100c06ccd5c5ad424ca741efb0689dc427" -abcorrectHash = H.hashOfHashes (H.hashOfHashes (H.hash "A") (H.hash "B")) (H.hash "Correct") -- "084aeef37cbdb2e19255853cbae6d22c78aaaea7273aa39af4db96cc62c9bdac" +abcHash, abcorrectHash :: LFMBT.LFMBTreeHashV0 +abcHash = LFMBT.LFMBTreeHash $ H.hashOfHashes (H.hashOfHashes (H.hash "A") (H.hash "B")) (H.hash "C") -- "dbe11e36aa89a963103de7f8ad09c1100c06ccd5c5ad424ca741efb0689dc427" +abcorrectHash = LFMBT.LFMBTreeHash $ H.hashOfHashes (H.hashOfHashes (H.hash "A") (H.hash "B")) (H.hash "Correct") -- "084aeef37cbdb2e19255853cbae6d22c78aaaea7273aa39af4db96cc62c9bdac" + +abcHashV1, abcorrectHashV1 :: LFMBT.LFMBTreeHashV1 +abcHashV1 = LFMBT.LFMBTreeHash . H.hashLazy . S.runPutLazy $ do + S.putWord64be 3 + S.put abcHash +abcorrectHashV1 = LFMBT.LFMBTreeHash . H.hashLazy . S.runPutLazy $ do + S.putWord64be 3 + S.put abcorrectHash testingFunction :: IO () testingFunction = do @@ -36,17 +47,17 @@ testingFunction = do tree <- foldM (\acc v -> snd <$> append v acc) (empty :: LFMBTree Word64 HashedBufferedRef BS.ByteString) ["A", "B", "C"] testElements <- mapM (`lookup` tree) [0 .. 3] liftIO $ testElements `shouldBe` map Just ["A", "B", "C"] ++ [Nothing] - h <- getHashM tree :: BlobStoreM H.Hash + h <- getHashM tree liftIO $ h `shouldBe` abcHash tree' <- loadRef =<< (storeRef tree :: BlobStoreM (BlobRef (LFMBTree Word64 HashedBufferedRef BS.ByteString))) testElements' <- mapM (`lookup` tree') [0 .. 3] liftIO $ testElements' `shouldBe` map Just ["A", "B", "C"] ++ [Nothing] - h' <- getHashM tree' :: BlobStoreM H.Hash + h' <- getHashM tree' liftIO $ h' `shouldBe` abcHash Just (_, tree'') <- update (\v -> return ((), v `BS.append` "orrect")) 2 tree' testElements'' <- mapM (`lookup` tree'') [0 .. 3] liftIO $ testElements'' `shouldBe` map Just ["A", "B", "Correct"] ++ [Nothing] - h'' <- getHashM tree'' :: BlobStoreM H.Hash + h'' <- getHashM tree'' liftIO $ h'' `shouldBe` abcorrectHash ) @@ -69,12 +80,12 @@ testingFunction2 = do testHashAsLFMBTV0 :: Property testHashAsLFMBTV0 = forAll (fmap BS.pack <$> listOf (vector 10)) $ \bs -> LFMBT.hashAsLFMBTV0 (H.hash "EmptyLFMBTree") (getHash <$> bs) - === LFMBT.theLFMBTreeHash @'BlockHashVersion0 getHash (LFMBT.fromFoldable @Word64 bs) + === LFMBT.theLFMBTreeHash @'BlockHashVersion0 (getHash (LFMBT.fromFoldable @Word64 bs)) testHashAsLFMBTV1 :: Property testHashAsLFMBTV1 = forAll (fmap BS.pack <$> listOf (vector 10)) $ \bs -> LFMBT.hashAsLFMBTV1 (H.hash "EmptyLFMBTree") (getHash <$> bs) - === LFMBT.theLFMBTreeHash @'BlockHashVersion1 getHash (LFMBT.fromFoldable @Word64 bs) + === LFMBT.theLFMBTreeHash @'BlockHashVersion1 (getHash (LFMBT.fromFoldable @Word64 bs)) tests :: Spec tests = diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs index f73e70cf33..4cfdff8475 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs @@ -37,7 +37,7 @@ import Concordium.Types import Concordium.Types.AnonymityRevokers import Concordium.Types.HashableTo import Concordium.Types.IdentityProviders -import qualified Concordium.Types.Transactions as Trns +import qualified Concordium.Types.TransactionOutcomes as Trns import Control.Exception import Control.Monad.Identity import Control.Monad.RWS.Strict as RWS hiding (state) @@ -107,7 +107,19 @@ testFinalizeABlock = do let proof2 = VRF.prove (fst $ randomKeyPair (mkStdGen 1)) "proof2" now <- liftIO $ getCurrentTime -- FIXME: Statehash is stubbed out with a placeholder hash - pb <- makePendingBlock (fst $ randomBlockKeyPair (mkStdGen 1)) 1 (bpHash genesisBlock) 0 proof1 proof2 NoFinalizationData [] (StateHashV0 minBound) (getHash Trns.emptyTransactionOutcomesV0) now + pb <- + makePendingBlock + (fst $ randomBlockKeyPair (mkStdGen 1)) + 1 + (bpHash genesisBlock) + 0 + proof1 + proof2 + NoFinalizationData + [] + (StateHashV0 minBound) + (Trns.toTransactionOutcomesHash Trns.emptyTransactionOutcomesHashV0) + now now' <- liftIO $ getCurrentTime blockPtr :: BlockPointerType TestM <- makeLiveBlock pb genesisBlock genesisBlock state now' 0 @@ -149,7 +161,19 @@ testFinalizeABlock = do -- add another block with different lfin and parent now'' <- liftIO $ getCurrentTime -- FIXME: statehash is stubbed out with a palceholder stash - pb2 <- makePendingBlock (fst $ randomBlockKeyPair (mkStdGen 1)) 2 (bpHash blockPtr) 0 proof1 proof2 NoFinalizationData [] (StateHashV0 minBound) (getHash Trns.emptyTransactionOutcomesV0) now'' + pb2 <- + makePendingBlock + (fst $ randomBlockKeyPair (mkStdGen 1)) + 2 + (bpHash blockPtr) + 0 + proof1 + proof2 + NoFinalizationData + [] + (StateHashV0 minBound) + (Trns.toTransactionOutcomesHash Trns.emptyTransactionOutcomesHashV0) + now'' now''' <- liftIO $ getCurrentTime blockPtr2 :: BlockPointerType TestM <- makeLiveBlock pb2 blockPtr genesisBlock state now''' 0 let frec2 = FinalizationRecord 2 (bpHash blockPtr2) (FinalizationProof [1] (sign "Hello" sk)) 0 From e44a8c398a3b21008caaf89c09a8d68fd7d649b6 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Tue, 5 Dec 2023 11:20:55 +0100 Subject: [PATCH 06/20] Instances hashing. --- .../src/Concordium/GlobalState/BlockState.hs | 8 ++++++-- .../GlobalState/Persistent/Instances.hs | 19 +++++++++++++++---- .../KonsensusV1/Consensus/Blocks.hs | 6 ------ .../Concordium/Types/TransactionOutcomes.hs | 5 ++--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs index b76a3087ae..6a871afa95 100644 --- a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs @@ -111,6 +111,10 @@ newtype AccountsHash (pv :: ProtocolVersion) = AccountsHash {theAccountsHash :: newtype ModulesHash (pv :: ProtocolVersion) = ModulesHash {theModulesHash :: H.Hash} deriving newtype (Eq, Ord, Show, Serialize) +-- | Hash associated with the instances table. +newtype InstancesHash (pv :: ProtocolVersion) = InstancesHash {theInstancesHash :: H.Hash} + deriving newtype (Eq, Ord, Show, Serialize) + -- | The hashes of the block state components, which are combined -- to produce a 'StateHash'. data BlockStateHashInputs (pv :: ProtocolVersion) = BlockStateHashInputs @@ -121,7 +125,7 @@ data BlockStateHashInputs (pv :: ProtocolVersion) = BlockStateHashInputs bshModules :: ModulesHash pv, bshBankStatus :: H.Hash, bshAccounts :: AccountsHash pv, - bshInstances :: H.Hash, + bshInstances :: InstancesHash pv, bshUpdates :: H.Hash, bshBlockRewardDetails :: BlockRewardDetailsHash pv } @@ -139,7 +143,7 @@ makeBlockStateHash BlockStateHashInputs{..} = ) ( H.hashOfHashes (H.hashOfHashes (theModulesHash bshModules) bshBankStatus) - (H.hashOfHashes (theAccountsHash bshAccounts) bshInstances) + (H.hashOfHashes (theAccountsHash bshAccounts) (theInstancesHash bshInstances)) ) ) ( H.hashOfHashes diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs index aa9544cd3c..eb6303c0c4 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs @@ -29,7 +29,11 @@ import Concordium.Utils.Serialization (putByteStringLen) import Concordium.Utils.Serialization.Put import qualified Concordium.Wasm as Wasm -import Concordium.GlobalState.BlockState (InstanceInfoType (..), InstanceInfoTypeV (..)) +import Concordium.GlobalState.BlockState ( + InstanceInfoType (..), + InstanceInfoTypeV (..), + InstancesHash (..), + ) import qualified Concordium.GlobalState.ContractStateV1 as StateV1 import qualified Concordium.GlobalState.Instance as Instance import Concordium.GlobalState.Persistent.BlobStore @@ -632,9 +636,16 @@ instance Show (Instances pv) where show InstancesEmpty = "Empty" show (InstancesTree _ t) = showFix showITString t -instance (IsProtocolVersion pv, SupportsPersistentModule m) => MHashableTo m H.Hash (Instances pv) where - getHashM InstancesEmpty = return $ H.hash "EmptyInstances" - getHashM (InstancesTree _ t) = getHash <$> mproject t +makeInstancesHash :: forall pv. (IsProtocolVersion pv) => Word64 -> H.Hash -> InstancesHash pv +makeInstancesHash size inner = case sBlockHashVersionFor (protocolVersion @pv) of + SBlockHashVersion0 -> InstancesHash inner + SBlockHashVersion1 -> InstancesHash . H.hashLazy . runPutLazy $ do + putWord64be size + put inner + +instance (IsProtocolVersion pv, SupportsPersistentModule m) => MHashableTo m (InstancesHash pv) (Instances pv) where + getHashM InstancesEmpty = return $ makeInstancesHash 0 $ H.hash "EmptyInstances" + getHashM (InstancesTree size t) = makeInstancesHash size . getHash <$> mproject t instance (IsProtocolVersion pv, SupportsPersistentModule m) => BlobStorable m (Instances pv) where storeUpdate i@InstancesEmpty = return (putWord8 0, i) diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs index 483e27f2e1..5a14866f60 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Consensus/Blocks.hs @@ -49,7 +49,6 @@ import Concordium.KonsensusV1.TreeState.Implementation import qualified Concordium.KonsensusV1.TreeState.LowLevel as LowLevel import Concordium.KonsensusV1.TreeState.Types import Concordium.KonsensusV1.Types -import qualified Concordium.MerkleProofs as Merkle import Concordium.Scheduler (FilteredTransactions (..)) import Concordium.TimerMonad import Concordium.Types.BakerIdentity @@ -113,7 +112,6 @@ data BlockResult pv -- 'executeBlock' to complete the procedure (as necessary). uponReceivingBlock :: ( IsConsensusV1 (MPV m), - MonadProtocolVersion m, LowLevel.MonadTreeStateStore m, MonadState (SkovData (MPV m)) m, BlockStateStorage m, @@ -123,10 +121,6 @@ uponReceivingBlock :: PendingBlock (MPV m) -> m (BlockResult (MPV m)) uponReceivingBlock pendingBlock = do - mp <- Merkle.buildMerkleProof (const True) (sbBlock (pbBlock pendingBlock)) - logEvent Konsensus LLTrace $ show mp - let mr = Merkle.parseMerkleProof (snd Merkle.blockSchema) (fst Merkle.blockSchema) mp - logEvent Konsensus LLTrace $ show mr isShutdown <- use isConsensusShutdown if isShutdown then return BlockResultConsensusShutdown diff --git a/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs b/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs index 2a37d53d9a..be8f35e3fc 100644 --- a/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs +++ b/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs @@ -11,7 +11,6 @@ module Concordium.Types.TransactionOutcomes where -import qualified Data.Aeson as AE import Data.Hashable import qualified Data.Sequence as Seq import qualified Data.Serialize as S @@ -67,7 +66,7 @@ instance HashableTo (TransactionOutcomesHashV 'TOV0) TransactionOutcomes where -- @(outcomes <> special)@, where @outcomes@ and @special@ are the version 1 LFMBTree hashes -- of the normal and special transaction outcomes respectively. newtype TransactionOutcomesHash = TransactionOutcomesHash {tohGet :: H.Hash} - deriving newtype (Eq, Ord, Show, S.Serialize, AE.ToJSON, AE.FromJSON, AE.FromJSONKey, AE.ToJSONKey, Read, Hashable) + deriving newtype (Eq, Ord, Show, S.Serialize, Read, Hashable) -- | A wrapper around a 'H.Hash', representing the hash of transaction outcomes in -- a block. The type parameter indicates the hashing scheme used to derive the hash. @@ -86,7 +85,7 @@ newtype TransactionOutcomesHash = TransactionOutcomesHash {tohGet :: H.Hash} newtype TransactionOutcomesHashV (tov :: TransactionOutcomesVersion) = TransactionOutcomesHashV { theTransactionOutcomesHashV :: H.Hash } - deriving newtype (Eq, Ord, Show, S.Serialize, AE.ToJSON, AE.FromJSON, AE.FromJSONKey, AE.ToJSONKey, Read, Hashable) + deriving newtype (Eq, Ord, Show, S.Serialize, Read, Hashable) -- | Convert a 'TransactionOutcomesHashV' to a 'TransactionOutcomesHash'. This erases the -- type-level information about the transaction outcomes hashing version used. From 1b6ad0c153ff4023dd941db32b2d6b9fab066f88 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Mon, 11 Dec 2023 14:00:50 +0100 Subject: [PATCH 07/20] Revise contract instance testing. --- .../src/Concordium/GlobalState/Instance.hs | 280 +------ .../Persistent/BlockState/Modules.hs | 6 + .../GlobalState/Persistent/Instances.hs | 21 +- .../tests/globalstate/Basic/InstanceTable.hs | 214 ----- .../tests/globalstate/Basic/Instances.hs | 139 ---- .../globalstate/GlobalStateTests/Instances.hs | 753 ++++++++++++------ 6 files changed, 550 insertions(+), 863 deletions(-) delete mode 100644 concordium-consensus/tests/globalstate/Basic/InstanceTable.hs delete mode 100644 concordium-consensus/tests/globalstate/Basic/Instances.hs diff --git a/concordium-consensus/src/Concordium/GlobalState/Instance.hs b/concordium-consensus/src/Concordium/GlobalState/Instance.hs index a9846bc0be..7dcac93de6 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Instance.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Instance.hs @@ -1,43 +1,14 @@ -{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveFunctor #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} --- | --- The "basic" implementation of contract state, which keeps the state in memory. --- This module contains both some common abstractions (e.g., HasInstanceAddress) --- as well as the basic implementation which should ideally be in Concordium.GlobalState.Basic.Instance. --- At some future point we should consider splitting this module into two as outlined above. -module Concordium.GlobalState.Instance where +-- | Basic interface definitions for smart contract instances. +module Concordium.GlobalState.Instance (InstanceParameters (..), HasInstanceAddress (..)) where -import qualified Concordium.Crypto.SHA256 as H -import qualified Concordium.Crypto.SHA256 as SHA256 -import qualified Concordium.GlobalState.ContractStateV1 as StateV1 -import qualified Concordium.GlobalState.Wasm as GSWasm -import Concordium.Types -import Concordium.Types.HashableTo -import qualified Concordium.Wasm as Wasm -import Data.Maybe -import Data.Serialize import qualified Data.Set as Set --- | State of a smart contract parametrized by the contract version. This is the --- "basic" version which keeps the state in memory. The persistent version is --- defined in Concordium.GlobalState.Persistent.Instance. -data InstanceStateV (v :: Wasm.WasmVersion) where - InstanceStateV0 :: !Wasm.ContractState -> InstanceStateV GSWasm.V0 - InstanceStateV1 :: !StateV1.InMemoryPersistentState -> InstanceStateV GSWasm.V1 - --- There is no versioning added to this. Contract state is always serialized in --- the context of an instance, which gives it a version. -instance Serialize (InstanceStateV GSWasm.V0) where - put (InstanceStateV0 model) = put model - get = InstanceStateV0 <$> get +import Concordium.Types +import qualified Concordium.Wasm as Wasm -instance Serialize (InstanceStateV GSWasm.V1) where - put (InstanceStateV1 model) = put model - get = InstanceStateV1 <$> get +import qualified Concordium.GlobalState.Wasm as GSWasm -- | The fixed parameters associated with a smart contract instance, parametrized by the type -- of the instrumented module. @@ -66,244 +37,3 @@ instance Show (InstanceParameters im) where show InstanceParameters{..} = show _instanceAddress ++ " :: " ++ show instanceContractModule ++ "." ++ show instanceInitName where instanceContractModule = GSWasm.miModuleRef instanceModuleInterface - -instance HashableTo H.Hash (InstanceParameters im) where - getHash InstanceParameters{..} = - makeInstanceParameterHash - _instanceAddress - instanceOwner - (GSWasm.miModuleRef instanceModuleInterface) - instanceInitName - --- | A versioned basic in-memory instance, parametrized by the version of the --- Wasm module that is associated with it. -data InstanceV instrumentedModule (v :: Wasm.WasmVersion) = InstanceV - { -- | The fixed parameters of the instance - -- These can be changed with the 'Upgrade' feature introduced in PV5. - _instanceVParameters :: !(InstanceParameters instrumentedModule), - -- | The current local state of the instance - _instanceVModel :: !(InstanceStateV v), - -- | The current amount of GTU owned by the instance - _instanceVAmount :: !Amount, - -- | Hash of the smart contract instance - _instanceVHash :: !H.Hash - } - -class HasInstanceFields a where - instanceAmount :: a -> Amount - instanceHash :: a -> H.Hash - -instance HasInstanceFields (InstanceV im v) where - {-# INLINE instanceAmount #-} - instanceAmount = _instanceVAmount - {-# INLINE instanceHash #-} - instanceHash = _instanceVHash - -instance HasInstanceFields (Instance im) where - instanceAmount (InstanceV0 i) = instanceAmount i - instanceAmount (InstanceV1 i) = instanceAmount i - instanceHash (InstanceV0 i) = instanceHash i - instanceHash (InstanceV1 i) = instanceHash i - -instance HasInstanceAddress (InstanceV im v) where - instanceAddress = instanceAddress . _instanceVParameters - -instance HasInstanceAddress (Instance im) where - instanceAddress (InstanceV0 i) = instanceAddress i - instanceAddress (InstanceV1 i) = instanceAddress i - --- | An instance of a smart contract. -data Instance im - = InstanceV0 (InstanceV (im GSWasm.V0) GSWasm.V0) - | InstanceV1 (InstanceV (im GSWasm.V1) GSWasm.V1) - -type BasicInstance = Instance GSWasm.InstrumentedModuleV - -instance Show (Instance im) where - show (InstanceV0 InstanceV{..}) = show _instanceVParameters ++ " {balance=" ++ show _instanceVAmount ++ ", hash = " ++ show _instanceVHash ++ "}" - show (InstanceV1 InstanceV{..}) = show _instanceVParameters ++ " {balance=" ++ show _instanceVAmount ++ ", hash =" ++ show _instanceVHash ++ "}" - -instance HashableTo H.Hash (Instance im) where - getHash (InstanceV0 InstanceV{..}) = _instanceVHash - getHash (InstanceV1 InstanceV{..}) = _instanceVHash - --- | Compute the hash of the instance parameters. -makeInstanceParameterHash :: ContractAddress -> AccountAddress -> ModuleRef -> Wasm.InitName -> H.Hash -makeInstanceParameterHash ca aa modRef conName = H.hashLazy $ runPutLazy $ do - put ca - put aa - put modRef - put conName - --- | Construct the hash of a basic instance from the __hash of the parameters__, the state, and amount for a V0 instance. -makeInstanceHashV0' :: H.Hash -> InstanceStateV GSWasm.V0 -> Amount -> H.Hash -makeInstanceHashV0' paramHash (InstanceStateV0 conState) a = H.hashLazy $ runPutLazy $ do - put paramHash - putByteString (H.hashToByteString (getHash conState)) - put a - --- | Construct the hash of a basic instance from the instance parameters, the state, and amount for a V0 instance. -makeInstanceHashV0 :: InstanceParameters v -> InstanceStateV GSWasm.V0 -> Amount -> H.Hash -makeInstanceHashV0 = makeInstanceHashV0' . getHash - --- | Construct the hash of a basic instance from the __hash of the parameters__, --- the state, and amount for a V1 instance. Note that V1 and V0 instance hashes --- will be different assuming no hash collisions since 'ModuleRef's for V0 and --- V1 are distinct (because the version is included in the hash), and --- 'ModuleRef' is included in the parameter hash. -makeInstanceHashV1' :: H.Hash -> InstanceStateV GSWasm.V1 -> Amount -> H.Hash -makeInstanceHashV1' paramHash (InstanceStateV1 conState) a = H.hashLazy $ runPutLazy $ do - put paramHash - put (getHash conState :: SHA256.Hash) - put a - --- | Construct the hash of a basic instance from the instance parameters, the state, and amount for a V1 instance. -makeInstanceHashV1 :: InstanceParameters im -> InstanceStateV GSWasm.V1 -> Amount -> H.Hash -makeInstanceHashV1 = makeInstanceHashV1' . getHash - --- | Compute the hash of either a V0 or V1 instance. The version is determined by the type parameter. -makeInstanceHash :: InstanceParameters im -> InstanceStateV v -> Amount -> H.Hash -makeInstanceHash params state = - case state of - InstanceStateV0 _ -> makeInstanceHashV0' (getHash params) state - InstanceStateV1 _ -> makeInstanceHashV1' (getHash params) state - -makeInstanceV :: - -- | Name of the init method used to initialize the contract. - Wasm.InitName -> - -- | Receive functions suitable for this instance. - Set.Set Wasm.ReceiveName -> - -- | Module interface - GSWasm.ModuleInterfaceA im -> - -- | Initial state - InstanceStateV v -> - -- | Initial balance - Amount -> - -- | Owner/creator of the instance. - AccountAddress -> - -- | Address for the instance - ContractAddress -> - InstanceV im v -makeInstanceV instanceInitName instanceReceiveFuns instanceModuleInterface _instanceVModel _instanceVAmount instanceOwner _instanceAddress = - InstanceV - { _instanceVHash = makeInstanceHash _instanceVParameters _instanceVModel _instanceVAmount, - .. - } - where - _instanceVParameters = InstanceParameters{..} - -makeInstance :: - -- | Name of the init method used to initialize the contract. - Wasm.InitName -> - -- | Receive functions suitable for this instance. - Set.Set Wasm.ReceiveName -> - -- | Module interface - GSWasm.ModuleInterfaceA (im v) -> - -- | Initial state - InstanceStateV v -> - -- | Initial balance - Amount -> - -- | Owner/creator of the instance. - AccountAddress -> - -- | Address for the instance - ContractAddress -> - Instance im -makeInstance instanceInitName instanceReceiveFuns instanceModuleInterface _instanceVModel _instanceVAmount instanceOwner _instanceAddress = - case _instanceVModel of - InstanceStateV0{} -> InstanceV0 instanceV - InstanceStateV1{} -> InstanceV1 instanceV - where - instanceV = makeInstanceV instanceInitName instanceReceiveFuns instanceModuleInterface _instanceVModel _instanceVAmount instanceOwner _instanceAddress - --- | Update a given smart contract instance. -updateInstanceV :: AmountDelta -> Maybe (InstanceStateV v) -> Maybe (GSWasm.ModuleInterfaceA im, Set.Set Wasm.ReceiveName) -> InstanceV im v -> InstanceV im v -updateInstanceV delta val maybeNewModule i = updateInstanceV' amnt val maybeNewModule i - where - amnt = applyAmountDelta delta (_instanceVAmount i) - --- | Update a given smart contract instance with exactly the given amount, state and possibly upgrade the module. -updateInstanceV' :: Amount -> Maybe (InstanceStateV v) -> Maybe (GSWasm.ModuleInterfaceA im, Set.Set Wasm.ReceiveName) -> InstanceV im v -> InstanceV im v -updateInstanceV' amnt val maybeNewMod i = - i - { _instanceVModel = newVal, - _instanceVAmount = amnt, - _instanceVParameters = newParams, - _instanceVHash = makeInstanceHash newParams newVal amnt - } - where - newVal = fromMaybe (_instanceVModel i) val - newParams = maybe (_instanceVParameters i) (\(nm, newEntrypoints) -> (_instanceVParameters i){instanceModuleInterface = nm, instanceReceiveFuns = newEntrypoints}) maybeNewMod - --- | Serialize a V0 smart contract instance in V0 format. -putV0InstanceV0 :: Putter (InstanceV im GSWasm.V0) -putV0InstanceV0 InstanceV{_instanceVParameters = InstanceParameters{..}, ..} = do - -- InstanceParameters - -- Only put the Subindex part of the address - put (contractSubindex _instanceAddress) - put instanceOwner - put (GSWasm.miModuleRef instanceModuleInterface) - put instanceInitName - -- instanceReceiveFuns, instanceModuleInterface and instanceParameterHash - -- are not included, since they can be derived from context. - put _instanceVModel - put _instanceVAmount - --- | Serialize a V1 smart contract instance in V0 format. -putV1InstanceV0 :: Putter (InstanceV im GSWasm.V1) -putV1InstanceV0 InstanceV{_instanceVParameters = InstanceParameters{..}, ..} = do - -- InstanceParameters - -- Only put the Subindex part of the address - put (contractSubindex _instanceAddress) - put instanceOwner - put (GSWasm.miModuleRef instanceModuleInterface) - put instanceInitName - -- instanceReceiveFuns, instanceModuleInterface and instanceParameterHash - -- are not included, since they can be derived from context. - put _instanceVModel - put _instanceVAmount - --- | Deserialize a V0 smart contract instance in V0 format. -getV0InstanceV0 :: - -- | Function for resolving the receive functions and module interface. - (ModuleRef -> Wasm.InitName -> Maybe (Set.Set Wasm.ReceiveName, GSWasm.ModuleInterface im)) -> - -- | Index of the contract - ContractIndex -> - Get (InstanceV (im GSWasm.V0) GSWasm.V0) -getV0InstanceV0 resolve idx = do - -- InstanceParameters - subindex <- get - let _instanceAddress = ContractAddress idx subindex - instanceOwner <- get - instanceContractModule <- get - instanceInitName <- get - (instanceReceiveFuns, instanceModuleInterface) <- - case resolve instanceContractModule instanceInitName of - Just (r, GSWasm.ModuleInterfaceV0 iface) -> return (r, iface) - Just (_, GSWasm.ModuleInterfaceV1 _) -> fail "Expected module version 0, but module version 1 encountered." - Nothing -> fail "Unable to resolve smart contract" - _instanceVModel <- get - _instanceVAmount <- get - return $ makeInstanceV instanceInitName instanceReceiveFuns instanceModuleInterface _instanceVModel _instanceVAmount instanceOwner _instanceAddress - --- | Deserialize a V1 smart contract instance in V0 format. -getV1InstanceV0 :: - -- | Function for resolving the receive functions and module interface. - (ModuleRef -> Wasm.InitName -> Maybe (Set.Set Wasm.ReceiveName, GSWasm.ModuleInterface im)) -> - -- | Index of the contract - ContractIndex -> - Get (InstanceV (im GSWasm.V1) GSWasm.V1) -getV1InstanceV0 resolve idx = do - -- InstanceParameters - subindex <- get - let _instanceAddress = ContractAddress idx subindex - instanceOwner <- get - instanceContractModule <- get - instanceInitName <- get - (instanceReceiveFuns, instanceModuleInterface) <- - case resolve instanceContractModule instanceInitName of - Just (_, GSWasm.ModuleInterfaceV0 _) -> fail "Expected module version 1, but module version 0 encountered." - Just (r, GSWasm.ModuleInterfaceV1 iface) -> return (r, iface) - Nothing -> fail "Unable to resolve smart contract" - _instanceVModel <- get - _instanceVAmount <- get - return $ makeInstanceV instanceInitName instanceReceiveFuns instanceModuleInterface _instanceVModel _instanceVAmount instanceOwner _instanceAddress diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs index bc2350e8e5..21adcb1ad0 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs @@ -17,6 +17,7 @@ module Concordium.GlobalState.Persistent.BlockState.Modules ( SupportsPersistentModule, getModuleInterface, PersistentInstrumentedModuleV, + makePersistentInstrumentedModuleV, loadInstrumentedModuleV, emptyModules, getInterface, @@ -75,6 +76,11 @@ data PersistentInstrumentedModuleV (v :: WasmVersion) PIMVPtr !(BlobPtr (GSWasm.InstrumentedModuleV v)) deriving (Show) +-- | Make a 'PersistentInstrumentedModuleV' from a 'GSWasm.InstrumentedModuleV', retaining it in +-- memory only. +makePersistentInstrumentedModuleV :: GSWasm.InstrumentedModuleV v -> PersistentInstrumentedModuleV v +makePersistentInstrumentedModuleV = PIMVMem + -- | Load a 'PersistentInstrumentedModuleV', retrieving the artifact. -- If the artifact has been persisted to the blob store, the artifact will wrap a pointer into -- the memory-mapped blob store. diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs index eb6303c0c4..f1bd8f8aee 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs @@ -78,6 +78,7 @@ data PersistentInstanceParameters = PersistentInstanceParameters -- | Hash of the fixed parameters pinstanceParameterHash :: !H.Hash } + deriving (Eq) instance Show PersistentInstanceParameters where show PersistentInstanceParameters{..} = show pinstanceAddress ++ " :: " ++ show pinstanceContractModule ++ "." ++ show pinstanceInitName @@ -553,12 +554,12 @@ newContractInstanceIT mk t0 = (\(res, v) -> (res,) <$> membed v) =<< nci 0 t0 =< nci offset _ (Branch h f True _ l r) = do projl <- mproject l projr <- mproject r - if branchHasVacancies projl + if hasVacancies projl then do (res, projl') <- nci offset l projl l' <- membed projl' return (res, makeBranch h f projl' projr l' r) - else assert (branchHasVacancies projr) $ do + else assert (hasVacancies projr) $ do (res, projr') <- nci (setBit offset (fromIntegral h)) r projr r' <- membed projr' return (res, makeBranch h f projl projr' l r') @@ -614,7 +615,7 @@ migrateIT modules (BufferedFix bf) = BufferedFix <$> migrateReference go bf data Instances pv = -- | The empty instance table InstancesEmpty - | -- | A non-empty instance table (recording the size) + | -- | A non-empty instance table, recording the number of leaf nodes, including vacancies InstancesTree !Word64 !(BufferedFix (IT pv)) migrateInstances :: @@ -680,11 +681,21 @@ newContractInstance fnew InstancesEmpty = do let ca = ContractAddress 0 0 (res, newInst) <- fnew ca (res,) . InstancesTree 1 <$> membed (Leaf newInst) -newContractInstance fnew (InstancesTree s it) = (\(res, it') -> (res, InstancesTree (s + 1) it')) <$> newContractInstanceIT fnew it +newContractInstance fnew (InstancesTree s it) = do + ((isFreshIndex, !res), !it') <- newContractInstanceIT fnew' it + let !s' = if isFreshIndex then s + 1 else s + insts = InstancesTree s' it' + return (res, insts) + where + fnew' ca = do + (r, inst) <- fnew ca + -- The size of the tree grows exactly when the new subindex is 0. + -- Otherwise, a vacancy is filled, and the size does not grow. + return ((contractSubindex ca == 0, r), inst) deleteContractInstance :: forall m pv. (IsProtocolVersion pv, SupportsPersistentModule m) => ContractAddress -> Instances pv -> m (Instances pv) deleteContractInstance _ InstancesEmpty = return InstancesEmpty -deleteContractInstance addr t0@(InstancesTree s it0) = dci (fmap (InstancesTree (s - 1)) . membed) (contractIndex addr) =<< mproject it0 +deleteContractInstance addr t0@(InstancesTree s it0) = dci (fmap (InstancesTree s) . membed) (contractIndex addr) =<< mproject it0 where dci succCont i (Leaf inst) | i == 0 = do diff --git a/concordium-consensus/tests/globalstate/Basic/InstanceTable.hs b/concordium-consensus/tests/globalstate/Basic/InstanceTable.hs deleted file mode 100644 index 96aa06e254..0000000000 --- a/concordium-consensus/tests/globalstate/Basic/InstanceTable.hs +++ /dev/null @@ -1,214 +0,0 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -module Basic.InstanceTable where - -import qualified Concordium.Crypto.SHA256 as H -import Concordium.GlobalState.Instance -import Concordium.Types -import Concordium.Types.HashableTo - -import Data.Serialize -import Data.Word -import Lens.Micro.Internal (Index, IxValue, Ixed) -import Lens.Micro.Platform - -data InstanceTable - = -- | The empty instance table - Empty - | -- | A non-empty instance table (recording the number of instances present) - Tree !Word64 !IT - deriving (Show) - -computeBranchHash :: IT -> IT -> H.Hash -computeBranchHash t1 t2 = H.hashShort $! (H.hashToShortByteString (getHash t1 :: H.Hash) <> H.hashToShortByteString (getHash t2 :: H.Hash)) - --- | Internal tree nodes of an 'InstanceTable'. --- Branches satisfy the following invariant properties: --- * The left branch is always a full sub-tree with height 1 less than the parent (or a leaf if the parent height is 0) --- * The right branch has height less than the parent --- * The hash is @computeBranchHash l r@ where @l@ and @r@ are the left and right subtrees --- * The first @Bool@ is @True@ if the tree is full, i.e. the right sub-tree is full with height 1 less than the parent --- * The second @Bool@ is @True@ if the tree has vacant leaves: either @hasVacancies l@ or @hasVacancies r@ is @True@ -data IT - = -- | A branch has the following fields: - -- * the height of the branch (0 if all children are leaves) - -- * whether the branch is a full binary tree - -- * whether the tree has vacant leaves - -- * the Merkle hash (lazy) - -- * the left and right subtrees - Branch !Word8 !Bool !Bool H.Hash IT IT - | -- | A leaf holds a contract instance - Leaf !BasicInstance - | -- | A vacant leaf records the 'ContractSubindex' of the last instance - -- with this 'ContractIndex'. - VacantLeaf !ContractSubindex - deriving (Show) - -instance HashableTo H.Hash IT where - getHash (Branch _ _ _ h _ _) = h - getHash (Leaf i) = getHash i - getHash (VacantLeaf si) = H.hash $ runPut $ put si - -instance HashableTo H.Hash InstanceTable where - -- The hash of the empty tree is defined arbitrarily to be the hash of the string "EmptyInstances" - getHash Empty = H.hash "EmptyInstances" - getHash (Tree _ t) = getHash t - --- | A fold over the leaves of an 'IT' -foldIT :: SimpleFold IT (Either ContractSubindex BasicInstance) -foldIT up (Branch _ _ _ _ l r) = foldIT up l <> foldIT up r -foldIT up t@(Leaf i) = t <$ up (Right i) -foldIT up t@(VacantLeaf si) = t <$ up (Left si) - -type instance Index IT = ContractIndex -type instance IxValue IT = BasicInstance - -instance Ixed IT where - ix i upd br@(Branch b f v _ t1 t2) - | i < 2 ^ b = mkBranch <$> ix i upd t1 <*> pure t2 - | i < 2 ^ (b + 1) = mkBranch t1 <$> (ix (i - 2 ^ b) upd t2) - | otherwise = pure br - where - mkBranch t1' t2' = Branch b f v (computeBranchHash t1' t2') t1' t2' - ix i upd l@(Leaf inst) - | i == 0 = Leaf <$> upd inst - | otherwise = pure l - ix _ _ v@(VacantLeaf _) = pure v - -type instance Index InstanceTable = ContractAddress -type instance IxValue InstanceTable = BasicInstance - -instance Ixed InstanceTable where - ix _ _ t@Empty = pure t - ix i upd (Tree s t) = Tree s <$> (ix (contractIndex i) . filtered ((== i) . instanceAddress)) upd t - --- | Determine if an 'IT' is a full binary tree. -isFull :: IT -> Bool -isFull (Branch _ f _ _ _ _) = f -isFull _ = True - --- | The height for the level above. -nextHeight :: IT -> Word8 -nextHeight (Branch h _ _ _ _ _) = h + 1 -nextHeight _ = 0 - -hasVacancies :: IT -> Bool -hasVacancies (Branch _ _ v _ _ _) = v -hasVacancies (Leaf _) = False -hasVacancies (VacantLeaf _) = True - -newContractInstance :: Lens InstanceTable InstanceTable ContractAddress BasicInstance -newContractInstance mk Empty = Tree 1 . Leaf <$> mk (ContractAddress 0 0) -newContractInstance mk (Tree s0 t0) = Tree (s0 + 1) <$> nci 0 t0 - where - -- Insert into a tree with vacancies: insert in left if it has vacancies, otherwise right - nci offset (Branch h f True _ l r) - | hasVacancies l = let newBranch l' = mkBranch h f (hasVacancies l' || hasVacancies r) l' r in newBranch <$> nci offset l - | hasVacancies r = let newBranch r' = mkBranch h f (hasVacancies r') l r' in newBranch <$> nci (offset + 2 ^ h) r - | otherwise = error "newContractInstance: branch has vacancies, but children do not" - -- Insert into full tree with no vacancies: create new branch at top level - nci offset b@(Branch h True False _ _ _) = mkBranch (h + 1) False False b <$> (leaf (offset + 2 ^ (h + 1)) 0) - -- Insert into non-full tree with no vacancies: insert on right subtree (invariant implies left is full, but can add to right) - nci offset (Branch h False False _ l r) = newBranch <$> nci (offset + 2 ^ h) r - where - newBranch r' = mkBranch h (isFull r' && nextHeight r' == h) False l r' - -- Insert at leaf: create a new branch - nci offset b@(Leaf _) = mkBranch 0 True False b <$> (leaf (offset + 1) 0) - -- Insert at vacant leaf: create leaf with next subindex - nci offset (VacantLeaf si) = leaf offset (succ si) - mkBranch h f v t1' t2' = Branch h f v (computeBranchHash t1' t2') t1' t2' - leaf ind subind = Leaf <$> mk (ContractAddress ind subind) - --- | Delete the contract instance with the given 'ContractIndex'. -deleteContractInstance :: ContractIndex -> InstanceTable -> InstanceTable -deleteContractInstance _ Empty = Empty -deleteContractInstance i0 (Tree s0 t0) = uncurry Tree $ dci i0 t0 - where - dci i l@(Leaf inst) - | i == 0 = (s0 - 1, VacantLeaf $ contractSubindex $ instanceAddress inst) - | otherwise = (s0, l) - dci _ vl@(VacantLeaf _) = (s0, vl) - dci i b@(Branch h f _ _ l r) - | i < 2 ^ h = let (s', l') = dci i l in (s', mkBranch l' r) - | i < 2 ^ (h + 1) = let (s', r') = dci (i - 2 ^ h) r in (s', mkBranch l r') - | otherwise = (s0, b) - where - mkBranch t1' t2' = Branch h f (hasVacancies t1' || hasVacancies t2') (computeBranchHash t1' t2') t1' t2' - --- | Delete the contract instance at the given 'ContractAddress'. -deleteContractInstanceExact :: ContractAddress -> InstanceTable -> InstanceTable -deleteContractInstanceExact _ Empty = Empty -deleteContractInstanceExact addr (Tree s0 t0) = uncurry Tree $ dci (contractIndex addr) t0 - where - dci i l@(Leaf inst) - | i == 0 && addr == instanceAddress inst = - (s0 - 1, VacantLeaf $ contractSubindex $ instanceAddress inst) - | otherwise = (s0, l) - dci _ vl@(VacantLeaf _) = (s0, vl) - dci i b@(Branch h f _ _ l r) - | i < 2 ^ h = let (s', l') = dci i l in (s', mkBranch l' r) - | i < 2 ^ (h + 1) = let (s', r') = dci (i - 2 ^ h) r in (s', mkBranch l r') - | otherwise = (s0, b) - where - mkBranch t1' t2' = Branch h f (hasVacancies t1' || hasVacancies t2') (computeBranchHash t1' t2') t1' t2' - --- | Construct an 'InstanceTable' given a monadic function that --- will be invoked for each 'ContractIndex' in sequence to give --- the 'ContractSubindex' (for a vacancy) or 'Instance', until --- the function returns 'Nothing', indicating there are no more --- instances in the constructed table. -constructM :: - (Monad m) => - (ContractIndex -> m (Maybe (Either ContractSubindex BasicInstance))) -> - m InstanceTable -constructM build = c 0 0 [] - where - -- The list argument is a stack of @Maybe IT@ such that, for each @Just t@ in the list: - -- \* @t@ is full (satisfying the invariant properties of 'IT' instances); - -- \* The height of @t@ is one less than its index in the list. - c !idx !count l = - build idx >>= \case - Nothing -> return $! collapse0 count l - Just (Left si) -> c (idx + 1) count $! bubble (VacantLeaf si) l - Just (Right inst) -> c (idx + 1) (count + 1) $! bubble (Leaf inst) l - -- Add a new entry to the stack. @t@ is always a full 'IT' at level one - -- less than that required for the next index of the stack. - bubble t [] = [Just t] - bubble t (Nothing : l) = Just t : l - bubble t (Just t' : l) = Nothing : (bubble $! mkBranch True t' t) l - -- Collapse a stack with the above invariant properties into an 'InstanceTable'. - collapse0 _ [] = Empty - collapse0 count (Nothing : l) = collapse0 count l - collapse0 count (Just t : l) = collapse1 count l t - -- Here @t@ is either a non-full tree at the appropriate level of the stack, - -- or is a full tree at a lower level. - collapse1 count [] t = Tree count t - collapse1 count (Nothing : l) t = collapse1 count l t - collapse1 count (Just t' : l) t = collapse1 count l (mkBranch False t' t) - mkBranch f t1' t2' = Branch (nextHeight t1') f (hasVacancies t1' || hasVacancies t2') (computeBranchHash t1' t2') t1' t2' - --- | A collection of smart contract instances. -newtype Instances = Instances - { _instances :: InstanceTable - } - -makeLenses ''Instances - -instance HashableTo H.Hash Instances where - getHash = getHash . _instances - -type instance Index Instances = ContractAddress -type instance IxValue Instances = BasicInstance - -instance Ixed Instances where - ix z = instances . ix z - -instance Show Instances where - show (Instances Empty) = "Instances {}" - show (Instances (Tree _ t)) = "Instances {\n" ++ (concatMap f $ t ^.. foldIT) ++ "}" - where - f (Left _) = "" - f (Right inst) = show inst <> "\n" diff --git a/concordium-consensus/tests/globalstate/Basic/Instances.hs b/concordium-consensus/tests/globalstate/Basic/Instances.hs deleted file mode 100644 index b6008b6a78..0000000000 --- a/concordium-consensus/tests/globalstate/Basic/Instances.hs +++ /dev/null @@ -1,139 +0,0 @@ -{-# LANGUAGE GADTs #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} - -module Basic.Instances ( - InstanceParameters (..), - Instance (..), - InstanceV (..), - HasInstanceAddress (..), - makeInstance, - Instances, - emptyInstances, - getInstance, - updateInstanceAt, - updateInstanceAt', - createInstance, - deleteInstance, - foldInstances, - instanceCount, - - -- * Serialization - putInstancesV0, - getInstancesV0, -) where - -import Basic.InstanceTable - -import Concordium.GlobalState.Instance -import qualified Concordium.GlobalState.Wasm as GSWasm -import Concordium.Types -import qualified Concordium.Wasm as Wasm - -import Data.Serialize -import qualified Data.Set as Set -import Data.Word -import Lens.Micro.Platform - --- | The empty set of smart contract instances. -emptyInstances :: Instances -emptyInstances = Instances Empty - --- | Get the smart contract instance at the given address, if it exists. -getInstance :: ContractAddress -> Instances -> Maybe BasicInstance -getInstance addr (Instances iss) = iss ^? ix addr - --- | Update the instance at the specified address with an amount delta and --- potentially new state and module. If new state is not provided the state of the instance --- is not changed. If a new module is not provided the module of the instance --- is not changed. If there is no instance with the given address, this does --- nothing. If the instance at the given address has a different version than --- given this function raises an exception. -updateInstanceAt :: - forall v. - (Wasm.IsWasmVersion v) => - ContractAddress -> - AmountDelta -> - Maybe (InstanceStateV v) -> - Maybe (GSWasm.ModuleInterfaceV v, Set.Set Wasm.ReceiveName) -> - Instances -> - Instances -updateInstanceAt ca amt val maybeModule (Instances iss) = Instances (iss & ix ca %~ updateOnlyV) - where - -- only update if the instance matches the state version. Otherwise raise an exception. - updateOnlyV = case Wasm.getWasmVersion @v of - Wasm.SV0 -> \case - InstanceV0 i -> InstanceV0 $ updateInstanceV amt val Nothing i - InstanceV1 _ -> error "Expected a V0 instance, but got V1." - Wasm.SV1 -> \case - InstanceV0 _ -> error "Expected a V1 instance, but got V0" - InstanceV1 i -> InstanceV1 $ updateInstanceV amt val maybeModule i - --- | Update the instance at the specified address with a __new amount__ and --- potentially new state. If new state is not provided the state of the instance --- is not changed. If a new module is not provided the module of the instance --- is not changed. If there is no instance with the given address, this does --- nothing. If the instance at the given address has a different version than --- given this function raises an exception. -updateInstanceAt' :: forall v. (Wasm.IsWasmVersion v) => ContractAddress -> Amount -> Maybe (InstanceStateV v) -> Maybe (GSWasm.ModuleInterfaceV v, Set.Set Wasm.ReceiveName) -> Instances -> Instances -updateInstanceAt' ca amt val maybeModule (Instances iss) = Instances (iss & ix ca %~ updateOnlyV) - where - -- only update if the instance matches the state version. Otherwise raise an exception. - updateOnlyV = case Wasm.getWasmVersion @v of - Wasm.SV0 -> \case - InstanceV0 i -> InstanceV0 $ updateInstanceV' amt val Nothing i - InstanceV1 _ -> error "Expected a V0 instance, but got V1." - Wasm.SV1 -> \case - InstanceV0 _ -> error "Expected a V1 instance, but got V0" - InstanceV1 i -> InstanceV1 $ updateInstanceV' amt val maybeModule i - --- | Create a new smart contract instance. -createInstance :: (ContractAddress -> BasicInstance) -> Instances -> (BasicInstance, Instances) -createInstance mkInst (Instances iss) = Instances <$> (iss & newContractInstance <%~ mkInst) - --- | Delete the instance with the given address. Does nothing --- if there is no such instance. -deleteInstance :: ContractAddress -> Instances -> Instances -deleteInstance ca (Instances i) = Instances (deleteContractInstanceExact ca i) - --- | A fold over smart contract instances. -foldInstances :: SimpleFold Instances BasicInstance -foldInstances _ is@(Instances Empty) = is <$ mempty -foldInstances f is@(Instances (Tree _ t)) = is <$ (foldIT . _Right) f t - -instanceCount :: Instances -> Word64 -instanceCount (Instances Empty) = 0 -instanceCount (Instances (Tree c _)) = c - --- | Serialize 'Instances' in V0 format. -putInstancesV0 :: Putter Instances -putInstancesV0 (Instances Empty) = putWord8 0 -putInstancesV0 (Instances (Tree _ t)) = do - mapM_ putOptInstance (t ^.. foldIT) - putWord8 0 - where - putOptInstance (Left si) = do - putWord8 1 - put si - putOptInstance (Right inst) = do - case inst of - InstanceV0 i -> do - putWord8 2 - putV0InstanceV0 i - InstanceV1 i -> do - putWord8 3 - putV1InstanceV0 i - --- | Deserialize 'Instances' in V0 format. -getInstancesV0 :: - (ModuleRef -> Wasm.InitName -> Maybe (Set.Set Wasm.ReceiveName, GSWasm.ModuleInterface GSWasm.InstrumentedModuleV)) -> - Get Instances -getInstancesV0 resolve = Instances <$> constructM buildInstance - where - buildInstance idx = - getWord8 >>= \case - 0 -> return Nothing - 1 -> Just . Left <$> get - 2 -> Just . Right . InstanceV0 <$> getV0InstanceV0 resolve idx - 3 -> Just . Right . InstanceV1 <$> getV1InstanceV0 resolve idx - _ -> fail "Bad instance list" diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs index b0a8c4fbe1..bd53d18127 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs @@ -1,5 +1,9 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} @@ -14,11 +18,9 @@ import Data.Serialize import qualified Data.Set as Set import qualified Data.Text as Text import Data.Word -import Lens.Micro.Platform import qualified Concordium.Crypto.SHA256 as H import qualified Concordium.GlobalState.ContractStateV1 as StateV1 -import Concordium.GlobalState.Instance import qualified Concordium.GlobalState.Wasm as GSWasm import qualified Concordium.Scheduler.WasmIntegration as WasmV0 import qualified Concordium.Scheduler.WasmIntegration.V1 as WasmV1 @@ -29,9 +31,16 @@ import qualified Concordium.Wasm as Wasm import qualified Data.ByteString as BS import qualified Data.FixedByteString as FBS -import Basic.InstanceTable -import Basic.Instances - +import qualified Concordium.GlobalState.Basic.BlockState.LFMBTree as LFMBTree +import Concordium.GlobalState.BlockState +import Concordium.GlobalState.Persistent.BlobStore +import Concordium.GlobalState.Persistent.BlockState.Modules +import Concordium.GlobalState.Persistent.Cache +import qualified Concordium.GlobalState.Persistent.Instances as Instances +import Concordium.GlobalState.Persistent.MonadicRecursive +import Concordium.ID.Types (accountAddressSize) +import Control.Exception +import Control.Monad.Reader import Test.Hspec import Test.QuickCheck @@ -59,33 +68,157 @@ validContractArtifactsV1 = mapMaybe packModule contractSourcesV1 let source = Wasm.ModuleSource sourceBytes in (source,) <$> WasmV1.processModule SP5 (Wasm.WasmModuleV source) -checkBinary :: (Show a) => (a -> a -> Bool) -> a -> a -> String -> String -> String -> Either String () -checkBinary bop x y sbop sx sy = unless (bop x y) $ Left $ "Not satisfied: " ++ sx ++ " (" ++ show x ++ ") " ++ sbop ++ " " ++ sy ++ " (" ++ show y ++ ")" - -invariantIT :: ContractIndex -> IT -> Either String (Word8, Bool, Bool, ContractIndex, H.Hash, Word64) -invariantIT offset (Leaf inst) = do - checkBinary (==) (contractIndex $ instanceAddress inst) offset "==" "account index" "expected value" - return (0, True, False, succ offset, getHash inst, 1) -invariantIT offset (VacantLeaf si) = return (0, True, True, succ offset, H.hash $ runPut $ put si, 0) -invariantIT offset (Branch h f v hsh l r) = do - (hl, fl, vl, offset', hshl, cl) <- invariantIT offset l +-- | Assert that a binary predicate holds. +checkBinary :: (Show a, MonadFail m) => (a -> a -> Bool) -> a -> a -> String -> String -> String -> m () +checkBinary bop x y sbop sx sy = + unless (bop x y) $ + fail $ + "Not satisfied: " ++ sx ++ " (" ++ show x ++ ") " ++ sbop ++ " " ++ sy ++ " (" ++ show y ++ ")" + +-- | Check an invariant on 'Instances.IT'. The return value is a tuple consisting of: +-- * the height of the branch (0 for leaves) +-- * whether the branch is full (in the tree sense - not in the sense of no vacancies) +-- * whether the branch has vacancies +-- * the index of the next leaf +-- * the Merkle hash of the branch +invariantIT :: + (IsProtocolVersion pv) => + ContractIndex -> + Instances.IT pv (BufferedFix (Instances.IT pv)) -> + TestMonad (Word8, Bool, Bool, ContractIndex, H.Hash) +invariantIT offset (Instances.Leaf inst) = do + params <- Instances.loadInstanceParameters inst + checkBinary (==) (contractIndex $ Instances.pinstanceAddress params) offset "==" "account index" "expected value" + return (0, True, False, succ offset, getHash inst) +invariantIT offset (Instances.VacantLeaf si) = do + return (0, True, True, succ offset, H.hash (encode si)) +invariantIT offset (Instances.Branch h f v hsh l r) = do + (hl, fl, vl, offset', hshl) <- invariantIT offset =<< mproject l checkBinary (==) hl h "==" "sucessor level of left child" "node level" - unless fl $ Left "tree is not left-full" - (hr, fr, vr, offset'', hshr, cr) <- invariantIT offset' r + unless fl $ fail "tree is not left-full" + (hr, fr, vr, offset'', hshr) <- invariantIT offset' =<< mproject r checkBinary (==) hsh (H.hash $ runPut $ put hshl <> put hshr) "==" "branch hash" "hash(leftHash <> rightHash)" checkBinary (<=) hr h "<=" "successor level of right child" "node level" checkBinary (==) f (fr && hr == h) "<->" "branch marked full" "right child is full at next lower level" checkBinary (==) v (vl || vr) "<->" "branch has vacancies" "at least one child has vacancies" - return (succ h, f, v, offset'', hsh, cl + cr) - -invariantInstanceTable :: InstanceTable -> Either String () -invariantInstanceTable Empty = Right () -invariantInstanceTable (Tree c0 t) = do - (_, _, _, _, _, c) <- invariantIT 0 t - checkBinary (==) c0 c "==" "reported number of instances" "actual number" + return (succ h, f, v, offset'', hsh) + +-- | Check an invariant on 'Instances.Instances'. +invariantInstances :: (IsProtocolVersion pv) => Instances.Instances pv -> TestMonad () +invariantInstances Instances.InstancesEmpty = return () +invariantInstances (Instances.InstancesTree size bf) = do + (_, _, _, ContractIndex recSize, _) <- invariantIT 0 =<< mproject bf + checkBinary (==) recSize size "==" "measured size" "recorded size" + +-- | A model for an individual instance. +data ModelInstance = forall v. + (Wasm.IsWasmVersion v) => + ModelInstance + { mInstanceParameters :: !Instances.PersistentInstanceParameters, + mInstanceModule :: !(Wasm.ModuleSource v), + mInstanceInterface :: !(GSWasm.ModuleInterfaceV v), + mInstanceState :: !(Instances.InstanceStateV v), + mInstanceStateHash :: !Instances.InstanceStateHash, + mInstanceAmount :: !Amount + } -invariantInstances :: Instances -> Either String () -invariantInstances = invariantInstanceTable . _instances +instance Show ModelInstance where + show ModelInstance{..} = + "ModelInstance{" + ++ "mInstanceParameters = " + ++ show mInstanceParameters + ++ ", " + ++ "mInstanceModule = " + ++ show mInstanceModule + ++ ", " + ++ "mInstanceInterface = " + ++ show mInstanceInterface + ++ ", " + ++ "mInstanceStateHash = " + ++ show mInstanceStateHash + ++ ", " + ++ "mInstanceAmount = " + ++ show mInstanceAmount + ++ "}" + +instance HashableTo H.Hash ModelInstance where + getHash (ModelInstance params _ _ (Instances.InstanceStateV0 _) isHash modAmt) = + Instances.makeInstanceHashV0 (getHash params) isHash modAmt + getHash (ModelInstance params _ _ (Instances.InstanceStateV1 _) isHash modAmt) = + Instances.makeInstanceHashV0 (getHash params) isHash modAmt + +-- | The address associated with a model of a smart contract instance. +mInstanceAddr :: ModelInstance -> ContractAddress +mInstanceAddr = Instances.pinstanceAddress . mInstanceParameters + +-- | Construct a 'Instances.PersistentInstance' from a 'ModelInstance'. +toPersistentInstance :: ModelInstance -> TestMonad (Instances.PersistentInstance pv) +toPersistentInstance (ModelInstance @v params modSrc iface pinstanceModel _ pinstanceAmount) = do + pinstanceParameters <- refMake @_ @BufferedRef params + moduleVSource <- storeRef (Wasm.WasmModuleV modSrc) + case Wasm.getWasmVersion @v of + Wasm.SV0 -> do + pinstanceModuleInterface <- + refMake + (ModuleV0 ModuleV{moduleVInterface = makePersistentInstrumentedModuleV <$> iface, ..}) + let pinstanceHash = + Instances.makeInstanceHashV0State + (Instances.pinstanceParameterHash params) + pinstanceModel + pinstanceAmount + return $ Instances.PersistentInstanceV0 Instances.PersistentInstanceV{..} + Wasm.SV1 -> do + pinstanceModuleInterface <- + refMake + (ModuleV1 ModuleV{moduleVInterface = makePersistentInstrumentedModuleV <$> iface, ..}) + pinstanceHash <- + Instances.makeInstanceHashV1State + (Instances.pinstanceParameterHash params) + pinstanceModel + pinstanceAmount + return $ Instances.PersistentInstanceV1 Instances.PersistentInstanceV{..} + +-- | Assert that a 'ModelInstance' matches a 'Instances.PersistentInstance'. +modelsPersistentInstance :: ModelInstance -> Instances.PersistentInstance pv -> TestMonad Property +modelsPersistentInstance modInst perInst = do + case (modInst, perInst) of + (ModelInstance modParams modSrc modIFace modModel@Instances.InstanceStateV0{} _ modAmt, Instances.PersistentInstanceV0 Instances.PersistentInstanceV{..}) -> do + perParams <- refLoad pinstanceParameters + ModuleV{..} <- unsafeToModuleV <$> refLoad pinstanceModuleInterface + perSrc <- loadRef moduleVSource + perInstrMod <- mapM loadInstrumentedModuleV moduleVInterface + statesProp <- compareStates pinstanceModel modModel + return $ + counterexample "instance parameters" (perParams === modParams) + .&&. counterexample "module source" (Wasm.wmvSource perSrc === modSrc) + .&&. counterexample "module interface" (perInstrMod == modIFace) + .&&. counterexample "instance state" statesProp + .&&. counterexample "amount" (pinstanceAmount === modAmt) + (ModelInstance modParams modSrc modIFace modModel@Instances.InstanceStateV1{} _ modAmt, Instances.PersistentInstanceV1 Instances.PersistentInstanceV{..}) -> do + perParams <- refLoad pinstanceParameters + ModuleV{..} <- unsafeToModuleV <$> refLoad pinstanceModuleInterface + perSrc <- loadRef moduleVSource + perInstrMod <- mapM loadInstrumentedModuleV moduleVInterface + statesProp <- compareStates pinstanceModel modModel + return $ + counterexample "instance parameters" (perParams === modParams) + .&&. counterexample "module source" (Wasm.wmvSource perSrc === modSrc) + .&&. counterexample "module interface" (perInstrMod == modIFace) + .&&. counterexample "instance state" statesProp + .&&. counterexample "amount" (pinstanceAmount === modAmt) + _ -> return $ counterexample "instance version mismatch" False + where + compareStates :: Instances.InstanceStateV v -> Instances.InstanceStateV v -> TestMonad Property + compareStates (Instances.InstanceStateV0 cs0) (Instances.InstanceStateV0 cs1) = + return (cs0 === cs1) + compareStates (Instances.InstanceStateV1 ps0) (Instances.InstanceStateV1 ps1) = do + bs0 <- StateV1.toByteString ps0 + bs1 <- StateV1.toByteString ps1 + return (bs0 === bs1) + +-- | Generate an arbitrary account address. +genAccountAddress :: Gen AccountAddress +genAccountAddress = AccountAddress . FBS.pack <$> vector accountAddressSize -- These generators name contracts as numbers to make sure the names are valid. genInitName :: Gen Wasm.InitName @@ -108,94 +241,169 @@ genReceiveNames = do return (i, Set.fromList receives) return $ Map.fromList ns +-- | Generate a V0 contract state. genV0ContractState :: Gen Wasm.ContractState genV0ContractState = do n <- choose (1, 1000) Wasm.ContractState . BS.pack <$> vector n --- This currently always generates the empty state. +-- | Generate a V1 contract state. genV1ContractState :: Gen StateV1.InMemoryPersistentState genV1ContractState = do seed <- arbitrary len <- choose (0, 10) return $ StateV1.generatePersistentTree seed len -makeDummyInstance :: InstanceData -> Gen (ContractAddress -> Instance GSWasm.InstrumentedModuleV) -makeDummyInstance (InstanceDataV0 model amount) = do - let owner = AccountAddress . FBS.pack . replicate 32 $ 0 - (_, mInterface@GSWasm.ModuleInterface{..}) <- elements validContractArtifactsV0 - initName <- if Set.null miExposedInit then return (Wasm.InitName "init_") else elements (Set.toList miExposedInit) - let receiveNames = fromMaybe Set.empty $ Map.lookup initName miExposedReceive - return $ makeInstance initName receiveNames mInterface model amount owner -makeDummyInstance (InstanceDataV1 model amount) = do - let owner = AccountAddress . FBS.pack . replicate 32 $ 1 - (_, mInterface@GSWasm.ModuleInterface{..}) <- elements validContractArtifactsV1 - initName <- if Set.null miExposedInit then return (Wasm.InitName "init_") else elements (Set.toList miExposedInit) - let receiveNames = fromMaybe Set.empty $ Map.lookup initName miExposedReceive - return $ makeInstance initName receiveNames mInterface model amount owner - -data InstanceData - = InstanceDataV0 (InstanceStateV Wasm.V0) Amount - | InstanceDataV1 (InstanceStateV Wasm.V1) Amount - -instance Eq InstanceData where - (InstanceDataV0 (InstanceStateV0 v1) a1) == (InstanceDataV0 (InstanceStateV0 v2) a2) = v1 == v2 && a1 == a2 - (InstanceDataV1 v1 a1) == (InstanceDataV1 v2 a2) = encode v1 == encode v2 && a1 == a2 - _ == _ = False - -instance Show InstanceData where - show (InstanceDataV0 (InstanceStateV0 v) a) = "V0: " ++ show v ++ ", " ++ show a - show (InstanceDataV1 s a) = "V1: " ++ show (encode s) ++ ", " ++ show a - -instance Arbitrary InstanceData where - arbitrary = oneof [InstanceDataV0 <$> v0 <*> arbitrary, InstanceDataV1 <$> v1 <*> arbitrary] - where - v0 = InstanceStateV0 <$> genV0ContractState - v1 = InstanceStateV1 <$> genV1ContractState - -instanceData :: Instance GSWasm.InstrumentedModuleV -> InstanceData -instanceData (InstanceV0 InstanceV{..}) = InstanceDataV0 _instanceVModel _instanceVAmount -instanceData (InstanceV1 InstanceV{..}) = InstanceDataV1 _instanceVModel _instanceVAmount +-- | Generate a model of a contract instance. This produces a function that takes the address and +-- returns the model instance so that the address can be determined after the non-deterministic +-- generation of the instance. +genModelInstance :: Gen (ContractAddress -> ModelInstance) +genModelInstance = oneof [genV0, genV1] + where + genV0 = do + pinstanceOwner <- genAccountAddress + (mInstanceModule, mInstanceInterface@GSWasm.ModuleInterface{..}) <- elements validContractArtifactsV0 + let pinstanceContractModule = Wasm.getModuleRef (Wasm.WasmModuleV mInstanceModule) + pinstanceInitName <- if Set.null miExposedInit then return (Wasm.InitName "init_") else elements (Set.toList miExposedInit) + let pinstanceReceiveFuns = fromMaybe Set.empty $ Map.lookup pinstanceInitName miExposedReceive + memState <- genV0ContractState + let mInstanceStateHash = getHash memState + let mInstanceState = Instances.InstanceStateV0 memState + mInstanceAmount <- arbitrary + return $ \pinstanceAddress -> + ModelInstance + { mInstanceParameters = + Instances.PersistentInstanceParameters + { pinstanceParameterHash = + Instances.makeInstanceParameterHash + pinstanceAddress + pinstanceOwner + pinstanceContractModule + pinstanceInitName, + .. + }, + .. + } + genV1 = do + pinstanceOwner <- genAccountAddress + (mInstanceModule, mInstanceInterface@GSWasm.ModuleInterface{..}) <- elements validContractArtifactsV1 + let pinstanceContractModule = Wasm.getModuleRef (Wasm.WasmModuleV mInstanceModule) + pinstanceInitName <- if Set.null miExposedInit then return (Wasm.InitName "init_") else elements (Set.toList miExposedInit) + let pinstanceReceiveFuns = fromMaybe Set.empty $ Map.lookup pinstanceInitName miExposedReceive + memState <- genV1ContractState + let mInstanceStateHash = getHash memState + mInstanceState = Instances.InstanceStateV1 . StateV1.makePersistent $ memState + mInstanceAmount <- arbitrary + return $ \pinstanceAddress -> + ModelInstance + { mInstanceParameters = + Instances.PersistentInstanceParameters + { pinstanceParameterHash = + Instances.makeInstanceParameterHash + pinstanceAddress + pinstanceOwner + pinstanceContractModule + pinstanceInitName, + .. + }, + .. + } +-- | Convert an instance table to a list of the hashes of the leaves. +instancesToHashList :: (IsProtocolVersion pv) => Instances.Instances pv -> TestMonad [H.Hash] +instancesToHashList Instances.InstancesEmpty = return [] +instancesToHashList (Instances.InstancesTree _ instTab) = go [] =<< mproject instTab + where + go accum Instances.Branch{..} = do + accum' <- go accum =<< mproject branchRight + go accum' =<< mproject branchLeft + go accum (Instances.Leaf inst) = + return $ getHash inst : accum + go accum (Instances.VacantLeaf si) = + return $ H.hash (encode si) : accum + +-- | A model for the instance table. data Model = Model - { -- Data of instances - modelInstances :: Map.Map ContractIndex (ContractSubindex, InstanceData), - -- The next free subindex for free indexes + { -- | Data of instances + modelInstances :: Map.Map ContractIndex ModelInstance, + -- | The next free subindex for free indexes modelFree :: Map.Map ContractIndex ContractSubindex, - -- The lowest index that has never been assigned + -- | The lowest index that has never been assigned modelBound :: ContractIndex } - deriving (Eq, Show) + deriving (Show) + +-- | Convert a model instance table to a list of the hashes of the leaves. +modelToHashList :: Model -> [H.Hash] +modelToHashList Model{..} = l 0 + where + l i + | i == modelBound = [] + | Just mi <- Map.lookup i modelInstances = getHash mi : l (succ i) + | Just si <- Map.lookup i modelFree = H.hash (encode (si - 1)) : l (succ i) + | otherwise = error "Missing leaf in model" + +instance (IsProtocolVersion pv) => HashableTo (InstancesHash pv) Model where + getHash m@Model{..} = + Instances.makeInstancesHash (_contractIndex modelBound) $ + LFMBTree.hashAsLFMBTV0 emptyHash (modelToHashList m) + where + emptyHash = H.hash "EmptyInstances" +-- | The initial empty instance table model. emptyModel :: Model emptyModel = Model Map.empty Map.empty 0 -modelGetInstanceData :: ContractAddress -> Model -> Maybe InstanceData -modelGetInstanceData (ContractAddress ci csi) m = do - (csi', idata) <- Map.lookup ci (modelInstances m) - guard $ csi == csi' - return idata - -modelUpdateInstanceAt :: forall v. (Wasm.IsWasmVersion v) => ContractAddress -> Amount -> InstanceStateV v -> Model -> Model -modelUpdateInstanceAt (ContractAddress ci csi) amt val m = m{modelInstances = Map.adjust upd ci (modelInstances m)} +-- | Get the model instance at a particular address in the model instance table. +modelGetInstanceData :: ContractAddress -> Model -> Maybe ModelInstance +modelGetInstanceData ca@(ContractAddress ci _) m = do + inst <- Map.lookup ci (modelInstances m) + guard $ ca == mInstanceAddr inst + return inst + +-- | Update a model instance as a particular address in the model instance table. +-- This does nothing if the instance does not exist. +-- If the instance does exist, then it must be of the same version as the supplied state. +modelUpdateInstanceAt :: + forall v. + (Wasm.IsWasmVersion v) => + ContractAddress -> + Amount -> + Instances.InstanceStateV v -> + Instances.InstanceStateHash -> + Model -> + Model +modelUpdateInstanceAt addr@(ContractAddress ci _) amt val hsh m = + m{modelInstances = Map.adjust upd ci (modelInstances m)} where - upd o@(csi', ex) - | csi == csi' = case Wasm.getWasmVersion @v of - Wasm.SV0 -> case ex of - InstanceDataV0 _ _ -> (csi, InstanceDataV0 val amt) - _ -> error "Contract version mismatch." - Wasm.SV1 -> case ex of - InstanceDataV1 _ _ -> (csi, InstanceDataV1 val amt) - _ -> error "Contract version mismatch." - | otherwise = o - -modelCreateInstance :: (ContractAddress -> Instance GSWasm.InstrumentedModuleV) -> Model -> (ContractAddress, Model) + upd inst@ModelInstance{..} + | addr == mInstanceAddr inst = case (mInstanceState, val) of + (Instances.InstanceStateV0 _, Instances.InstanceStateV0 _) -> + ModelInstance + { mInstanceState = val, + mInstanceAmount = amt, + mInstanceStateHash = hsh, + .. + } + (Instances.InstanceStateV1 _, Instances.InstanceStateV1 _) -> + ModelInstance + { mInstanceState = val, + mInstanceAmount = amt, + mInstanceStateHash = hsh, + .. + } + _ -> error "Contract version mismatch." + | otherwise = inst + +-- | Update a model instance table by creating a new instance. Returns the address of the new +-- instances as well as the updated model. +modelCreateInstance :: (ContractAddress -> ModelInstance) -> Model -> (ContractAddress, Model) modelCreateInstance mk m | null (modelFree m) = let ca = ContractAddress (modelBound m) 0 in ( ca, m - { modelInstances = Map.insert (modelBound m) (0, instanceData (mk ca)) (modelInstances m), + { modelInstances = Map.insert (modelBound m) (mk ca) (modelInstances m), modelBound = succ $ modelBound m } ) @@ -206,16 +414,18 @@ modelCreateInstance mk m in ( ca, m - { modelInstances = Map.insert ci (csi, instanceData (mk ca)) (modelInstances m), + { modelInstances = Map.insert ci (mk ca) (modelInstances m), modelFree = free' } ) +-- | Update a model instance table by deleting the instance at a given contract address. +-- This does nothing to the model if there is no instance at the given address. modelDeleteInstance :: ContractAddress -> Model -> Model -modelDeleteInstance (ContractAddress ci csi) m = case Map.lookup ci (modelInstances m) of +modelDeleteInstance ca@(ContractAddress ci csi) m = case Map.lookup ci (modelInstances m) of Nothing -> m - Just (csi', _) -> - if csi /= csi' + Just inst -> + if ca /= mInstanceAddr inst then m else m @@ -223,170 +433,246 @@ modelDeleteInstance (ContractAddress ci csi) m = case Map.lookup ci (modelInstan modelFree = Map.insert ci (succ csi) (modelFree m) } -instanceTableToModel :: InstanceTable -> Model -instanceTableToModel Empty = emptyModel -instanceTableToModel (Tree _ t0) = ttm 0 emptyModel t0 - where - ttm offset m (Branch h _ _ _ l r) = - let m' = ttm offset m l - in ttm (offset + 2 ^ h) m' r - ttm offset m (Leaf inst) = - m - { modelInstances = Map.insert offset (contractSubindex $ instanceAddress inst, instanceData inst) (modelInstances m), - modelBound = modelBound m + 1 - } - ttm offset m (VacantLeaf si) = - m - { modelFree = Map.insert offset (succ si) (modelFree m), - modelBound = modelBound m + 1 - } - -modelCheck :: Instances -> Model -> Property -modelCheck (Instances t) m = m === instanceTableToModel t - -checkEqualThen :: (Monad m, Eq a, Show a) => a -> a -> m Property -> m Property -checkEqualThen a b r = if a /= b then return (a === b) else r - -checkBoolThen :: (Monad m) => String -> Bool -> m Property -> m Property -checkBoolThen _ True r = r -checkBoolThen ex False _ = return $ counterexample ex False - -checkEitherThen_ :: (Monad m) => Either String a -> m Property -> m Property -checkEitherThen_ (Left ex) _ = return $ counterexample ex False -checkEitherThen_ (Right _) r = r - -checkInvariantThen :: (Monad m) => Instances -> m Property -> m Property -checkInvariantThen insts r = case invariantInstances insts of - Right _ -> r - Left ex -> return $ counterexample (ex ++ "\n" ++ show (_instances insts)) False - +-- | Choose an arbitrary key-value pair from a map. arbitraryMapElement :: Map.Map k v -> Gen (k, v) arbitraryMapElement m = do ind <- choose (0, Map.size m - 1) return (Map.elemAt ind m) -generateFromUpdates :: Int -> Gen (Instances, Model) -generateFromUpdates n0 = gen n0 emptyInstances emptyModel +-- | A test monad that can be used for performing operations on an instance table. +-- This uses the in-memory blob store. +newtype TestMonad a = TestMonad {runTestMonad :: ModuleCache -> MemBlobStore -> IO a} + deriving + (Functor, Applicative, Monad, MonadIO, MonadFail) + via (ReaderT ModuleCache (ReaderT MemBlobStore IO)) + deriving + (MonadBlobStore) + via (ReaderT ModuleCache (MemBlobStoreT IO)) + +instance MonadCache ModuleCache TestMonad where + getCache = TestMonad $ \c _ -> return c + +-- | Run a 'TestMonad' with a fresh in-memory blob store and an empty 0-sized module cache. +runTestMonadFresh :: TestMonad a -> IO a +runTestMonadFresh a = bracket newMemBlobStore destroyMemBlobStore $ \mbs -> do + c <- newModuleCache 0 + runTestMonad a c mbs + +-- | Generate a 'TestMonad' action for generating an instance table (by repeated creation and +-- deletion of instances), and a corresponding model. +generateFromUpdates :: (IsProtocolVersion pv) => Int -> Gen (TestMonad (Instances.Instances pv), Model) +generateFromUpdates n0 = gen n0 (return Instances.emptyInstances) emptyModel where gen 0 insts model = return (insts, model) - gen n insts model = oneof $ [create, create, create] ++ if null (modelInstances model) then [] else [deleteExisting] + gen n insts model = oneof $ [create, create, create] ++ [deleteExisting | not (null (modelInstances model))] where create = do - instData <- arbitrary - dummyInstance <- makeDummyInstance instData - let (_, insts') = createInstance dummyInstance insts + dummyInstance <- genModelInstance + + let insts' = fmap snd . Instances.newContractInstance (\ca -> ((),) <$> toPersistentInstance (dummyInstance ca)) =<< insts let (_, model') = modelCreateInstance dummyInstance model gen (n - 1) insts' model' deleteExisting = do - (ci, (csi, _)) <- arbitraryMapElement (modelInstances model) + (_, mi) <- arbitraryMapElement (modelInstances model) let - ca = ContractAddress ci csi - insts' = deleteInstance ca insts + ca = mInstanceAddr mi + insts' = Instances.deleteContractInstance ca =<< insts model' = modelDeleteInstance ca model gen (n - 1) insts' model' -testUpdates :: Int -> Gen Property -testUpdates n0 = if n0 <= 0 then return (property True) else tu n0 emptyInstances emptyModel +-- | Create and delete instances, then check invariants hold. +testCreateDelete :: forall pv. (IsProtocolVersion pv) => SProtocolVersion pv -> Int -> Property +testCreateDelete _ n = forAllShow (generateFromUpdates @pv n) (show . snd) $ + \(insts, model) -> idempotentIOProperty $ runTestMonadFresh $ do + insts' <- insts + invariantInstances insts' + hlActual <- instancesToHashList insts' + let hlModel = modelToHashList model + + hActual <- getHashM @_ @(InstancesHash pv) insts' + let hModel = getHash model + return $ hlActual === hlModel .&&. hActual === hModel + +-- | Check the structural invariants of the instances table and check that the hashes match the +-- model. +checkInvariants :: + forall pv. + (IsProtocolVersion pv) => + Model -> + Instances.Instances pv -> + TestMonad () +checkInvariants model insts = do + invariantInstances insts + hlActual <- instancesToHashList insts + let hlModel = modelToHashList model + checkBinary (==) hlActual hlModel "==" "actual hash list" "model hash list" + hActual <- getHashM @_ @(InstancesHash pv) insts + let hModel = getHash model + checkBinary (==) hActual hModel "==" "actual root hash" "model root hash" + +-- | An abstracted representation of an update to the instance table that can be useful when +-- reporting failures. +data Update = Create ContractAddress | Delete ContractAddress | Update ContractAddress + deriving (Show) + +-- | Test the various operations on the instance table for the given number of iterations. +-- After each operation, the invariants are asserted. +testUpdates :: forall pv. (IsProtocolVersion pv) => SProtocolVersion pv -> Int -> Gen Property +testUpdates _ n0 = do + (events, prop) <- tu n0 [] (return Instances.emptyInstances) emptyModel + return $ counterexample (show events) $ idempotentIOProperty $ runTestMonadFresh prop where - tu 0 insts model = checkInvariantThen insts $ return $ modelCheck insts model - tu n insts model = - checkInvariantThen insts $ - checkEqualThen model (instanceTableToModel $ _instances insts) $ - oneof $ - [create, deleteAbsent, updateAbsent] - ++ (if null (modelInstances model) then [] else [updateExisting, deleteExisting]) - ++ (if null (modelFree model) then [] else [deleteFree, updateFree]) + tu 0 evts insts model = return (reverse evts, checkInvariants @pv model =<< insts) + tu n evts insts0 model = + oneof $ + [create, deleteAbsent, updateAbsent] + ++ (if null (modelInstances model) then [] else [updateExisting, deleteExisting]) + ++ (if null (modelFree model) then [] else [deleteFree, updateFree]) where + insts = do + i <- insts0 + checkInvariants model i + return i create = do - instData <- arbitrary - dummyInstance <- makeDummyInstance instData - let (ca, insts') = createInstance dummyInstance insts + dummyInstance <- genModelInstance let (cam, model') = modelCreateInstance dummyInstance model - checkEqualThen (instanceAddress ca) cam $ - tu (n - 1) insts' model' + let insts' = do + (ca, i) <- Instances.newContractInstance (\ca -> (ca,) <$> toPersistentInstance (dummyInstance ca)) =<< insts + checkBinary (==) ca cam "==" "new instance address" "model new instance address" + return i + tu (n - 1) (Create cam : evts) insts' model' deleteAbsent = do ci <- ContractIndex <$> choose (fromIntegral $ modelBound model, maxBound) csi <- ContractSubindex <$> arbitrary - let - ca = ContractAddress ci csi - insts' = deleteInstance ca insts + let ca = ContractAddress ci csi + insts' = Instances.deleteContractInstance ca =<< insts model' = modelDeleteInstance ca model - tu (n - 1) insts' model' + tu (n - 1) (Delete ca : evts) insts' model' + updateAbsent = do + -- Pick a never-used contract index. ci <- ContractIndex <$> choose (fromIntegral $ modelBound model, maxBound) csi <- ContractSubindex <$> arbitrary - arbitrary >>= \case - InstanceDataV0 v a -> do - let - ca = ContractAddress ci csi - insts' = updateInstanceAt' ca a (Just v) Nothing insts - model' = modelUpdateInstanceAt ca a v model - tu (n - 1) insts' model' - InstanceDataV1 v a -> do - let - ca = ContractAddress ci csi - insts' = updateInstanceAt' ca a (Just v) Nothing insts - model' = modelUpdateInstanceAt ca a v model - tu (n - 1) insts' model' + let ca = ContractAddress ci csi + let insts' = do + i <- insts + res <- + Instances.updateContractInstance + (\_ -> fail "Update called on instance that should not exist.") + ca + i + unless (isNothing res) $ + fail "Expected Nothing result when updating missing contract instance." + return i + -- We do not update the model, as updating a non-existing instance will have no + -- effect. + tu (n - 1) (Update ca : evts) insts' model + updateExisting = do - (ci, (csi0, curVer)) <- arbitraryMapElement (modelInstances model) + (ci, mi@ModelInstance{..}) <- arbitraryMapElement (modelInstances model) + let csi0 = contractSubindex (mInstanceAddr mi) + -- We use a valid index, but possibly invalid subindex. csi <- oneof [return csi0, ContractSubindex <$> arbitrary] - case curVer of - InstanceDataV0 _ _ -> do - v <- InstanceStateV0 <$> genV0ContractState - a <- arbitrary - let - ca = ContractAddress ci csi - insts' = updateInstanceAt' ca a (Just v) Nothing insts - model' = modelUpdateInstanceAt ca a v model - tu (n - 1) insts' model' - InstanceDataV1 _ _ -> do - v <- InstanceStateV1 <$> genV1ContractState - a <- arbitrary - let - ca = ContractAddress ci csi - insts' = updateInstanceAt' ca a (Just v) Nothing insts - model' = modelUpdateInstanceAt ca a v model - tu (n - 1) insts' model' + let ca = ContractAddress ci csi + (newAmt :: Amount) <- arbitrary + case mInstanceState of + Instances.InstanceStateV0{} -> do + cs <- genV0ContractState + let newState = Instances.InstanceStateV0 cs + let newStateHash = getHash cs + let insts' = do + i <- insts + let upd (Instances.PersistentInstanceV0 inst) = do + Instances.PersistentInstanceParameters{..} <- + refLoad + (Instances.pinstanceParameters inst) + return + ( (), + Instances.PersistentInstanceV0 + inst + { Instances.pinstanceAmount = newAmt, + Instances.pinstanceModel = newState, + Instances.pinstanceHash = + Instances.makeInstanceHashV0 + pinstanceParameterHash + newStateHash + newAmt + } + ) + upd _ = error "Instance version does not match expected value." + res <- Instances.updateContractInstance upd ca i + return $ maybe i snd res + let model' = modelUpdateInstanceAt ca newAmt newState newStateHash model + tu (n - 1) (Update ca : evts) insts' model' + Instances.InstanceStateV1{} -> do + imps <- genV1ContractState + let newState = Instances.InstanceStateV1 (StateV1.makePersistent imps) + let newStateHash = getHash imps + let insts' = do + i <- insts + let upd (Instances.PersistentInstanceV1 inst) = do + Instances.PersistentInstanceParameters{..} <- + refLoad + (Instances.pinstanceParameters inst) + return + ( (), + Instances.PersistentInstanceV1 + inst + { Instances.pinstanceAmount = newAmt, + Instances.pinstanceModel = newState, + Instances.pinstanceHash = + Instances.makeInstanceHashV1 + pinstanceParameterHash + newStateHash + newAmt + } + ) + upd _ = error "Instance version does not match expected value." + res <- Instances.updateContractInstance upd ca i + return $ maybe i snd res + let model' = modelUpdateInstanceAt ca newAmt newState newStateHash model + tu (n - 1) (Update ca : evts) insts' model' + deleteExisting = do - (ci, (csi0, _)) <- arbitraryMapElement (modelInstances model) + (ci, mi) <- arbitraryMapElement (modelInstances model) + let csi0 = contractSubindex (mInstanceAddr mi) + -- We use a valid index, but possibly invalid subindex. csi <- oneof [return csi0, ContractSubindex <$> arbitrary] - let - ca = ContractAddress ci csi - insts' = deleteInstance ca insts + let ca = ContractAddress ci csi + insts' = Instances.deleteContractInstance ca =<< insts model' = modelDeleteInstance ca model - tu (n - 1) insts' model' + tu (n - 1) (Delete ca : evts) insts' model' + updateFree = do (ci, csi0) <- arbitraryMapElement (modelFree model) csi <- ContractSubindex <$> oneof [choose (0, fromIntegral csi0 - 1), choose (fromIntegral csi0, maxBound)] - arbitrary >>= \case - InstanceDataV0 v a -> do - let - ca = ContractAddress ci csi - insts' = updateInstanceAt' ca a (Just v) Nothing insts - model' = modelUpdateInstanceAt ca a v model - tu (n - 1) insts' model' - InstanceDataV1 v a -> do - let - ca = ContractAddress ci csi - insts' = updateInstanceAt' ca a (Just v) Nothing insts - model' = modelUpdateInstanceAt ca a v model - tu (n - 1) insts' model' + let ca = ContractAddress ci csi + let insts' = do + i <- insts + res <- + Instances.updateContractInstance + (\_ -> fail "Update called on instance that should not exist.") + ca + i + unless (isNothing res) $ + fail "Expected Nothing result when updating missing contract instance." + return i + -- We do not update the model, as updating a non-existing instance will have no + -- effect. + tu (n - 1) (Update ca : evts) insts' model + deleteFree = do (ci, csi0) <- arbitraryMapElement (modelFree model) csi <- oneof [return csi0, ContractSubindex <$> arbitrary] let ca = ContractAddress ci csi - insts' = deleteInstance ca insts + insts' = Instances.deleteContractInstance ca =<< insts model' = modelDeleteInstance ca model - tu (n - 1) insts' model' - -testCreateDelete :: Int -> Gen Property -testCreateDelete n = do - (insts, model) <- generateFromUpdates n - checkInvariantThen insts $ return $ modelCheck insts model + tu (n - 1) (Delete ca : evts) insts' model' -testGetInstance :: Instances -> Model -> Gen Property +-- | Given a 'TestMonad' that generates an instance table and a corresponding model, test that +-- getting arbitrary contract addresses returns the same result in the instance table and model. +testGetInstance :: (IsProtocolVersion pv) => TestMonad (Instances.Instances pv) -> Model -> Gen Property testGetInstance insts model = oneof $ [present | not (null $ modelInstances model)] @@ -394,29 +680,36 @@ testGetInstance insts model = ++ [absent] where present = do - (ci, (csi, d)) <- arbitraryMapElement (modelInstances model) - return $ fmap instanceData (getInstance (ContractAddress ci csi) insts) === Just d + (_, mi) <- arbitraryMapElement (modelInstances model) + return $ idempotentIOProperty $ runTestMonadFresh $ do + i <- Instances.lookupContractInstance (mInstanceAddr mi) =<< insts + case i of + Nothing -> return $ counterexample ("Missing instance @" ++ show (mInstanceAddr mi)) False + Just ai -> modelsPersistentInstance mi ai deleted = do (ci, csi0) <- arbitraryMapElement (modelFree model) csi <- ContractSubindex <$> oneof [choose (0, fromIntegral csi0 - 1), choose (fromIntegral csi0, maxBound)] - return $ fmap instanceData (getInstance (ContractAddress ci csi) insts) === Nothing + let ca = ContractAddress ci csi + return $ idempotentIOProperty $ runTestMonadFresh $ do + i <- Instances.lookupContractInstance ca =<< insts + return $ counterexample ("Instance should be deleted @" ++ show ca) (isNothing i) absent = do ci <- ContractIndex <$> choose (fromIntegral $ modelBound model, maxBound) csi <- ContractSubindex <$> arbitrary - return $ fmap instanceData (getInstance (ContractAddress ci csi) insts) === Nothing - -testFoldInstances :: Instances -> Model -> Property -testFoldInstances insts model = allInsts === modInsts - where - allInsts = (\i -> (instanceAddress i, instanceData i)) <$> (insts ^.. foldInstances) - modInsts = (\(ci, (csi, d)) -> (ContractAddress ci csi, d)) <$> Map.toAscList (modelInstances model) + let ca = ContractAddress ci csi + return $ idempotentIOProperty $ runTestMonadFresh $ do + i <- Instances.lookupContractInstance ca =<< insts + return $ counterexample ("Instance should be absent @" ++ show ca) (isNothing i) tests :: Word -> Spec -tests lvl = describe "GlobalStateTests.Instances" $ do - it "getInstance" $ +tests lvl = describe "GlobalStateTests.Instances" $ parallel $ do + it "getInstance (P7)" $ withMaxSuccess (100 * fromIntegral lvl) $ - forAllBlind (generateFromUpdates 5000) $ + forAllBlind (generateFromUpdates @'P7 5000) $ \(i, m) -> withMaxSuccess 100 $ testGetInstance i m - it "foldInstances" $ withMaxSuccess 100 $ forAllBlind (generateFromUpdates 5000) $ uncurry testFoldInstances - it "10000 create/delete - check at end" $ withMaxSuccess 10 $ testCreateDelete 10000 - it "500 instance updates - check every step" $ withMaxSuccess (100 * fromIntegral lvl) $ testUpdates 500 + -- The hashing scheme for P1-P6 should be the same, but distinct from P7 onwards. + it "5 create/delete - check at end (P5)" $ withMaxSuccess 5000 $ testCreateDelete SP5 5 + it "5 create/delete - check at end (P7)" $ withMaxSuccess 5000 $ testCreateDelete SP7 5 + it "10000 create/delete - check at end (P7)" $ withMaxSuccess 10 $ testCreateDelete SP7 10000 + it "500 instance updates - check every step (P5)" $ withMaxSuccess (100 * fromIntegral lvl) $ testUpdates SP5 500 + it "500 instance updates - check every step (P7)" $ withMaxSuccess (100 * fromIntegral lvl) $ testUpdates SP7 500 From c72edb47154b77d4e2cb0d7299d576d7eac17926 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Wed, 13 Dec 2023 16:54:49 +0100 Subject: [PATCH 08/20] Update base. --- concordium-base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordium-base b/concordium-base index 569b37dcd3..ffc52d3716 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit 569b37dcd3c43b787103dee97fca7e52a13ed1ba +Subproject commit ffc52d371609feb4d33b476e0a23daf71c5c526a From 457e7d472a50ab5e9cb0934f3c31104066d0a0e2 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Wed, 13 Dec 2023 18:19:17 +0100 Subject: [PATCH 09/20] Clean up PoolRewards. --- .../Basic/BlockState/PoolRewards.hs | 264 ------------------ .../src/Concordium/GlobalState/BlockState.hs | 2 +- .../GlobalState/Persistent/BlobStore.hs | 2 +- .../GlobalState/Persistent/Genesis.hs | 4 +- .../GlobalState/Persistent/PoolRewards.hs | 47 ++-- .../src/Concordium/GlobalState/PoolRewards.hs | 48 ++++ .../src/Concordium/KonsensusV1/Scheduler.hs | 2 +- .../Scheduler/TreeStateEnvironment.hs | 2 +- 8 files changed, 71 insertions(+), 300 deletions(-) delete mode 100644 concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs create mode 100644 concordium-consensus/src/Concordium/GlobalState/PoolRewards.hs diff --git a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs deleted file mode 100644 index 58ca4c4241..0000000000 --- a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/PoolRewards.hs +++ /dev/null @@ -1,264 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeFamilies #-} - -module Concordium.GlobalState.Basic.BlockState.PoolRewards where - -import Control.Exception -import qualified Data.Map.Strict as Map -import Data.Serialize -import Data.Singletons -import qualified Data.Vector as Vec -import Data.Word -import Lens.Micro.Platform - -import Concordium.Crypto.SHA256 as Hash -import Concordium.Types -import Concordium.Types.HashableTo -import Concordium.Utils.BinarySearch -import Concordium.Utils.Serialization - -import qualified Concordium.GlobalState.Basic.BlockState.LFMBTree as LFMBT -import Concordium.GlobalState.CapitalDistribution -import Concordium.GlobalState.Rewards -import Concordium.Utils - --- | 'BakerPoolRewardDetails' tracks the rewards that have been earned by a baker pool in the current --- reward period. These are used to pay out the rewards at the payday. -data BakerPoolRewardDetails = BakerPoolRewardDetails - { -- | The number of blocks baked by this baker in the reward period - blockCount :: !Word64, - -- | The total transaction fees accrued to this pool in the reward period - transactionFeesAccrued :: !Amount, - -- | Whether the pool contributed to a finalization proof in the reward period - finalizationAwake :: !Bool - } - deriving (Eq, Show) - -instance Serialize BakerPoolRewardDetails where - put BakerPoolRewardDetails{..} = do - putWord64be blockCount - put transactionFeesAccrued - putBool finalizationAwake - - get = BakerPoolRewardDetails <$> getWord64be <*> get <*> getBool - -instance HashableTo Hash.Hash BakerPoolRewardDetails where - getHash = Hash.hash . encode - --- | Baker pool reward details with no rewards accrued to the baker. -emptyBakerPoolRewardDetails :: BakerPoolRewardDetails -emptyBakerPoolRewardDetails = - BakerPoolRewardDetails - { blockCount = 0, - transactionFeesAccrued = 0, - finalizationAwake = False - } - -instance (Monad m) => MHashableTo m Hash.Hash BakerPoolRewardDetails - --- | Details of rewards accruing over the course of a reward period, and details about the capital --- distribution for this reward period and (possibly) the next. -data PoolRewards (bhv :: BlockHashVersion) = PoolRewards - { -- | The capital distribution for the next reward period. - -- This is updated the epoch before a payday. - nextCapital :: !(Hashed' (CapitalDistributionHash' bhv) CapitalDistribution), - -- | The capital distribution for the current reward period. - currentCapital :: !(Hashed' (CapitalDistributionHash' bhv) CapitalDistribution), - -- | The details of rewards accruing to baker pools. - -- These are indexed by the index of the baker in the capital distribution (_not_ the BakerId). - -- There must be an entry for each baker in 'currentCapital'. - bakerPoolRewardDetails :: !(Vec.Vector BakerPoolRewardDetails), - -- | The transaction reward amount accruing to the passive delegators. - passiveDelegationTransactionRewards :: !Amount, - -- | The transaction reward fraction accruing to the foundation. - foundationTransactionRewards :: !Amount, - -- | The next payday occurs at the start of this epoch. - nextPaydayEpoch :: !Epoch, - -- | The rate at which tokens are minted for the current reward period. - nextPaydayMintRate :: !MintRate - } - deriving (Show) - --- | Traversal for accessing the reward details for a particular baker ID. -rewardDetails :: BakerId -> Traversal' (PoolRewards bhv) BakerPoolRewardDetails -rewardDetails bid f pr - | Just (index, _) <- mindex = - (\bprd -> pr{bakerPoolRewardDetails = bprd}) - <$> ix (fromIntegral index) f (bakerPoolRewardDetails pr) - | otherwise = pure pr - where - mindex = binarySearchI bcBakerId (bakerPoolCapital $ _unhashed $ currentCapital pr) bid - --- | Look up the baker capital and reward details for a baker ID. -lookupBakerCapitalAndRewardDetails :: BakerId -> PoolRewards bhv -> Maybe (BakerCapital, BakerPoolRewardDetails) -lookupBakerCapitalAndRewardDetails bid PoolRewards{..} = do - (index, capital) <- binarySearchI bcBakerId (bakerPoolCapital $ _unhashed currentCapital) bid - rds <- bakerPoolRewardDetails ^? ix (fromIntegral index) - return (capital, rds) - -instance (IsBlockHashVersion bhv) => HashableTo (PoolRewardsHash bhv) (PoolRewards bhv) where - getHash PoolRewards{..} = - PoolRewardsHash . Hash.hashOfHashes (getCDHash nextCapital) $ - Hash.hashOfHashes (getCDHash currentCapital) $ - Hash.hashOfHashes prdHash $ - getHash $ - runPut $ - put passiveDelegationTransactionRewards - <> put foundationTransactionRewards - <> put nextPaydayEpoch - <> put nextPaydayMintRate - where - getCDHash = theCapitalDistributionHash @bhv . getHash - prdHash = LFMBT.theLFMBTreeHash $ LFMBT.lfmbtHash' (sing @bhv) getHash bakerPoolRewardDetails - --- | The empty 'PoolRewards', where there are no bakers, delegators or rewards. --- This is generally not used except as a dummy value for testing. -emptyPoolRewards :: (IsBlockHashVersion bhv) => PoolRewards bhv -emptyPoolRewards = - PoolRewards - { nextCapital = makeHashed emptyCapitalDistribution, - currentCapital = makeHashed emptyCapitalDistribution, - bakerPoolRewardDetails = Vec.empty, - passiveDelegationTransactionRewards = 0, - foundationTransactionRewards = 0, - nextPaydayEpoch = 0, - nextPaydayMintRate = MintRate 0 0 - } - --- | A 'Putter' for 'PoolRewards'. --- The 'bakerPoolRewardDetails' is serialized as a flat list, with the length implied by the --- length of @bakerPoolCapital (_unhashed currentCapital)@. -putPoolRewards :: Putter (PoolRewards bhv) -putPoolRewards PoolRewards{..} = do - put (_unhashed nextCapital) - put (_unhashed currentCapital) - assert - ( Vec.length (bakerPoolCapital (_unhashed currentCapital)) - == Vec.length bakerPoolRewardDetails - ) - $ mapM_ put bakerPoolRewardDetails - put passiveDelegationTransactionRewards - put foundationTransactionRewards - put nextPaydayEpoch - put nextPaydayMintRate - --- | Deserialize 'PoolRewards'. --- The 'bakerPoolRewardDetails' is serialized as a flat list, with the length implied by the --- length of @bakerPoolCapital (_unhashed currentCapital)@. -getPoolRewards :: (IsBlockHashVersion bhv) => Get (PoolRewards bhv) -getPoolRewards = do - nextCapital <- makeHashed <$> get - currentCapital <- makeHashed <$> get - bakerPoolRewardDetails <- - Vec.replicateM - (Vec.length (bakerPoolCapital (_unhashed currentCapital))) - get - passiveDelegationTransactionRewards <- get - foundationTransactionRewards <- get - nextPaydayEpoch <- get - nextPaydayMintRate <- get - return PoolRewards{..} - --- | List of baker and number of blocks baked by this baker in the reward period. -bakerBlockCounts :: PoolRewards bhv -> [(BakerId, Word64)] -bakerBlockCounts PoolRewards{..} = - zipWith - bc - (Vec.toList (bakerPoolCapital (_unhashed currentCapital))) - (Vec.toList bakerPoolRewardDetails) - where - bc BakerCapital{..} BakerPoolRewardDetails{..} = (bcBakerId, blockCount) - --- | Rotate the capital distribution, so that the current capital distribution is replaced by the --- next one, and set up empty pool rewards. -rotateCapitalDistribution :: PoolRewards bhv -> PoolRewards bhv -rotateCapitalDistribution pr = - pr - { currentCapital = nextCapital pr, - bakerPoolRewardDetails = - Vec.replicate - (Vec.length (bakerPoolCapital (_unhashed (nextCapital pr)))) - emptyBakerPoolRewardDetails - } - --- | Set the next 'CapitalDistribution'. -setNextCapitalDistribution :: - (IsBlockHashVersion bhv) => - CapitalDistribution -> - PoolRewards bhv -> - PoolRewards bhv -setNextCapitalDistribution cd pr = - pr{nextCapital = makeHashed cd} - --- | Construct 'PoolRewards' for migrating from 'P3' to 'P4'. --- This is used to construct the state of the genesis block. -makePoolRewardsForMigration :: - (IsBlockHashVersion bhv) => - -- | Current epoch bakers and stakes, in ascending order of 'BakerId'. - Vec.Vector (BakerId, Amount) -> - -- | Next epoch bakers and stakes, in ascending order of 'BakerId'. - Vec.Vector (BakerId, Amount) -> - -- | 'BakerId's of baked blocks - [BakerId] -> - -- | Epoch of next payday - Epoch -> - -- | Mint rate for the next payday - MintRate -> - PoolRewards bhv -makePoolRewardsForMigration curBakers nextBakers bakedBlocks npEpoch npMintRate = - PoolRewards - { nextCapital = makeCD nextBakers, - currentCapital = makeCD curBakers, - bakerPoolRewardDetails = makePRD <$> curBakers, - passiveDelegationTransactionRewards = 0, - foundationTransactionRewards = 0, - nextPaydayEpoch = npEpoch, - nextPaydayMintRate = npMintRate - } - where - makeCD bkrs = - makeHashed $ - CapitalDistribution - { bakerPoolCapital = makeBakerCapital <$> bkrs, - passiveDelegatorsCapital = Vec.empty - } - makeBakerCapital (bid, amt) = BakerCapital bid amt Vec.empty - blockCounts = foldr (\bid -> at' bid . non 0 %~ (+ 1)) Map.empty bakedBlocks - makePRD (bid, _) = - BakerPoolRewardDetails - { blockCount = Map.findWithDefault 0 bid blockCounts, - transactionFeesAccrued = 0, - finalizationAwake = False - } - --- | Make initial pool rewards for a genesis block state. -makeInitialPoolRewards :: - (IsBlockHashVersion bhv) => - -- | Genesis capital distribution - CapitalDistribution -> - -- | Epoch of next payday - Epoch -> - -- | Mint rate - MintRate -> - PoolRewards bhv -makeInitialPoolRewards cdist npEpoch npMintRate = - PoolRewards - { nextCapital = initCD, - currentCapital = initCD, - bakerPoolRewardDetails = bprd, - passiveDelegationTransactionRewards = 0, - foundationTransactionRewards = 0, - nextPaydayEpoch = npEpoch, - nextPaydayMintRate = npMintRate - } - where - initCD = makeHashed cdist - bprd = Vec.replicate (length (bakerPoolCapital cdist)) emptyBakerPoolRewardDetails - --- | The total capital passively delegated in the current reward period's capital distribution. -currentPassiveDelegationCapital :: PoolRewards bhv -> Amount -currentPassiveDelegationCapital = - Vec.sum . fmap dcDelegatorCapital . passiveDelegatorsCapital . _unhashed . currentCapital diff --git a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs index 6a871afa95..e5fb37e803 100644 --- a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs @@ -74,10 +74,10 @@ import Concordium.Utils.Serialization import qualified Concordium.Wasm as Wasm import Concordium.GlobalState.BakerInfo -import Concordium.GlobalState.Basic.BlockState.PoolRewards import Concordium.GlobalState.CapitalDistribution import Concordium.GlobalState.Instance import Concordium.GlobalState.Parameters hiding (getChainParameters) +import Concordium.GlobalState.Persistent.PoolRewards import Concordium.GlobalState.Rewards import Concordium.GlobalState.Types import Concordium.Types.Accounts diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs index 9d10724718..9612691a90 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs @@ -168,9 +168,9 @@ import Concordium.GlobalState.Persistent.MonadicRecursive -- Imports for providing instances import Concordium.Common.Time import Concordium.GlobalState.Account -import Concordium.GlobalState.Basic.BlockState.PoolRewards import Concordium.GlobalState.CapitalDistribution import qualified Concordium.GlobalState.Parameters as Parameters +import Concordium.GlobalState.PoolRewards import Concordium.Logger (MonadLogger) import Concordium.Types import Concordium.Types.Accounts diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs index 6ba09baa9d..4612c42b08 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs @@ -18,7 +18,6 @@ import qualified Concordium.Genesis.Data.P4 as P4 import qualified Concordium.Genesis.Data.P5 as P5 import qualified Concordium.Genesis.Data.P6 as P6 import qualified Concordium.Genesis.Data.P7 as P7 -import qualified Concordium.GlobalState.Basic.BlockState.PoolRewards as Basic import qualified Concordium.GlobalState.CapitalDistribution as CapDist import qualified Concordium.GlobalState.Persistent.Account as Account import qualified Concordium.GlobalState.Persistent.Accounts as Accounts @@ -32,6 +31,7 @@ import qualified Concordium.GlobalState.Persistent.LFMBTree as LFMBT import qualified Concordium.GlobalState.Persistent.PoolRewards as Rewards import qualified Concordium.GlobalState.Persistent.ReleaseSchedule as ReleaseSchedule import qualified Concordium.GlobalState.Persistent.Trie as Trie +import qualified Concordium.GlobalState.PoolRewards as PoolRewards import qualified Concordium.GlobalState.Rewards as Rewards import qualified Concordium.GlobalState.TransactionTable as TransactionTable import qualified Concordium.ID.Types as Types @@ -194,7 +194,7 @@ buildGenesisBlockState vcgp GenesisData.GenesisState{..} = do } bakerPoolRewardDetails <- LFMBT.fromAscList $ - replicate (Vec.length agsBakerCapitals) Basic.emptyBakerPoolRewardDetails + replicate (Vec.length agsBakerCapitals) PoolRewards.emptyBakerPoolRewardDetails BS.BlockRewardDetailsV1 <$> Blob.refMakeFlushed Rewards.PoolRewards diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs index d7691e6d61..938e49cc70 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs @@ -5,11 +5,10 @@ {-# LANGUAGE TypeFamilies #-} module Concordium.GlobalState.Persistent.PoolRewards ( - module Concordium.GlobalState.Basic.BlockState.PoolRewards, + BakerPoolRewardDetails (..), CapitalDistributionRef, PoolRewards (..), emptyPoolRewards, - makerPersistentPoolRewards, putPoolRewards, bakerBlockCounts, rotateCapitalDistribution, @@ -32,20 +31,13 @@ import Concordium.Crypto.SHA256 as Hash import Concordium.Types import Concordium.Types.HashableTo import Concordium.Utils.BinarySearch +import Concordium.Utils.Serialization.Put (MonadPut (..)) -import qualified Concordium.GlobalState.Basic.BlockState.LFMBTree as BasicLFMBT -import Concordium.GlobalState.Rewards - -import Concordium.GlobalState.Basic.BlockState.PoolRewards ( - BakerPoolRewardDetails (..), - ) -import qualified Concordium.GlobalState.Basic.BlockState.PoolRewards as BasicPoolRewards import Concordium.GlobalState.CapitalDistribution - import Concordium.GlobalState.Persistent.BlobStore import qualified Concordium.GlobalState.Persistent.LFMBTree as LFMBT - -import Concordium.Utils.Serialization.Put (MonadPut (liftPut)) +import Concordium.GlobalState.PoolRewards +import Concordium.GlobalState.Rewards type CapitalDistributionRef (bhv :: BlockHashVersion) = HashedBufferedRef' (CapitalDistributionHash' bhv) CapitalDistribution @@ -242,7 +234,7 @@ instance (MonadBlobStore m, IsBlockHashVersion bhv) => MHashableTo m (PoolReward return $! PoolRewardsHash . Hash.hashOfHashes (theCapitalDistributionHash @bhv hNextCapital) $ Hash.hashOfHashes (theCapitalDistributionHash @bhv hCurrentCapital) $ - Hash.hashOfHashes (BasicLFMBT.theLFMBTreeHash @bhv hBakerPoolRewardDetails) $ + Hash.hashOfHashes (LFMBT.theLFMBTreeHash @bhv hBakerPoolRewardDetails) $ getHash $ runPut $ put passiveDelegationTransactionRewards @@ -259,26 +251,21 @@ instance (MonadBlobStore m, IsBlockHashVersion bhv) => Cacheable m (PoolRewards foundationTransactionRewards <- cache (foundationTransactionRewards pr) return PoolRewards{..} -makerPersistentPoolRewards :: (MonadBlobStore m, IsBlockHashVersion bhv) => BasicPoolRewards.PoolRewards bhv -> m (PoolRewards bhv) -makerPersistentPoolRewards bpr = do - nc <- refMake (_unhashed (BasicPoolRewards.nextCapital bpr)) - cc <- refMake (_unhashed (BasicPoolRewards.currentCapital bpr)) - bprd <- LFMBT.fromAscList $ Vec.toList $ BasicPoolRewards.bakerPoolRewardDetails bpr +-- | The empty 'PoolRewards'. +emptyPoolRewards :: (MonadBlobStore m, IsBlockHashVersion bhv) => m (PoolRewards bhv) +emptyPoolRewards = do + emptyCDRef <- refMake emptyCapitalDistribution return PoolRewards - { nextCapital = nc, - currentCapital = cc, - bakerPoolRewardDetails = bprd, - passiveDelegationTransactionRewards = BasicPoolRewards.passiveDelegationTransactionRewards bpr, - foundationTransactionRewards = BasicPoolRewards.foundationTransactionRewards bpr, - nextPaydayEpoch = BasicPoolRewards.nextPaydayEpoch bpr, - nextPaydayMintRate = BasicPoolRewards.nextPaydayMintRate bpr + { nextCapital = emptyCDRef, + currentCapital = emptyCDRef, + bakerPoolRewardDetails = LFMBT.empty, + passiveDelegationTransactionRewards = 0, + foundationTransactionRewards = 0, + nextPaydayEpoch = 0, + nextPaydayMintRate = MintRate 0 0 } --- | The empty 'PoolRewards'. -emptyPoolRewards :: (MonadBlobStore m, IsBlockHashVersion bhv) => m (PoolRewards bhv) -emptyPoolRewards = makerPersistentPoolRewards BasicPoolRewards.emptyPoolRewards - -- | List of baker and number of blocks baked by this baker in the reward period. bakerBlockCounts :: (MonadBlobStore m, IsBlockHashVersion bhv) => PoolRewards bhv -> m [(BakerId, Word64)] bakerBlockCounts PoolRewards{..} = do @@ -305,7 +292,7 @@ rotateCapitalDistribution oldPoolRewards = do LFMBT.fromAscList $ replicate (Vec.length (bakerPoolCapital nextCap)) - BasicPoolRewards.emptyBakerPoolRewardDetails + emptyBakerPoolRewardDetails refMake $ pr { currentCapital = nextCapital pr, diff --git a/concordium-consensus/src/Concordium/GlobalState/PoolRewards.hs b/concordium-consensus/src/Concordium/GlobalState/PoolRewards.hs new file mode 100644 index 0000000000..330fbe638b --- /dev/null +++ b/concordium-consensus/src/Concordium/GlobalState/PoolRewards.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + +module Concordium.GlobalState.PoolRewards where + +import Data.Serialize +import Data.Word + +import Concordium.Crypto.SHA256 as Hash +import Concordium.Types +import Concordium.Types.HashableTo +import Concordium.Utils.Serialization + +-- | 'BakerPoolRewardDetails' tracks the rewards that have been earned by a baker pool in the current +-- reward period. These are used to pay out the rewards at the payday. +data BakerPoolRewardDetails = BakerPoolRewardDetails + { -- | The number of blocks baked by this baker in the reward period + blockCount :: !Word64, + -- | The total transaction fees accrued to this pool in the reward period + transactionFeesAccrued :: !Amount, + -- | Whether the pool contributed to a finalization proof in the reward period + finalizationAwake :: !Bool + } + deriving (Eq, Show) + +instance Serialize BakerPoolRewardDetails where + put BakerPoolRewardDetails{..} = do + putWord64be blockCount + put transactionFeesAccrued + putBool finalizationAwake + + get = BakerPoolRewardDetails <$> getWord64be <*> get <*> getBool + +instance HashableTo Hash.Hash BakerPoolRewardDetails where + getHash = Hash.hash . encode + +instance (Monad m) => MHashableTo m Hash.Hash BakerPoolRewardDetails + +-- | Baker pool reward details with no rewards accrued to the baker. +emptyBakerPoolRewardDetails :: BakerPoolRewardDetails +emptyBakerPoolRewardDetails = + BakerPoolRewardDetails + { blockCount = 0, + transactionFeesAccrued = 0, + finalizationAwake = False + } diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs b/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs index 3416a09c65..e86508bd2d 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs @@ -16,10 +16,10 @@ import Concordium.Types import Concordium.Types.SeedState import Concordium.GlobalState.BakerInfo -import Concordium.GlobalState.Basic.BlockState.PoolRewards (BakerPoolRewardDetails) import Concordium.GlobalState.BlockState import Concordium.GlobalState.CapitalDistribution import qualified Concordium.GlobalState.Persistent.BlockState as PBS +import Concordium.GlobalState.PoolRewards (BakerPoolRewardDetails) import Concordium.GlobalState.TransactionTable import Concordium.GlobalState.Types import Concordium.KonsensusV1.LeaderElection diff --git a/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs b/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs index 12562acef5..036de19268 100644 --- a/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs +++ b/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs @@ -27,12 +27,12 @@ import Data.Word import Lens.Micro.Platform import qualified Concordium.GlobalState.BakerInfo as BI -import Concordium.GlobalState.Basic.BlockState.PoolRewards import Concordium.GlobalState.BlockMonads import Concordium.GlobalState.BlockPointer import Concordium.GlobalState.BlockState import Concordium.GlobalState.CapitalDistribution import Concordium.GlobalState.Parameters +import Concordium.GlobalState.Persistent.PoolRewards (BakerPoolRewardDetails (..)) import Concordium.GlobalState.Rewards import Concordium.GlobalState.TreeState import Concordium.Kontrol.Bakers From 5fc9b64402631b32e8166ef3e04085c25919a489 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Thu, 4 Jan 2024 11:59:31 +0100 Subject: [PATCH 10/20] Addressing comments --- concordium-base | 2 +- .../GlobalState/Basic/BlockState/LFMBTree.hs | 13 ++++++++- .../GlobalState/Persistent/Instances.hs | 29 ++++++++++++------- .../src/Concordium/KonsensusV1/Types.hs | 5 ++-- .../tests/scheduler/SchedulerTests/Payday.hs | 2 +- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/concordium-base b/concordium-base index ffc52d3716..62e6e221aa 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit ffc52d371609feb4d33b476e0a23daf71c5c526a +Subproject commit 62e6e221aad006b54f756e9fa00623392250689e diff --git a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs index a3a5eafa6c..bb7496b120 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs @@ -349,6 +349,8 @@ hashP1FromFoldable = getHash . fromFoldable @Word64 -- | Hash a list of hashes in the LFMBTree format, using the specified hash for the empty tree. -- This avoids building the full tree. +-- This uses the V0 hashing scheme, where the hash of a node is the hash of the concatenated +-- hashes of its children. hashAsLFMBTV0 :: -- | Hash to use for empty list H.Hash -> @@ -376,7 +378,16 @@ lfmbtV0Hash' :: (Foldable f) => (v -> H.Hash) -> f v -> LFMBTreeHashV0 {-# INLINE lfmbtV0Hash' #-} lfmbtV0Hash' hsh = LFMBTreeHash . hashAsLFMBTV0 (theLFMBTreeHash emptyTreeHash) . map hsh . toList -hashAsLFMBTV1 :: H.Hash -> [H.Hash] -> H.Hash +-- | Hash a list of hashes in the LFMBTree format, using the specified hash for the empty tree. +-- This avoids building the full tree. +-- This uses the V1 hashing scheme, where the top level hash is the hash of the concatenation of +-- the number of leaves in the tree and the V0 hash. +hashAsLFMBTV1 :: + -- | Hash to use for empty list + H.Hash -> + -- | List of hashes to construct into Merkle tree + [H.Hash] -> + H.Hash hashAsLFMBTV1 e l = H.hashLazy $! S.runPutLazy $ do S.putWord64be (fromIntegral $ length l) S.put $ hashAsLFMBTV0 e l diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs index f1bd8f8aee..249685b88f 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Instances.hs @@ -637,6 +637,10 @@ instance Show (Instances pv) where show InstancesEmpty = "Empty" show (InstancesTree _ t) = showFix showITString t +-- | Compute an @'InstancesHash' pv@ given the size and root hash of the instances table. +-- The behaviour is dependent on the block hashing version associated with the protocol version, +-- namely @BlockHashVersionFor pv@. For @BlockHashVersion0@, only the root hash is used. +-- for @BlockHashVersion1@, the size is concatenated with the root hash and then hashed. makeInstancesHash :: forall pv. (IsProtocolVersion pv) => Word64 -> H.Hash -> InstancesHash pv makeInstancesHash size inner = case sBlockHashVersionFor (protocolVersion @pv) of SBlockHashVersion0 -> InstancesHash inner @@ -644,7 +648,10 @@ makeInstancesHash size inner = case sBlockHashVersionFor (protocolVersion @pv) o putWord64be size put inner -instance (IsProtocolVersion pv, SupportsPersistentModule m) => MHashableTo m (InstancesHash pv) (Instances pv) where +instance + (IsProtocolVersion pv, SupportsPersistentModule m) => + MHashableTo m (InstancesHash pv) (Instances pv) + where getHashM InstancesEmpty = return $ makeInstancesHash 0 $ H.hash "EmptyInstances" getHashM (InstancesTree size t) = makeInstancesHash size . getHash <$> mproject t @@ -677,21 +684,21 @@ emptyInstances :: Instances pv emptyInstances = InstancesEmpty newContractInstance :: forall m pv a. (IsProtocolVersion pv, SupportsPersistentModule m) => (ContractAddress -> m (a, PersistentInstance pv)) -> Instances pv -> m (a, Instances pv) -newContractInstance fnew InstancesEmpty = do +newContractInstance createInstanceFn InstancesEmpty = do let ca = ContractAddress 0 0 - (res, newInst) <- fnew ca + (res, newInst) <- createInstanceFn ca (res,) . InstancesTree 1 <$> membed (Leaf newInst) -newContractInstance fnew (InstancesTree s it) = do - ((isFreshIndex, !res), !it') <- newContractInstanceIT fnew' it - let !s' = if isFreshIndex then s + 1 else s - insts = InstancesTree s' it' - return (res, insts) +newContractInstance createInstanceFn (InstancesTree size tree) = do + ((isFreshIndex, !result), !nextTree) <- newContractInstanceIT createFnWithFreshness tree + let !nextSize = if isFreshIndex then size + 1 else size + nextInstancesTree = InstancesTree nextSize nextTree + return (result, nextInstancesTree) where - fnew' ca = do - (r, inst) <- fnew ca + createFnWithFreshness newContractAddress = do + (result, createdInstance) <- createInstanceFn newContractAddress -- The size of the tree grows exactly when the new subindex is 0. -- Otherwise, a vacancy is filled, and the size does not grow. - return ((contractSubindex ca == 0, r), inst) + return ((contractSubindex newContractAddress == 0, result), createdInstance) deleteContractInstance :: forall m pv. (IsProtocolVersion pv, SupportsPersistentModule m) => ContractAddress -> Instances pv -> m (Instances pv) deleteContractInstance _ InstancesEmpty = return InstancesEmpty diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs index bebf5a8cef..8de27f3b91 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs @@ -16,6 +16,7 @@ import qualified Data.Map.Strict as Map import Data.Maybe import Data.Serialize import qualified Data.Set as Set +import Data.Singletons import qualified Data.Vector as Vector import Data.Word import Numeric.Natural @@ -32,13 +33,11 @@ import Concordium.Types import Concordium.Types.HashableTo import Concordium.Types.Option import Concordium.Types.Parameters (IsConsensusV1) +import Concordium.Types.TransactionOutcomes import Concordium.Types.Transactions import Concordium.Utils.BinarySearch import Concordium.Utils.Serialization -import Concordium.Types.TransactionOutcomes -import Data.Singletons - -- | The message that is signed by a finalizer to certify a block. data QuorumSignatureMessage = QuorumSignatureMessage { -- | Hash of the genesis block. diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs b/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs index 573fcc1541..3cbc4f6789 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs @@ -38,7 +38,6 @@ import Concordium.Types.SeedState import Concordium.Birk.Bake import qualified Concordium.GlobalState.AccountMap.LMDB as LMDBAccountMap import Concordium.GlobalState.BakerInfo -import Concordium.GlobalState.Basic.BlockState.PoolRewards (BakerPoolRewardDetails (transactionFeesAccrued)) import Concordium.GlobalState.BlockPointer (BlockPointer (_bpState)) import Concordium.GlobalState.BlockState import Concordium.GlobalState.CapitalDistribution @@ -48,6 +47,7 @@ import Concordium.GlobalState.Persistent.BlockPointer import Concordium.GlobalState.Persistent.BlockState import qualified Concordium.GlobalState.Persistent.BlockState as BS import Concordium.GlobalState.Persistent.TreeState +import Concordium.GlobalState.PoolRewards (BakerPoolRewardDetails (transactionFeesAccrued)) import Concordium.GlobalState.TreeState import Concordium.Startup import qualified SchedulerTests.Helpers as Helpers From 5ae1ef310dc158209d2bcbd665cc23893de14a0a Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Fri, 5 Jan 2024 10:59:18 +0100 Subject: [PATCH 11/20] Update base. --- concordium-base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordium-base b/concordium-base index 62e6e221aa..298496627a 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit 62e6e221aad006b54f756e9fa00623392250689e +Subproject commit 298496627a2f7bc6621cee4d476c7980bac2d1d1 From 1db5c9fb7d89af238d849ae632854e956fea1239 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Mon, 8 Jan 2024 16:17:32 +0100 Subject: [PATCH 12/20] Fix tests. --- .../ConcordiumTests/KonsensusV1/CatchUp.hs | 4 +- .../ConcordiumTests/KonsensusV1/Timeout.hs | 54 +++++++++---------- .../ConcordiumTests/KonsensusV1/Types.hs | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs index c096afa8bc..1663b5b951 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs @@ -451,7 +451,7 @@ catchupWithTwoBranchesResponse sProtocolVersion = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "f36a049939054eac3e8662e4ab0310d8e12381ee2ba77a9c16fa19c205ea64b3" + { dbhv1BlockResultHash = read "15de5c588b1eef119b2c03e7baf124deb0b3a01260ccc43cb7e470922d67c531" } } TestBlocks.succeedReceiveBlock b4 @@ -586,7 +586,7 @@ testMakeCatchupStatus sProtocolVersion = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "f36a049939054eac3e8662e4ab0310d8e12381ee2ba77a9c16fa19c205ea64b3" + { dbhv1BlockResultHash = read "15de5c588b1eef119b2c03e7baf124deb0b3a01260ccc43cb7e470922d67c531" } } TestBlocks.succeedReceiveBlock b4 diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs index 1babee078d..600b7dfa46 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs @@ -398,22 +398,22 @@ testReceiveTimeoutMessage :: Spec testReceiveTimeoutMessage sProtocolVersion = describe "Receive timeout message" $ do - it "rejects obsolete round" $ receiveAndCheck sd obsoleteRoundMessage $ Rejected ObsoleteRound - it "rejects obsolete qc" $ receiveAndCheck sd obsoleteQCMessage $ Rejected ObsoleteQC - it "initializes catch-up upon future epoch" $ receiveAndCheck sd futureEpochTM CatchupRequired - it "rejects from a non finalizer" $ receiveAndCheck sd notAFinalizerQCMessage $ Rejected NotAFinalizer - it "rejects on unknown finalization committee" $ receiveAndCheck sd unknownFinalizationCommittee $ Rejected ObsoleteQC - it "rejects on an invalid signature" $ receiveAndCheck sd invalidSignatureMessage $ Rejected InvalidSignature - it "initializes catch-up upon a future round" $ receiveAndCheck sd futureRoundTM CatchupRequired - it "rejects when the qc points to an old finalized block" $ receiveAndCheck sd obsoleteQCPointer $ Rejected ObsoleteQCPointer - it "initializes catch-up when the qc pointer is unknown" $ receiveAndCheck sd unknownQCPointer CatchupRequired - it "rejects when the qc points to a dead block" $ receiveAndCheck sd qcPointerIsDead $ Rejected DeadQCPointer - it "initializes catch-up when qc pointer is pending" $ receiveAndCheck sd qcPointerIsPending CatchupRequired - it "returns duplicate upon a duplicate timeout message" $ receiveAndCheck sd duplicateMessage $ Rejected Duplicate - it "rejects double signing" $ receiveAndCheck sd doubleSignMessage $ Rejected DoubleSigning - it "received a valid timeout message" $ - receiveAndCheck sd validTimeoutMessage $ - Received $ + it "rejects obsolete round" $ receiveAndCheck sd obsoleteRoundMessage $ Rejected ObsoleteRound + it "rejects obsolete qc" $ receiveAndCheck sd obsoleteQCMessage $ Rejected ObsoleteQC + it "initializes catch-up upon future epoch" $ receiveAndCheck sd futureEpochTM CatchupRequired + it "rejects from a non finalizer" $ receiveAndCheck sd notAFinalizerQCMessage $ Rejected NotAFinalizer + it "rejects on unknown finalization committee" $ receiveAndCheck sd unknownFinalizationCommittee $ Rejected ObsoleteQC + it "rejects on an invalid signature" $ receiveAndCheck sd invalidSignatureMessage $ Rejected InvalidSignature + it "initializes catch-up upon a future round" $ receiveAndCheck sd futureRoundTM CatchupRequired + it "rejects when the qc points to an old finalized block" $ receiveAndCheck sd obsoleteQCPointer $ Rejected ObsoleteQCPointer + it "initializes catch-up when the qc pointer is unknown" $ receiveAndCheck sd unknownQCPointer CatchupRequired + it "rejects when the qc points to a dead block" $ receiveAndCheck sd qcPointerIsDead $ Rejected DeadQCPointer + it "initializes catch-up when qc pointer is pending" $ receiveAndCheck sd qcPointerIsPending CatchupRequired + it "returns duplicate upon a duplicate timeout message" $ receiveAndCheck sd duplicateMessage $ Rejected Duplicate + it "rejects double signing" $ receiveAndCheck sd doubleSignMessage $ Rejected DoubleSigning + it "received a valid timeout message" $ + receiveAndCheck sd validTimeoutMessage $ + Received $ PartiallyVerifiedTimeoutMessage validTimeoutMessage finalizers True (Present $ Common.someBlockPointer sProtocolVersion liveBlockHash 1 0) where -- A valid timeout message that should pass the initial verification. @@ -548,13 +548,13 @@ testReceiveTimeoutMessage sProtocolVersion = testExecuteTimeoutMessages :: forall pv. (IsConsensusV1 pv, IsProtocolVersion pv) => SProtocolVersion pv -> Spec testExecuteTimeoutMessages sProtocolVersion = describe "execute timeout messages" $ do - it "rejects message with invalid bls signature" $ execute invalidAggregateSignature InvalidAggregateSignature - it "accepts message where there is already checked a valid qc for the round" $ execute validMessageAbsentQCPointer ExecutionSuccess - it "rejects message with invalid qc signature (qc round is better than recorded highest qc)" $ execute invalidQCTimeoutMessage $ InvalidQC $ someInvalidQC 2 0 - it "accepts message where qc is ok (qc round is better than recorded highest qc)" $ execute newValidQCTimeoutMessage ExecutionSuccess - it "rejects message with qc round no greater than highest qc and invalic qc" $ execute wrongEpochMessage $ InvalidQC $ someInvalidQC 0 0 - it "accepts message with qc round no greather than highest qc and valid qc" $ execute oldValidQCTimeoutMessage ExecutionSuccess - it "accepts message with qc already checked for that round and qc checks out (qc round <= higest qc)" $ execute oldRoundValidTimeoutMessage ExecutionSuccess + it "rejects message with invalid bls signature" $ execute invalidAggregateSignature InvalidAggregateSignature + it "accepts message where there is already checked a valid qc for the round" $ execute validMessageAbsentQCPointer ExecutionSuccess + it "rejects message with invalid qc signature (qc round is better than recorded highest qc)" $ execute invalidQCTimeoutMessage $ InvalidQC $ someInvalidQC 2 0 + it "accepts message where qc is ok (qc round is better than recorded highest qc)" $ execute newValidQCTimeoutMessage ExecutionSuccess + it "rejects message with qc round no greater than highest qc and invalic qc" $ execute wrongEpochMessage $ InvalidQC $ someInvalidQC 0 0 + it "accepts message with qc round no greather than highest qc and valid qc" $ execute oldValidQCTimeoutMessage ExecutionSuccess + it "accepts message with qc already checked for that round and qc checks out (qc round <= higest qc)" $ execute oldRoundValidTimeoutMessage ExecutionSuccess where -- action that runs @executeTimeoutMessage@ on the provided -- timeout message and checks that it matches the expectation. @@ -675,10 +675,10 @@ testCheckTimeoutCertificate :: Spec testCheckTimeoutCertificate sProtocolVersion = describe "check timeout certificate" $ do - it "accepts timeout certificate" checkOkTC - it "rejects with wrong genesis" wrongGenesis - it "rejects when there is not enough weight" insufficientWeight - it "rejects when the signature is invalid" invalidSignature + it "accepts timeout certificate" checkOkTC + it "rejects with wrong genesis" wrongGenesis + it "rejects when there is not enough weight" insufficientWeight + it "rejects when the signature is invalid" invalidSignature where checkOkTC = runTest $ do finComm <- use $ skovEpochBakers . currentEpochBakers . bfFinalizers diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs index d0bbe7135e..77ecc1658a 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs @@ -319,7 +319,7 @@ propSerializeSignedBlock :: (IsProtocolVersion pv) => SProtocolVersion pv -> Property -propSerializeSignedBlock sProtocolVersion = +propSerializeSignedBlock _ = forAll (genSignedBlock @pv) $ \sb -> case runGet (getSignedBlock (TransactionTime 42)) $! runPut (putSignedBlock sb) of Left _ -> False From 43dbb9f529b131de49c1cf11a3f55310ff0a7d73 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Tue, 9 Jan 2024 10:55:57 +0100 Subject: [PATCH 13/20] Address comments. --- .../src/Concordium/KonsensusV1/Types.hs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs index bbdacc892f..85345de56c 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Types.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Types.hs @@ -1283,15 +1283,17 @@ instance (Monad m, BlockHashVersionFor pv ~ 'BlockHashVersion1) => Merkle.Merkle put bbRound put bbEpoch put (qcBlock bbQuorumCertificate) - let timestampBaker = optProof ["quasi", "meta", "bakerInfo", "timestampBaker"] . rawMerkle . runPut $ do + let biPath = ["quasi", "meta", "bakerInfo"] + let timestampBaker = optProof (biPath ++ ["timestampBaker"]) . rawMerkle . runPut $ do put bbTimestamp put bbBaker - let nonce = optProof ["quasi", "meta", "bakerInfo", "nonce"] . rawMerkle . encode $ bbNonce - let bakerInfo = optProof ["quasi", "meta", "bakerInfo"] [timestampBaker, nonce] - let qcPath = ["quasi", "meta", "certificatesHash", "quorumCertificate"] + let nonce = optProof (biPath ++ ["nonce"]) . rawMerkle . encode $ bbNonce + let bakerInfo = optProof biPath [timestampBaker, nonce] + let chPath = ["quasi", "meta", "certificatesHash"] + let qcPath = chPath ++ ["quorumCertificate"] qcMerkleProof <- Merkle.buildMerkleProof (open . (qcPath ++)) bbQuorumCertificate let qcHash = optProof qcPath qcMerkleProof - let tfPath = ["quasi", "meta", "certificatesHash", "timeoutFinalization"] + let tfPath = chPath ++ ["timeoutFinalization"] let tcPath = tfPath ++ ["timeoutCertificate"] tcMerkleProof <- Merkle.buildMerkleProof (open . (tcPath ++)) bbTimeoutCertificate let tcHash = optProof tcPath tcMerkleProof @@ -1299,7 +1301,7 @@ instance (Monad m, BlockHashVersionFor pv ~ 'BlockHashVersion1) => Merkle.Merkle efeMerkleProof <- Merkle.buildMerkleProof (open . (efePath ++)) bbEpochFinalizationEntry let efeHash = optProof efePath efeMerkleProof let tfHash = optProof tfPath [tcHash, efeHash] - let certificatesHash = optProof ["quasi", "meta", "certificatesHash"] [qcHash, tfHash] + let certificatesHash = optProof chPath [qcHash, tfHash] let blockMeta = optProof ["quasi", "meta"] [bakerInfo, certificatesHash] let blockData = case bbDerivableHashes of DerivableBlockHashesV1{..} -> From 45526d51283a8b6cdbd90c7f2536301521b4b565 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Fri, 26 Jan 2024 17:49:50 +0100 Subject: [PATCH 14/20] Address review comments. --- concordium-base | 2 +- .../GlobalState/Basic/BlockState/LFMBTree.hs | 22 +++++++++------ .../GlobalState/Persistent/BlobStore.hs | 2 ++ .../GlobalState/Persistent/BlockState.hs | 2 +- .../GlobalState/Persistent/LFMBTree.hs | 4 +-- .../GlobalState/Persistent/PoolRewards.hs | 28 ++----------------- .../Concordium/Types/TransactionOutcomes.hs | 2 +- .../globalstate/GlobalStateTests/Instances.hs | 14 ++++++++-- 8 files changed, 35 insertions(+), 41 deletions(-) diff --git a/concordium-base b/concordium-base index 3c039cdda2..e8d3ab90cd 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit 3c039cdda241cac3739a0ff7556f2fe53658c115 +Subproject commit e8d3ab90cdec4822458d233490bc44ecf1ae53eb diff --git a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs index bb7496b120..87906fc473 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Basic/BlockState/LFMBTree.hs @@ -59,7 +59,7 @@ module Concordium.GlobalState.Basic.BlockState.LFMBTree ( setBits, -- * Auxiliary definitions - emptyTreeHash, + emptyTreeHashV0, -- * Structure specification -- $specification @@ -127,7 +127,13 @@ data T v -- hash of the string "EmptyLFMBTree". -- -- * At 'BlockHashVersion1', the tree hash is defined as the hash of the concatenation of the --- size (as a 64-bit big-endian) and the 'BlockHashVersion0' hash of the tree. +-- size (as a 64-bit big-endian) and the 'BlockHashVersion0' hash of the tree. The size of the +-- tree fully determines its structure. Knowing the structure has two important benefits. +-- First, it avoids the possibility of a leaf being mis-interpreted as an inner node, in case +-- it is structurally similar. Second, it allows a verifier of a Merkle proof to know what +-- index a path corresponds to. (In particular, the first index of a right branch depends on +-- the size of the left branch, and that cannot be determined from the right branch alone, but +-- does follow from knowing the size of the tree.) newtype LFMBTreeHash' (bhv :: BlockHashVersion) = LFMBTreeHash {theLFMBTreeHash :: H.Hash} deriving newtype (Eq, Ord, Show, Read, Hashable, S.Serialize) @@ -152,13 +158,13 @@ instance (HashableTo H.Hash v) => HashableTo H.Hash (T v) where -- | The hash used to represent an empty LFMBTree. Defined as the hash of the -- string "EmptyLFMBTree". -emptyTreeHash :: LFMBTreeHashV0 -emptyTreeHash = LFMBTreeHash $ H.hash "EmptyLFMBTree" +emptyTreeHashV0 :: LFMBTreeHashV0 +emptyTreeHashV0 = LFMBTreeHash $ H.hash "EmptyLFMBTree" -- | The (P1) hash of an LFMBTree is defined as the hash of the string "EmptyLFMBTree" if it -- is empty or the hash of the tree otherwise. instance (HashableTo H.Hash v) => HashableTo LFMBTreeHashV0 (LFMBTree k v) where - getHash Empty = emptyTreeHash + getHash Empty = emptyTreeHashV0 getHash (NonEmpty _ v) = LFMBTreeHash $ getHash v -- | The P7 hash of an LFMBTree is the hash of concatenation of the size of the tree (Word64, @@ -376,7 +382,7 @@ lfmbtV0Hash = lfmbtV0Hash' getHash -- function to each element. lfmbtV0Hash' :: (Foldable f) => (v -> H.Hash) -> f v -> LFMBTreeHashV0 {-# INLINE lfmbtV0Hash' #-} -lfmbtV0Hash' hsh = LFMBTreeHash . hashAsLFMBTV0 (theLFMBTreeHash emptyTreeHash) . map hsh . toList +lfmbtV0Hash' hsh = LFMBTreeHash . hashAsLFMBTV0 (theLFMBTreeHash emptyTreeHashV0) . map hsh . toList -- | Hash a list of hashes in the LFMBTree format, using the specified hash for the empty tree. -- This avoids building the full tree. @@ -394,7 +400,7 @@ hashAsLFMBTV1 e l = H.hashLazy $! S.runPutLazy $ do -- | Get the hash of an LFMBTree constructed from a 'Foldable'. -- --- prop> lfmbtV0Hash l == getHash (fromFoldable @Word64 l) +-- prop> lfmbtV1Hash l == getHash (fromFoldable @Word64 l) lfmbtV1Hash :: (Foldable f, HashableTo H.Hash v) => f v -> LFMBTreeHashV1 {-# INLINE lfmbtV1Hash #-} lfmbtV1Hash = lfmbtV1Hash' getHash @@ -403,7 +409,7 @@ lfmbtV1Hash = lfmbtV1Hash' getHash -- function to each element. lfmbtV1Hash' :: (Foldable f) => (v -> H.Hash) -> f v -> LFMBTreeHashV1 {-# INLINE lfmbtV1Hash' #-} -lfmbtV1Hash' hsh = LFMBTreeHash . hashAsLFMBTV1 (theLFMBTreeHash emptyTreeHash) . map hsh . toList +lfmbtV1Hash' hsh = LFMBTreeHash . hashAsLFMBTV1 (theLFMBTreeHash emptyTreeHashV0) . map hsh . toList -- | Get the hash of an LFMBTree constructed from a 'Foldable'. lfmbtHash :: (Foldable f, HashableTo H.Hash v) => SBlockHashVersion bhv -> f v -> LFMBTreeHash' bhv diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs index 56824a3adb..289dcae984 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs @@ -1621,6 +1621,8 @@ migrateHashedBufferedRefKeepHash hb = do -- | Migrate a 'HashedBufferedRef'. The returned reference has a hash computed -- already. The input reference is uncached, and the new references is flushed -- to disk, as well as cached in memory. +-- The hash for the new reference is computed afresh, allowing for a change of +-- hashing scheme and/or a modification of the underlying data. migrateHashedBufferedRef :: (MonadTrans t, MHashableTo (t m) h2 b, BlobStorable m a, BlobStorable (t m) b) => (a -> t m b) -> diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs index 95da937c42..4fcecb31be 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs @@ -617,7 +617,7 @@ migrateBlockRewardDetails StateMigrationParametersP5ToP6{} _ _ (SomeParam TimePa migrateBlockRewardDetails StateMigrationParametersP6ToP7{} _ _ (SomeParam TimeParametersV1{..}) _ = \case (BlockRewardDetailsV1 hbr) -> BlockRewardDetailsV1 - <$> migrateHashedBufferedRef (migratePoolRewardsChangeHash (rewardPeriodEpochs _tpRewardPeriodLength)) hbr + <$> migrateHashedBufferedRef (migratePoolRewards (rewardPeriodEpochs _tpRewardPeriodLength)) hbr instance (MonadBlobStore m, IsBlockHashVersion bhv) => diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs index 30ccb32d7c..b619659265 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/LFMBTree.hs @@ -70,7 +70,7 @@ import Concordium.GlobalState.Basic.BlockState.LFMBTree ( LFMBTreeHash' (..), LFMBTreeHashV0, LFMBTreeHashV1, - emptyTreeHash, + emptyTreeHashV0, setBits, ) import Concordium.GlobalState.Persistent.BlobStore @@ -155,7 +155,7 @@ toHashV0 :: (MHashableTo m H.Hash v, MHashableTo m H.Hash (ref (T ref v))) => LFMBTree' k ref v -> m LFMBTreeHashV0 -toHashV0 Empty = return emptyTreeHash +toHashV0 Empty = return emptyTreeHashV0 toHashV0 (NonEmpty _ v) = LFMBTreeHash <$> getHashM v -- | Compute the version 1 hash of an LFMBTree. diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs index 938e49cc70..53520bcab7 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/PoolRewards.hs @@ -17,7 +17,6 @@ module Concordium.GlobalState.Persistent.PoolRewards ( lookupBakerCapitalAndRewardDetails, migratePoolRewardsP1, migratePoolRewards, - migratePoolRewardsChangeHash, ) where import Control.Exception (assert) @@ -68,36 +67,13 @@ data PoolRewards (bhv :: BlockHashVersion) = PoolRewards -- | Migrate pool rewards from @m@ to the new backing store @t m@. -- This takes the new next payday epoch as a parameter, since this should always be updated on --- a protocol update. This does not allow the hashing scheme to change in the migration, and --- thus can reuse hashes. +-- a protocol update. The hashes for the migratePoolRewards :: - (SupportMigration m t) => - Epoch -> - PoolRewards bhv -> - t m (PoolRewards bhv) -migratePoolRewards newNextPayday PoolRewards{..} = do - nextCapital' <- migrateHashedBufferedRefKeepHash nextCapital - currentCapital' <- migrateHashedBufferedRefKeepHash currentCapital - bakerPoolRewardDetails' <- LFMBT.migrateLFMBTree (migrateReference return) bakerPoolRewardDetails - return - PoolRewards - { nextCapital = nextCapital', - currentCapital = currentCapital', - bakerPoolRewardDetails = bakerPoolRewardDetails', - nextPaydayEpoch = newNextPayday, - .. - } - --- | Migrate pool rewards from @m@ to the new backing store @t m@. --- This takes the new next payday epoch as a parameter, since this should always be updated on --- a protocol update. Compared to 'migratePoolRewards', this implementation supports --- changing the hashing scheme used for the pool rewards. -migratePoolRewardsChangeHash :: (SupportMigration m t, IsBlockHashVersion bhv1) => Epoch -> PoolRewards bhv0 -> t m (PoolRewards bhv1) -migratePoolRewardsChangeHash newNextPayday PoolRewards{..} = do +migratePoolRewards newNextPayday PoolRewards{..} = do nextCapital' <- migrateHashedBufferedRef return nextCapital currentCapital' <- migrateHashedBufferedRef return currentCapital bakerPoolRewardDetails' <- LFMBT.migrateLFMBTree (migrateReference return) bakerPoolRewardDetails diff --git a/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs b/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs index be8f35e3fc..b1c13db00a 100644 --- a/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs +++ b/concordium-consensus/src/Concordium/Types/TransactionOutcomes.hs @@ -112,7 +112,7 @@ emptyTransactionOutcomesHashV1 = <> H.hashToShortByteString (H.hash "EmptyLFMBTree") ) --- | Hash of the empty V1 transaction outcomes structure. This transaction outcomes +-- | Hash of the empty V2 transaction outcomes structure. This transaction outcomes -- structure is used starting in protocol version 7. emptyTransactionOutcomesHashV2 :: TransactionOutcomesHashV 'TOV2 {-# NOINLINE emptyTransactionOutcomesHashV2 #-} diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs index bd53d18127..689ec2c904 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs @@ -75,7 +75,8 @@ checkBinary bop x y sbop sx sy = fail $ "Not satisfied: " ++ sx ++ " (" ++ show x ++ ") " ++ sbop ++ " " ++ sy ++ " (" ++ show y ++ ")" --- | Check an invariant on 'Instances.IT'. The return value is a tuple consisting of: +-- | Check an invariant on 'Instances.IT' (see 'invariantInstances'). The return value is a tuple +-- consisting of: -- * the height of the branch (0 for leaves) -- * whether the branch is full (in the tree sense - not in the sense of no vacancies) -- * whether the branch has vacancies @@ -103,7 +104,16 @@ invariantIT offset (Instances.Branch h f v hsh l r) = do checkBinary (==) v (vl || vr) "<->" "branch has vacancies" "at least one child has vacancies" return (succ h, f, v, offset'', hsh) --- | Check an invariant on 'Instances.Instances'. +-- | Check the following invariant on 'Instances.Instances': +-- * At each leaf node, the account index matches the index in the table. +-- * At each branch node: +-- * The level of the node is 1+ the level of the left child. +-- * The left child is a full subtree. +-- * The recorded hash is the hash of the combined hashes of the left and right subtrees. +-- * The level of the node is at least 1+ the level of the right child. +-- * The branch is marked full if and only if the right subtree is full. +-- * The branch has vacancies exactly when at least one subtree has vacancies. +-- * The root records the correct size of the table. invariantInstances :: (IsProtocolVersion pv) => Instances.Instances pv -> TestMonad () invariantInstances Instances.InstancesEmpty = return () invariantInstances (Instances.InstancesTree size bf) = do From 4d6fb06aacee5e5b9d88b7a8e27a63a180c3e9e0 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Wed, 31 Jan 2024 11:37:17 +0100 Subject: [PATCH 15/20] Tests for Merkle proofs. --- concordium-base | 2 +- .../consensus/ConcordiumTests/KonsensusV1/Common.hs | 13 +++++++++++++ concordium-consensus/tests/consensus/Spec.hs | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/concordium-base b/concordium-base index e8d3ab90cd..f6a8bf3a48 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit e8d3ab90cdec4822458d233490bc44ecf1ae53eb +Subproject commit f6a8bf3a48a094fee0e7a5ba7cfe20c20d4497b2 diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs index 714f20e01b..70544940f5 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs @@ -166,3 +166,16 @@ forEveryProtocolVersionConsensusV1 check = forEveryProtocolVersion $ \spv pvString -> case consensusVersionFor spv of ConsensusV0 -> return () ConsensusV1 -> check spv pvString + +forEveryProtocolVersionBHV1 :: + ( forall pv. + (IsProtocolVersion pv, IsConsensusV1 pv, BlockHashVersionFor pv ~ 'BlockHashVersion1) => + SProtocolVersion pv -> + String -> + Spec + ) -> + Spec +forEveryProtocolVersionBHV1 check = + forEveryProtocolVersionConsensusV1 $ \spv pvString -> case sBlockHashVersionFor spv of + SBlockHashVersion1 -> check spv pvString + _ -> return () diff --git a/concordium-consensus/tests/consensus/Spec.hs b/concordium-consensus/tests/consensus/Spec.hs index 950e51696a..e552c183a1 100644 --- a/concordium-consensus/tests/consensus/Spec.hs +++ b/concordium-consensus/tests/consensus/Spec.hs @@ -22,6 +22,7 @@ import qualified ConcordiumTests.KonsensusV1.TransactionProcessingTest (tests) import qualified ConcordiumTests.KonsensusV1.TreeStateTest (tests) import qualified ConcordiumTests.KonsensusV1.Types (tests) import qualified ConcordiumTests.LeaderElectionTest (tests) +import qualified ConcordiumTests.MerkleProofs (tests) import qualified ConcordiumTests.PassiveFinalization (test) import qualified ConcordiumTests.ReceiveTransactionsTest (test) import qualified ConcordiumTests.Update (test) @@ -67,3 +68,4 @@ main = atLevel $ \lvl -> hspec $ do ConcordiumTests.KonsensusV1.CatchUp.tests ConcordiumTests.EndToEnd.CredentialDeploymentTests.tests lvl ConcordiumTests.EndToEnd.TransactionTableIntegrationTest.tests + ConcordiumTests.MerkleProofs.tests From 25703bf1a901d93f4b6959c6ed1bab53c68a9d6a Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Wed, 31 Jan 2024 16:23:44 +0100 Subject: [PATCH 16/20] Update base --- concordium-base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordium-base b/concordium-base index f6a8bf3a48..97d42999b4 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit f6a8bf3a48a094fee0e7a5ba7cfe20c20d4497b2 +Subproject commit 97d42999b46b0df8c0ecf55d14f398ebe1a2f7a0 From ec6062cc61c3f19e98116b10399a1675a6768e2c Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Fri, 2 Feb 2024 10:52:16 +0100 Subject: [PATCH 17/20] Add missing merkle proofs test file. --- .../consensus/ConcordiumTests/MerkleProofs.hs | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs b/concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs new file mode 100644 index 0000000000..353ee89854 --- /dev/null +++ b/concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs @@ -0,0 +1,181 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + +module ConcordiumTests.MerkleProofs where + +import qualified Data.ByteString as BS +import Data.Functor.Identity +import qualified Data.HashMap.Strict as HM +import Data.Serialize +import Test.Hspec +import Test.QuickCheck + +import Concordium.MerkleProofs +import Concordium.Types +import Concordium.Types.HashableTo +import Concordium.Types.Parameters + +import Concordium.KonsensusV1.Types +import Concordium.Types.Option + +import ConcordiumTests.KonsensusV1.Common +import ConcordiumTests.KonsensusV1.Consensus.Blocks +import ConcordiumTests.KonsensusV1.Types ( + genBakedBlock, + genQuorumCertificate, + genTimeoutCertificate, + ) + +-- | Test that the root hash of a quorum certificate Merkle proof matches the hash of the QC. +propQCMerkleProofMatchesHash :: Property +propQCMerkleProofMatchesHash = forAll genQuorumCertificate $ \qc -> + getHash qc === toRootHash (runIdentity (buildMerkleProof (const True) qc)) + +-- | Test that the root hash of a timeout certificate Merkle proof matches the hash of the TC. +propTCMerkleProofMatchesHash :: Property +propTCMerkleProofMatchesHash = forAll genTimeoutCertificate $ \tc -> + getHash (Present tc) === toRootHash (runIdentity (buildMerkleProof (const True) (Present tc))) + +-- | Test that the root hash of a baked block Merkle proof matches the block hash. +propBBMerkleProofMatchesHash :: + (IsProtocolVersion pv, BlockHashVersionFor pv ~ 'BlockHashVersion1) => + SProtocolVersion pv -> + Property +propBBMerkleProofMatchesHash spv = forAll (genBakedBlock spv) $ \bb -> + blockHash (getHash bb) === toRootHash (runIdentity (buildMerkleProof (const True) bb)) + +-- | Test that parsing a (full) baked block Merkle proof gives the expected structure. +propBBMerkleProofParse :: + forall pv. + (IsProtocolVersion pv, IsConsensusV1 pv, BlockHashVersionFor pv ~ 'BlockHashVersion1) => + SProtocolVersion pv -> + Property +propBBMerkleProofParse spv = + conjoin (theTest <$> [testBB1, testBB2, testBB3, testBB2', testBB3', testBB4', testBB3'', testBB1E, testBB2E, testBB3EX]) + .&&. forAll (genBakedBlock spv) theTest + where + theTest :: BakedBlock pv -> Property + theTest bb@BakedBlock{..} = + let proof = runIdentity (buildMerkleProof (const True) bb) + in case uncurry parseMerkleProof blockSchema proof of + Left err -> counterexample ("Failed to parse proof" ++ show err) False + Right (pt, hsh) -> + blockHash (getHash bb) === hsh + .&&. pt + === HM.fromList + [ ( "header", + Node + ( HM.fromList + [ ("epoch", Leaf (encode bbEpoch)), + ("parent", Leaf (encode (qcBlock bbQuorumCertificate))), + ("round", Leaf (encode bbRound)) + ] + ) + ), + ( "quasi", + Node + ( HM.fromList + [ ( "data", + Node + ( HM.fromList + [ ("transactions", Leaf (encode (computeTransactionsHash SBlockHashVersion1 bbTransactions))), + ("result", Leaf (encode (case bbDerivableHashes of DerivableBlockHashesV1{..} -> dbhv1BlockResultHash))) + ] + ) + ), + ( "meta", + Node + ( HM.fromList + [ ( "certificatesHash", + Node + ( HM.fromList + [ ( "timeoutFinalization", + Node + ( HM.fromList + [ ("epochFinalizationEntry", Node finEntry), + ("timeoutCertificate", Node timeoutCert) + ] + ) + ), + ("quorumCertificate", Node quorumCert) + ] + ) + ), + ( "bakerInfo", + Node + ( HM.fromList + [ ( "nonce", + Node + ( HM.fromList + [ ("blockNonce", Leaf (encode bbNonce)) + ] + ) + ), + ( "timestampBaker", + Node + ( HM.fromList + [ ("bakerId", Leaf (encode bbBaker)), + ("timestamp", Leaf (encode bbTimestamp)) + ] + ) + ) + ] + ) + ) + ] + ) + ) + ] + ) + ) + ] + where + finalizerQCRoundsFor rounds = + Node . HM.fromList . zip [show n | n <- [0 :: Integer ..]] $ + ( \(rnd, finSet) -> + Node (HM.fromList [("round", Leaf (encode rnd)), ("finalizers", Leaf (encodeFinSet finSet))]) + ) + <$> finalizerRoundsList rounds + encodeFinSet finSet = BS.drop 4 (encode finSet) + timeoutCert = case bbTimeoutCertificate of + Absent -> HM.fromList [("null", Leaf "")] + Present TimeoutCertificate{..} -> + HM.fromList + [ ("round", Leaf (encode tcRound)), + ("minEpoch", Leaf (encode tcMinEpoch)), + ("finalizerQCRoundsFirstEpoch", finalizerQCRoundsFor tcFinalizerQCRoundsFirstEpoch), + ("finalizerQCRoundsSecondEpoch", finalizerQCRoundsFor tcFinalizerQCRoundsSecondEpoch), + ("aggregateSignature", Leaf (encode tcAggregateSignature)) + ] + finEntry = case bbEpochFinalizationEntry of + Absent -> HM.fromList [("null", Leaf "")] + Present FinalizationEntry{..} -> + HM.fromList + [ ("finalizedBlock", Leaf (encode (qcBlock feFinalizedQuorumCertificate))), + ("finalizedRound", Leaf (encode (qcRound feFinalizedQuorumCertificate))), + ("epoch", Leaf (encode (qcEpoch feFinalizedQuorumCertificate))), + ("finalizedAggregateSignature", Leaf (encode (qcAggregateSignature feFinalizedQuorumCertificate))), + ("finalizedSignatories", Leaf (encodeFinSet (qcSignatories feFinalizedQuorumCertificate))), + ("successorAggregateSignature", Leaf (encode (qcAggregateSignature feSuccessorQuorumCertificate))), + ("successorSignatories", Leaf (encodeFinSet (qcSignatories feSuccessorQuorumCertificate))), + ("successorProof", Leaf (encode feSuccessorProof)) + ] + quorumCert = + let QuorumCertificate{..} = bbQuorumCertificate + in HM.fromList + [ ("block", Leaf (encode qcBlock)), + ("round", Leaf (encode qcRound)), + ("epoch", Leaf (encode qcEpoch)), + ("aggregateSignature", Leaf (encode qcAggregateSignature)), + ("signatories", Leaf (encodeFinSet qcSignatories)) + ] + +tests :: Spec +tests = describe "MerkleProofs" $ parallel $ do + it "Check hash result for QuorumCertificate" propQCMerkleProofMatchesHash + it "Check hash result for TimeoutCertificate" propTCMerkleProofMatchesHash + forEveryProtocolVersionBHV1 $ \spv pvString -> describe pvString $ do + it "Check hash result for BakedBlock" (propBBMerkleProofMatchesHash spv) + it "Correct parse of BakedBlock proof" (propBBMerkleProofParse spv) From 025e580f82902df707db2693914a949044ba9c41 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Fri, 2 Feb 2024 11:30:10 +0100 Subject: [PATCH 18/20] Update lock file --- concordium-node/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/concordium-node/Cargo.lock b/concordium-node/Cargo.lock index e139825878..5d00db0b5f 100644 --- a/concordium-node/Cargo.lock +++ b/concordium-node/Cargo.lock @@ -681,7 +681,7 @@ dependencies = [ [[package]] name = "concordium-contracts-common" -version = "8.1.1" +version = "9.0.0" dependencies = [ "base64", "bs58", @@ -710,7 +710,7 @@ dependencies = [ [[package]] name = "concordium-smart-contract-engine" -version = "3.1.0" +version = "4.0.0" dependencies = [ "anyhow", "byteorder", @@ -732,7 +732,7 @@ dependencies = [ [[package]] name = "concordium-wasm" -version = "3.0.0" +version = "4.0.0" dependencies = [ "anyhow", "concordium-contracts-common", @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "concordium_base" -version = "3.2.0" +version = "4.0.0" dependencies = [ "aes", "anyhow", From 02f8e8a2b6e847b53ea65ffa10c69237cf22d045 Mon Sep 17 00:00:00 2001 From: Thomas Dinsdale-Young Date: Mon, 5 Feb 2024 16:25:09 +0100 Subject: [PATCH 19/20] Revert accidental installer asset change --- .../installer/resources/WixUIBanner.png | Bin 48969 -> 9092 bytes .../installer/resources/WixUIDialog.png | Bin 150091 -> 113712 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/service/windows/installer/resources/WixUIBanner.png b/service/windows/installer/resources/WixUIBanner.png index c4b3beef182ab1a85411d4c6829376ad78505109..f3b3ce1375e0384f45d4086613eb31f493c90bf3 100644 GIT binary patch literal 9092 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

uA68H>h+#|!)?7@`dw>L2Ou+C(UJEXGMl{@ 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

|C<; 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 Date: Tue, 6 Feb 2024 14:56:35 +0100 Subject: [PATCH 20/20] Bump base --- concordium-base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordium-base b/concordium-base index ea9e60c359..290a4b7849 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit ea9e60c359a4696dd0f186fb775365ccfce93273 +Subproject commit 290a4b78496abaab2ce90c49442dc9e369b2b354