Skip to content

Commit d1ecb8b

Browse files
authored
Merge commit from fork
Change `FinalizerSet` inner representation to improve deserialize/serialize performance
2 parents ebc84d0 + 139b26e commit d1ecb8b

11 files changed

Lines changed: 152 additions & 52 deletions

File tree

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ on:
2222
env:
2323
UBUNTU_VERSION: '22.04'
2424
STATIC_LIBRARIES_IMAGE_TAG: 'rust-1.94_ghc-9.10.2'
25-
RUST_VERSION: '1.94'
25+
RUST_VERSION: '1.94.0'
2626
STACK_VERSION: '3.7.1'
2727
FLATBUFFERS_VERSION: '23.5.26'
2828
GHC_VERSION: '9.10.2'

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased changes
44

55
- upgraded rust version to 1.94
6+
- Change `FinalizerSet` inner representation to `ShortByteString` to improve performance on deserialize/serialize.
67

78
# 10.0.8
89

concordium-consensus/src/Concordium/KonsensusV1/Types.hs

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ module Concordium.KonsensusV1.Types where
1515
import Control.Monad
1616
import Data.Bits
1717
import qualified Data.ByteString as BS
18+
import qualified Data.ByteString.Short as BSS
1819
import qualified Data.Map.Strict as Map
1920
import Data.Maybe
21+
import qualified Data.ProtoLens.Combinators as Proto
2022
import Data.Serialize
2123
import qualified Data.Set as Set
2224
import Data.Singletons
2325
import qualified Data.Vector as Vector
2426
import Data.Word
25-
import Numeric.Natural
27+
import Lens.Micro.Platform
2628

2729
import qualified Concordium.Crypto.BlockSignature as BlockSig
2830
import qualified Concordium.Crypto.BlsSignature as Bls
@@ -43,8 +45,6 @@ import Concordium.Types.TransactionOutcomes
4345
import Concordium.Types.Transactions
4446
import Concordium.Utils.BinarySearch
4547
import Concordium.Utils.Serialization
46-
import qualified Data.ProtoLens.Combinators as Proto
47-
import Lens.Micro.Platform
4848
import qualified Proto.V2.Concordium.Types as Proto
4949
import qualified Proto.V2.Concordium.Types_Fields as ProtoFields
5050

@@ -241,69 +241,80 @@ computeFinalizationCommitteeHash FinalizationCommittee{..} =
241241
-- | A set of 'FinalizerIndex'es.
242242
-- This is represented as a bit vector, where the bit @i@ is set iff the finalizer index @i@ is
243243
-- in the set.
244-
newtype FinalizerSet = FinalizerSet {theFinalizerSet :: Natural}
244+
newtype FinalizerSet = FinalizerSet {theFinalizerSet :: BSS.ShortByteString}
245245
deriving (Eq)
246246

247247
-- | The serialization of a 'FinalizerSet' consists of a length (Word32, big-endian), followed by
248248
-- that many bytes, the first of which (if any) must be non-zero. These bytes encode the bit-vector
249249
-- in big-endian. This enforces that the serialization of a finalizer set is unique.
250+
--
251+
-- Internally, the bit-vector is stored little-endian, so finalizer index @i@ is represented by
252+
-- bit @i mod 8@ of byte @i div 8@. This makes membership tests constant time and keeps decoding
253+
-- linear in the number of serialized bytes.
250254
instance Serialize FinalizerSet where
251-
put fs = do
252-
let (byteCount, putBytes) = unroll 0 (return ()) (theFinalizerSet fs)
253-
putWord32be byteCount
254-
putBytes
255-
where
256-
unroll :: Word32 -> Put -> Natural -> (Word32, Put)
257-
-- Compute the number of bytes and construct a 'Put' that serializes in big-endian.
258-
-- We do this by adding the low order byte to the accumulated 'Put' (at the start)
259-
-- and recursing with the bitvector shifted right 8 bits.
260-
unroll bc cont 0 = (bc, cont)
261-
unroll bc cont n = unroll (bc + 1) (putWord8 (fromIntegral n) >> cont) (shiftR n 8)
255+
put (FinalizerSet fs) = do
256+
putWord32be $ fromIntegral $ BSS.length fs
257+
putByteString $ BS.reverse $ BSS.fromShort fs
262258
get = label "FinalizerSet" $ do
263259
byteCount <- getWord32be
264-
FinalizerSet <$> roll1 byteCount
265-
where
266-
roll1 0 = return 0
267-
roll1 bc = do
268-
b <- getWord8
269-
when (b == 0) $ fail "unexpected 0 byte"
270-
roll (bc - 1) (fromIntegral b)
271-
roll 0 n = return n
272-
roll bc n = do
273-
b <- getWord8
274-
roll (bc - 1) (shiftL n 8 .|. fromIntegral b)
260+
remainingBytes <- remaining
261+
when (toInteger byteCount > toInteger remainingBytes) $
262+
fail "FinalizerSet length exceeds remaining input"
263+
bytes <- getByteString (fromIntegral byteCount)
264+
when (not (BS.null bytes) && BS.head bytes == 0) $
265+
fail "unexpected 0 byte"
266+
return $! FinalizerSet $! BSS.toShort $! BS.reverse bytes
275267

276268
-- | Convert a 'FinalizerSet' to a list of 'FinalizerIndex', in ascending order.
277269
finalizerList :: FinalizerSet -> [FinalizerIndex]
278-
finalizerList = unroll 0 . theFinalizerSet
270+
finalizerList (FinalizerSet fs) = concat $ zipWith finalizersInByte [0, 8 ..] (BSS.unpack fs)
279271
where
280-
unroll _ 0 = []
281-
unroll i x
282-
| testBit x 0 = FinalizerIndex i : r
283-
| otherwise = r
284-
where
285-
r = unroll (i + 1) (shiftR x 1)
272+
finalizersInByte base byte =
273+
[ FinalizerIndex (base + bitIndex)
274+
| bitIndex <- [0 .. 7],
275+
testBit byte (fromIntegral bitIndex)
276+
]
286277

287278
-- | The empty set of finalizers
288279
emptyFinalizerSet :: FinalizerSet
289-
emptyFinalizerSet = FinalizerSet 0
280+
emptyFinalizerSet = FinalizerSet BSS.empty
290281

291282
-- | Add a finalizer to a 'FinalizerSet'.
292283
addFinalizer :: FinalizerSet -> FinalizerIndex -> FinalizerSet
293-
addFinalizer (FinalizerSet setOfFinalizers) (FinalizerIndex i) = FinalizerSet $ setBit setOfFinalizers (fromIntegral i)
284+
addFinalizer (FinalizerSet setOfFinalizers) (FinalizerIndex i) =
285+
FinalizerSet $ BSS.pack $ case splitAt byteIndex paddedBytes of
286+
(prefix, oldByte : suffix) -> prefix ++ setBit oldByte bitIndex : suffix
287+
-- This case cannot occur because 'paddedBytes' has length at least @byteIndex + 1@.
288+
_ -> paddedBytes
289+
where
290+
byteIndex = fromIntegral (i `div` 8)
291+
bitIndex = fromIntegral (i `mod` 8)
292+
bytes = BSS.unpack setOfFinalizers
293+
paddedBytes = bytes ++ replicate (byteIndex + 1 - length bytes) 0
294294

295295
-- | Test whether a given finalizer index is present in a finalizer set.
296296
memberFinalizerSet :: FinalizerIndex -> FinalizerSet -> Bool
297-
memberFinalizerSet (FinalizerIndex fi) (FinalizerSet setOfFinalizers) =
298-
testBit setOfFinalizers (fromIntegral fi)
297+
memberFinalizerSet (FinalizerIndex fi) (FinalizerSet setOfFinalizers)
298+
| byteIndex >= BSS.length setOfFinalizers = False
299+
| otherwise = testBit (BSS.index setOfFinalizers byteIndex) bitIndex
300+
where
301+
byteIndex = fromIntegral (fi `div` 8)
302+
bitIndex = fromIntegral (fi `mod` 8)
299303

300304
-- | Convert a list of [FinalizerIndex] to a 'FinalizerSet'.
301305
finalizerSet :: [FinalizerIndex] -> FinalizerSet
302-
finalizerSet = foldl' addFinalizer (FinalizerSet 0)
306+
finalizerSet = foldl' addFinalizer emptyFinalizerSet
303307

304308
-- | Test if the first finalizer set is a subset of the second.
305309
subsetFinalizerSet :: FinalizerSet -> FinalizerSet -> Bool
306-
subsetFinalizerSet (FinalizerSet s1) (FinalizerSet s2) = s1 .&. s2 == s1
310+
subsetFinalizerSet (FinalizerSet s1) (FinalizerSet s2) = go 0 (BSS.unpack s1)
311+
where
312+
go _ [] = True
313+
go byteIndex (b1 : rest) = b1 .&. b2 == b1 && go (byteIndex + 1) rest
314+
where
315+
b2
316+
| byteIndex < BSS.length s2 = BSS.index s2 byteIndex
317+
| otherwise = 0
307318

308319
instance Show FinalizerSet where
309320
show = show . finalizerList
@@ -329,7 +340,7 @@ data QuorumCertificate = QuorumCertificate
329340

330341
-- | For generating a genesis quorum certificate with empty signature and empty finalizer set.
331342
genesisQuorumCertificate :: BlockHash -> QuorumCertificate
332-
genesisQuorumCertificate genesisHash = QuorumCertificate genesisHash 0 0 mempty $ FinalizerSet 0
343+
genesisQuorumCertificate genesisHash = QuorumCertificate genesisHash 0 0 mempty emptyFinalizerSet
333344

334345
instance Serialize QuorumCertificate where
335346
put QuorumCertificate{..} = do
@@ -1091,6 +1102,7 @@ data DerivableBlockHashesBHV (bhv :: BlockHashVersion) where
10911102
DerivableBlockHashesBHV 'BlockHashVersion1
10921103

10931104
deriving instance Show (DerivableBlockHashesBHV bhv)
1105+
10941106
deriving instance Eq (DerivableBlockHashesBHV bhv)
10951107

10961108
-- | Serialize derivable hashes.

concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ dummyQuorumCertificate blockHash =
5959
qcRound = 0,
6060
qcEpoch = 0,
6161
qcAggregateSignature = mempty,
62-
qcSignatories = FinalizerSet 0
62+
qcSignatories = emptyFinalizerSet
6363
}
6464

6565
-- | A BlockNonce consisting

concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ dummyCertifiedBlock r =
8181
qcRound = r,
8282
qcEpoch = 0,
8383
qcAggregateSignature = mempty,
84-
qcSignatories = FinalizerSet 0
84+
qcSignatories = emptyFinalizerSet
8585
}
8686

8787
-- | Checking that advancing rounds via a quorum certificate results

concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/LMDB.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ dummyQC =
6767
qcRound = 1,
6868
qcEpoch = 1,
6969
qcAggregateSignature = QuorumSignature $ Bls.sign "someMessage" dummyBlsSK,
70-
qcSignatories = FinalizerSet 0
70+
qcSignatories = emptyFinalizerSet
7171
}
7272

7373
-- | A block signature keypair used to construct a block signature.

concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ dummyTimeoutMessage' sProtocolVersion fid e qce =
130130
tsmQCRound = 0,
131131
tsmQCEpoch = 0
132132
}
133-
quorumCert = QuorumCertificate (genesisHash sProtocolVersion) 0 qce mempty $ FinalizerSet 0
133+
quorumCert = QuorumCertificate (genesisHash sProtocolVersion) 0 qce mempty $ emptyFinalizerSet
134134

135135
-- | Generate a timeout message signed by the finalizer index @fid@ from
136136
-- 'bakers' above in epoch @e@ and qc epoch @e@.

concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ testProcessBlockItems sProtocolVersion = describe "processBlockItems" $ do
619619
qcRound = 0,
620620
qcEpoch = 0,
621621
qcAggregateSignature = mempty,
622-
qcSignatories = FinalizerSet 0
622+
qcSignatories = emptyFinalizerSet
623623
},
624624
..
625625
}

concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Types.hs

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
module ConcordiumTests.KonsensusV1.Types where
88

99
import qualified Data.ByteString as BS
10+
import qualified Data.ByteString.Short as BSS
11+
import Data.Either (isLeft)
12+
import qualified Data.FixedByteString as FBS
1013
import qualified Data.Map.Strict as Map
1114
import Data.Serialize
1215
import qualified Data.Vector as Vector
@@ -28,15 +31,20 @@ import Concordium.Types
2831
import qualified Concordium.Types.DummyData as Dummy
2932
import Concordium.Types.Option
3033
import Concordium.Types.Transactions
31-
import qualified Data.FixedByteString as FBS
3234

3335
import qualified Concordium.Types.TransactionOutcomes as TransactionOutcomes
3436
import qualified ConcordiumTests.KonsensusV1.Common as Common
3537

36-
-- | Generate a 'FinalizerSet'. The size parameter determines the size of the committee that
37-
-- the finalizers are (nominally) sampled from.
38+
-- | Generate a 'FinalizerSet'. The size parameter determines the size of the inner byte array, which
39+
-- amounts to s * 8 finalizer indices.
3840
genFinalizerSet :: Gen FinalizerSet
39-
genFinalizerSet = sized $ \s -> FinalizerSet . fromInteger <$> chooseInteger (0, 2 ^ s)
41+
genFinalizerSet = sized $ \s -> do
42+
len <- chooseInt (0, max 0 s)
43+
if len == 0
44+
then return $ FinalizerSet BSS.empty
45+
else do
46+
bytes <- vectorOf len (arbitrary @Word8) `suchThat` ((/= 0) . last)
47+
return $ FinalizerSet $ BSS.pack bytes
4048

4149
-- | An arbitrarily-chosen 'Bls.SecretKey'.
4250
someBlsSecretKey :: Bls.SecretKey
@@ -435,6 +443,60 @@ propFinalizerListIsInverseOfFinalizerSet =
435443
fis
436444
(finalizerSet $ finalizerList fis)
437445

446+
-- | Malformed 'FinalizerSet' serialization where the encoded byte count exceeds the payload.
447+
truncatedFinalizerSetBytes :: BS.ByteString
448+
truncatedFinalizerSetBytes = runPut $ do
449+
putWord32be 2
450+
putWord8 0xff
451+
452+
-- | Non-canonical 'FinalizerSet' serialization with a leading zero byte.
453+
nonCanonicalFinalizerSetBytes :: BS.ByteString
454+
nonCanonicalFinalizerSetBytes = runPut $ do
455+
putWord32be 2
456+
putWord8 0x00
457+
putWord8 0xff
458+
459+
-- | Wire encoding of finalizer set @{0, 1, 3, 10}@.
460+
encodedFinalizerSet01310 :: BS.ByteString
461+
encodedFinalizerSet01310 = runPut $ do
462+
putWord32be 2
463+
putWord8 0x04
464+
putWord8 0x0b
465+
466+
genSmallFinalizerIndex :: Gen FinalizerIndex
467+
genSmallFinalizerIndex = FinalizerIndex . fromIntegral <$> chooseInt (0, 255)
468+
469+
-- | 'memberFinalizerSet' returns 'True' for all finalizer indices in the list of finalizer
470+
-- indices used to construct the `FinalizerSet`
471+
propMemberFinalizerSetMatchesListMembership :: Property
472+
propMemberFinalizerSetMatchesListMembership =
473+
forAll (listOf genSmallFinalizerIndex) $ \indices ->
474+
let fs = finalizerSet indices
475+
in all (`memberFinalizerSet` fs) indices === True
476+
477+
-- | 'memberFinalizerSet' returns 'False' for any finalizer index _not_ in the list of finalizer
478+
-- indices used to construct the `FinalizerSet`
479+
propMemberFinalizerSetRejectsAbsent :: Property
480+
propMemberFinalizerSetRejectsAbsent =
481+
forAll (listOf genSmallFinalizerIndex) $ \indices ->
482+
forAll (listOf1 (suchThat genSmallFinalizerIndex (`notElem` indices))) $ \notIndices ->
483+
let fs = finalizerSet indices
484+
in any (`memberFinalizerSet` fs) notIndices === False
485+
486+
propSubsetFinalizerSetMatchesListSubset :: Property
487+
propSubsetFinalizerSetMatchesListSubset =
488+
forAll (listOf genSmallFinalizerIndex) $ \xs ->
489+
forAll (sublistOf xs) $ \ys ->
490+
subsetFinalizerSet (finalizerSet ys) (finalizerSet xs) === True
491+
492+
propSubsetFinalizerSetRejectsMissingMember :: Property
493+
propSubsetFinalizerSetRejectsMissingMember =
494+
withMaxSuccess 1000 $
495+
forAll (listOf genSmallFinalizerIndex) $ \ys ->
496+
forAll (suchThat genSmallFinalizerIndex (`notElem` ys)) $ \missing ->
497+
let xs = missing : ys
498+
in subsetFinalizerSet (finalizerSet xs) (finalizerSet ys) === False
499+
438500
tests :: Spec
439501
tests = describe "KonsensusV1.Types" $ do
440502
it "FinalizerSet serialization" propSerializeFinalizerSet
@@ -454,6 +516,31 @@ tests = describe "KonsensusV1.Types" $ do
454516
it "QuorumSignatureMessage signature check fails with different key" propSignQuorumSignatureMessageDiffKey
455517
it "QuorumSignatureMessage signature check fails with different body" propSignQuorumSignatureMessageDiffBody
456518
it "Conversion to and from FinalizerSet" propFinalizerListIsInverseOfFinalizerSet
519+
it "FinalizerSet membership matches list membership" propMemberFinalizerSetMatchesListMembership
520+
it "FinalizerSet membership rejects absent indices" propMemberFinalizerSetRejectsAbsent
521+
it "FinalizerSet membership reflects added finalizers" $ do
522+
let fs = finalizerSet [FinalizerIndex 0, FinalizerIndex 3, FinalizerIndex 7, FinalizerIndex 9]
523+
memberFinalizerSet (FinalizerIndex 0) fs `shouldBe` True
524+
memberFinalizerSet (FinalizerIndex 1) fs `shouldBe` False
525+
memberFinalizerSet (FinalizerIndex 3) fs `shouldBe` True
526+
memberFinalizerSet (FinalizerIndex 7) fs `shouldBe` True
527+
memberFinalizerSet (FinalizerIndex 8) fs `shouldBe` False
528+
memberFinalizerSet (FinalizerIndex 9) fs `shouldBe` True
529+
it "FinalizerSet subset matches list subset" propSubsetFinalizerSetMatchesListSubset
530+
it "FinalizerSet subset rejects if missing member" propSubsetFinalizerSetRejectsMissingMember
531+
it "FinalizerSet subset checks set inclusion" $ do
532+
let fs = finalizerSet [FinalizerIndex 0, FinalizerIndex 1, FinalizerIndex 3, FinalizerIndex 9]
533+
subset = finalizerSet [FinalizerIndex 0, FinalizerIndex 9]
534+
notSubset = finalizerSet [FinalizerIndex 1, FinalizerIndex 8]
535+
subsetFinalizerSet subset fs `shouldBe` True
536+
subsetFinalizerSet notSubset fs `shouldBe` False
537+
it "FinalizerSet serialization uses canonical big-endian wire encoding" $ do
538+
encode (finalizerSet [FinalizerIndex 0, FinalizerIndex 1, FinalizerIndex 3, FinalizerIndex 10]) `shouldBe` encodedFinalizerSet01310
539+
decode encodedFinalizerSet01310 `shouldBe` Right (finalizerSet [FinalizerIndex 0, FinalizerIndex 1, FinalizerIndex 3, FinalizerIndex 10])
540+
it "FinalizerSet deserialization rejects non-canonical leading zero byte" $ do
541+
(decode nonCanonicalFinalizerSetBytes :: Either String FinalizerSet) `shouldSatisfy` isLeft
542+
it "FinalizerSet deserialization rejects a byte count that exceeds the remaining input" $ do
543+
(decode truncatedFinalizerSetBytes :: Either String FinalizerSet) `shouldSatisfy` isLeft
457544
Common.forEveryProtocolVersionConsensusV1 $ \spv pvString -> do
458545
describe pvString $ do
459546
it "FinalizationEntry serialization" $ propSerializeFinalizationEntry spv

concordium-node/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)