diff --git a/changelog.d/20260311_091905_agustin.mista_committee_selection_rules.md b/changelog.d/20260311_091905_agustin.mista_committee_selection_rules.md new file mode 100644 index 0000000000..53aa1b992f --- /dev/null +++ b/changelog.d/20260311_091905_agustin.mista_committee_selection_rules.md @@ -0,0 +1,23 @@ + + + +### Non-Breaking + +- Add generic weigthed Fait-Accompli committee selection implementation. + + diff --git a/ouroboros-consensus.cabal b/ouroboros-consensus.cabal index a6f02c0dac..e2bc0cb8c7 100644 --- a/ouroboros-consensus.cabal +++ b/ouroboros-consensus.cabal @@ -115,6 +115,14 @@ library Ouroboros.Consensus.BlockchainTime.WallClock.Simple Ouroboros.Consensus.BlockchainTime.WallClock.Types Ouroboros.Consensus.BlockchainTime.WallClock.Util + Ouroboros.Consensus.Committee.AcrossEpochs + Ouroboros.Consensus.Committee.BitMap + Ouroboros.Consensus.Committee.Crypto + Ouroboros.Consensus.Committee.EveryoneVotes + Ouroboros.Consensus.Committee.LS + Ouroboros.Consensus.Committee.Types + Ouroboros.Consensus.Committee.WFA + Ouroboros.Consensus.Committee.WFALS Ouroboros.Consensus.Config Ouroboros.Consensus.Config.SecurityParam Ouroboros.Consensus.Config.SupportsNode @@ -223,6 +231,7 @@ library Ouroboros.Consensus.Node.Run Ouroboros.Consensus.Node.Serialisation Ouroboros.Consensus.NodeId + Ouroboros.Consensus.Peras.Crypto Ouroboros.Consensus.Peras.Params Ouroboros.Consensus.Peras.SelectView Ouroboros.Consensus.Peras.Vote @@ -342,6 +351,7 @@ library build-depends: FailT ^>=0.1.2, aeson, + array, base >=4.14 && <4.23, base-deriving-via, base16-bytestring >=1.0, diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/AcrossEpochs.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/AcrossEpochs.hs new file mode 100644 index 0000000000..77a6179d4b --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/AcrossEpochs.hs @@ -0,0 +1,56 @@ +-- | This module extends the committee selection scheme to work across epochs. +-- +-- This is needed to support the case of validating an old vote from a previous +-- epoch arriving too late. In the general case, this means we would need to +-- store an arbitrary number of past committee selections. However, since: +-- 1. the length of an epoch is much larger than the immutability window, and +-- 2. we don't care about validating old votes that cannot affect our current +-- selection beyond the immutability window, it follows that +-- we only need to store the committee selection for the current and previous +-- epochs. +module Ouroboros.Consensus.Committee.AcrossEpochs + ( InterEpochCommitteeSelection (..) + , newEpoch + ) where + +import Cardano.Ledger.BaseTypes (Nonce) +import Ouroboros.Consensus.Committee.Types + ( TargetCommitteeSize + ) +import qualified Ouroboros.Consensus.Committee.WFA as WFA +import qualified Ouroboros.Consensus.Committee.WFALS as WFALS + +data InterEpochCommitteeSelection crypto = InterEpochCommitteeSelection + { currEpochSelection :: WFALS.CommitteeSelection crypto + , prevEpochSelection :: WFALS.CommitteeSelection crypto + } + +-- | Update an inter-epoch committee selection at the beginning of a new epoch +newEpoch :: + -- | New epoch nonce + Nonce -> + -- | New epoch cumulative stake distribution + WFA.ExtWFAStakeDistr (WFALS.PublicKey crypto) -> + -- | New epoch expected committee size + TargetCommitteeSize -> + -- | Current inter-epoch committee selection + InterEpochCommitteeSelection crypto -> + Either WFA.WFAError (InterEpochCommitteeSelection crypto) +newEpoch + newEpochNonce + newEpochStakeDistr + newEpochCommitteeSize + interEpochSelection = do + newEpochSelection <- + WFALS.mkCommitteeSelection + newEpochNonce + newEpochCommitteeSize + newEpochStakeDistr + pure $ + InterEpochCommitteeSelection + { currEpochSelection = + newEpochSelection + , prevEpochSelection = + currEpochSelection + interEpochSelection + } diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/BitMap.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/BitMap.hs new file mode 100644 index 0000000000..03a12edd18 --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/BitMap.hs @@ -0,0 +1,131 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- | A compact bitmap representation for tracking voter participation or binary +-- flags. +-- +-- Adapted from @Cardano.Leios.BitMapPV@ in the @leios-wfa-ls-demo@ package, +-- with the \"PV\" suffix dropped. +module Ouroboros.Consensus.Committee.BitMap + ( BitMap + , bitmapFromIndices + , bitmapToIndices + , rawSerialiseBitMap + , rawDeserialiseBitMap + ) where + +import Cardano.Binary (FromCBOR (..), ToCBOR (..)) +import qualified Codec.CBOR.Decoding as CBOR +import qualified Codec.CBOR.Encoding as CBOR +import Control.Monad (forM_, when) +import Data.Bits + ( countTrailingZeros + , popCount + , unsafeShiftL + , (.&.) + , (.|.) + ) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Internal as BSI +import Data.Word (Word8) +import Foreign.Marshal.Utils (fillBytes) +import Foreign.Storable (peekByteOff, pokeByteOff) + +-- | A compact bitmap representation for tracking voter participation or binary +-- flags. The logical upper bound is stored explicitly so serialization +-- round-trips exactly. +data BitMap a = BitMap !a !BS.ByteString + deriving Eq + +instance Show a => Show (BitMap a) where + show (BitMap maxIx bs) = + "BitMap{maxIx=" + ++ show maxIx + ++ ",bytes=" + ++ show (BS.length bs) + ++ ",set=" + ++ show (countSetBits bs) + ++ "}" + where + countSetBits arr = + sum [popCount (BS.index arr i) | i <- [0 .. BS.length arr - 1]] + +-- | Construct a 'BitMap' from a list of indexes that should be set (flipped to +-- 1) and a maximum index (inclusive upper bound). +bitmapFromIndices :: + Integral a => + a -> + [a] -> + BitMap a +bitmapFromIndices maxIx flipped = + BitMap maxIx $ + BSI.unsafeCreate nBytes $ \ptr -> do + fillBytes ptr 0 nBytes + forM_ flipped $ \ix -> do + let !i = fromIntegral ix :: Int + when (i >= 0 && i <= maxI) $ do + let !byteIx = i `quot` 8 + !bitIx = i `rem` 8 + !mask = bitMask bitIx + w <- peekByteOff ptr byteIx :: IO Word8 + pokeByteOff ptr byteIx (w .|. mask) + where + !maxI = fromIntegral maxIx :: Int + !nBytes = (maxI `quot` 8) + 1 + +-- | Retrieve all indexes that are set (flipped to 1) in the bitmap, in +-- ascending order. +bitmapToIndices :: Integral a => BitMap a -> [a] +bitmapToIndices (BitMap maxIx bitmap) = + goBytes 0 + where + !maxI = fromIntegral maxIx :: Int + !nBytes = BS.length bitmap + + goBytes !byteIx + | byteIx >= nBytes = [] + | otherwise = + let !w = BS.index bitmap byteIx + in goBits (byteIx * 8) w ++ goBytes (byteIx + 1) + + goBits !_ 0 = [] + goBits !base !w = + let !bitIx = countTrailingZeros w + !i = base + bitIx + !w' = w .&. (w - 1) + in if i <= maxI + then fromIntegral i : goBits base w' + else [] + +bitMask :: Int -> Word8 +bitMask k = fromIntegral ((1 :: Int) `unsafeShiftL` k) + +-- | Raw serialisation of the bitmap (just the underlying bytes, without the +-- logical upper bound). +rawSerialiseBitMap :: BitMap a -> BS.ByteString +rawSerialiseBitMap (BitMap _ bs) = bs + +-- | Raw deserialisation of a bitmap from a logical upper bound and a +-- 'BS.ByteString'. Returns 'Nothing' if the byte string length does not match +-- the expected size for the given upper bound. +rawDeserialiseBitMap :: Integral a => a -> BS.ByteString -> Maybe (BitMap a) +rawDeserialiseBitMap maxIx bs + | BS.length bs /= expectedBytes = Nothing + | otherwise = Just (BitMap maxIx bs) + where + expectedBytes = (fromIntegral maxIx `quot` 8) + 1 + +instance ToCBOR a => ToCBOR (BitMap a) where + toCBOR (BitMap maxIx bs) = + CBOR.encodeListLen 2 + <> toCBOR maxIx + <> CBOR.encodeBytes bs + +instance (Integral a, FromCBOR a) => FromCBOR (BitMap a) where + fromCBOR = do + CBOR.decodeListLenOf 2 + maxIx <- fromCBOR + bs <- CBOR.decodeBytes + case rawDeserialiseBitMap maxIx bs of + Nothing -> fail "BitMap: invalid bitmap data or size mismatch" + Just bitmap -> pure bitmap diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/Crypto.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/Crypto.hs new file mode 100644 index 0000000000..06d711fdd8 --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/Crypto.hs @@ -0,0 +1,295 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Crypto interface needed for committee selection +module Ouroboros.Consensus.Committee.Crypto + ( -- * Types associated to committee selection + ElectionId + , VoteMessage + , PrivateKeys + , PublicKeys + + -- * Vote signature crypto interface + , CryptoSupportsVoteSigning (..) + , CryptoSupportsGroupVoteSigning (..) + , CryptoSupportsNaiveGroupVoteSigning + + -- * VRF crypto interface + , VRFPoolContext (..) + , NormalizedVRFOutput (..) + , CryptoSupportsVRF (..) + , CryptoSupportsGroupVRF (..) + , CryptoSupportsNaiveGroupVRF + ) where + +import Cardano.Ledger.BaseTypes (Nonce) +import Data.Either (partitionEithers) +import Data.Kind (Type) +import Data.List (intercalate) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as NE +import Data.Proxy (Proxy) + +-- * Types associated to committee selection + +-- | Election identifier used for committee seleciton +type family ElectionId crypto :: Type + +type family VoteMessage crypto :: Type + +type family PrivateKeys crypto + +-- | Public key type for wFA^LS committee membership +type family PublicKeys crypto + +-- * Signature crypto interface + +-- | Crypto interface used for signing and verifying votes +class + ( Eq (ElectionId crypto) + , Show (ElectionId crypto) + , Eq (VoteMessage crypto) + , Show (VoteMessage crypto) + , Eq (PrivateKeys crypto) + , Show (PrivateKeys crypto) + , Eq (PublicKeys crypto) + , Show (PublicKeys crypto) + , Eq (VoteSignaturePrivateKey crypto) + , Show (VoteSignaturePrivateKey crypto) + , Eq (VoteSignaturePublicKey crypto) + , Show (VoteSignaturePublicKey crypto) + , Eq (VoteSignature crypto) + , Show (VoteSignature crypto) + ) => + CryptoSupportsVoteSigning crypto + where + -- | Private key for signing + type VoteSignaturePrivateKey crypto :: Type + + -- | Public key used for verification + type VoteSignaturePublicKey crypto :: Type + + -- | Signature over an election identifier + data VoteSignature crypto :: Type + + -- | Cast a committee public key into a vote signature public key + getVoteSignaturePublicKey :: + Proxy crypto -> + PublicKeys crypto -> + VoteSignaturePublicKey crypto + + -- | Cast a committee private key into a vote signature private key + getVoteSignaturePrivateKey :: + Proxy crypto -> + PrivateKeys crypto -> + VoteSignaturePrivateKey crypto + + -- | Sign a vote election identifier + signVote :: + VoteSignaturePrivateKey crypto -> + ElectionId crypto -> + VoteMessage crypto -> + VoteSignature crypto + + -- | Verify a vote signature over an election identifier + verifyVoteSignature :: + VoteSignaturePublicKey crypto -> + ElectionId crypto -> + VoteMessage crypto -> + VoteSignature crypto -> + Either String () + +class + ( Semigroup (GroupVoteSignaturePublicKey crypto) + , Semigroup (GroupVoteSignature crypto) + , Eq (GroupVoteSignaturePublicKey crypto) + , Show (GroupVoteSignaturePublicKey crypto) + , Eq (GroupVoteSignature crypto) + , Show (GroupVoteSignature crypto) + ) => + CryptoSupportsGroupVoteSigning crypto + where + type GroupVoteSignaturePublicKey crypto :: Type + type GroupVoteSignature crypto :: Type + + liftVoteSignaturePublicKey :: + Proxy crypto -> VoteSignaturePublicKey crypto -> GroupVoteSignaturePublicKey crypto + liftVoteSignature :: Proxy crypto -> VoteSignature crypto -> GroupVoteSignature crypto + + verifyGroupVoteSignature :: + Proxy crypto -> + GroupVoteSignaturePublicKey crypto -> + ElectionId crypto -> + VoteMessage crypto -> + GroupVoteSignature crypto -> + Either String () + +class CryptoSupportsVoteSigning crypto => CryptoSupportsNaiveGroupVoteSigning crypto + +instance CryptoSupportsNaiveGroupVoteSigning crypto => CryptoSupportsGroupVoteSigning crypto where + type GroupVoteSignaturePublicKey crypto = NonEmpty (VoteSignaturePublicKey crypto) + type GroupVoteSignature crypto = NonEmpty (VoteSignature crypto) + + liftVoteSignaturePublicKey _proxy pk = NE.singleton pk + liftVoteSignature _proxy sig = NE.singleton sig + + verifyGroupVoteSignature _proxy groupPublicKey electionId message groupSignature + | length groupPublicKey /= length groupSignature = + Left "Group vote signature verification failed: mismatched number of public keys and signatures" + | otherwise = + let results = + zipWith + (\pk sig -> verifyVoteSignature pk electionId message sig) + (NE.toList groupPublicKey) + (NE.toList groupSignature) + (errors, _) = partitionEithers results + in case errors of + [] -> Right () + _ -> Left $ "Group vote signature verification failed: " ++ intercalate "; " errors + +-- * VRF crypto interface + +-- | Context in which a VRF input is evaluated. +-- +-- This distinguishes between the case where we want to compute our own VRF +-- output, and the case where we want to verify the VRF output of someone else. +data VRFPoolContext crypto + = -- | Compute our own VRF output by signing the VRF input with our signing key + VRFSignContext (VRFSigningKey crypto) + | -- | Verify the local sortition output of another participant by verifying + -- their signature over the VRF input using their verification key + VRFVerifyContext (VRFVerifyKey crypto) (VRFOutput crypto) + +deriving instance + ( Eq (VRFSigningKey crypto) + , Eq (VRFVerifyKey crypto) + , Eq (VRFOutput crypto) + ) => + Eq (VRFPoolContext crypto) + +deriving instance + ( Show (VRFSigningKey crypto) + , Show (VRFVerifyKey crypto) + , Show (VRFOutput crypto) + ) => + Show (VRFPoolContext crypto) + +-- | Normalized VRF outputs as a rational between 0 and 1 +newtype NormalizedVRFOutput = NormalizedVRFOutput + { unNormalizedVRFOutput :: Rational + } + deriving (Eq, Show) + +-- | Crypto interface used to evaluate non-persistent voters via local sortition +class + ( Eq (ElectionId crypto) + , Show (ElectionId crypto) + , Eq (PrivateKeys crypto) + , Show (PrivateKeys crypto) + , Eq (PublicKeys crypto) + , Show (PublicKeys crypto) + , Eq (VRFSigningKey crypto) + , Show (VRFSigningKey crypto) + , Eq (VRFVerifyKey crypto) + , Show (VRFVerifyKey crypto) + , Eq (VRFElectionInput crypto) + , Show (VRFElectionInput crypto) + , Eq (VRFOutput crypto) + , Show (VRFOutput crypto) + ) => + CryptoSupportsVRF crypto + where + -- | Private key used for computing our own VRF output + type VRFSigningKey crypto :: Type + + -- | Public key used for verifying the VRF output of other participants + type VRFVerifyKey crypto :: Type + + -- | Input to the verifiable random function. + -- + -- This is fixed across all participants for a given election. + data VRFElectionInput crypto :: Type + + -- | Output of the verifiable random function + data VRFOutput crypto :: Type + + -- | Cast a committee public key into a VRF verification key + getVRFVerifyKey :: + Proxy crypto -> + PublicKeys crypto -> + VRFVerifyKey crypto + + -- | Cast a committee private key into a VRF signing key + getVRFSigningKey :: + Proxy crypto -> + PrivateKeys crypto -> + VRFSigningKey crypto + + -- | Construct a VRF input from a nonce and an election identifier + mkVRFElectionInput :: + Nonce -> + ElectionId crypto -> + VRFElectionInput crypto + + -- | Evaluate a VRF input in a given context + evalVRF :: + VRFPoolContext crypto -> + VRFElectionInput crypto -> + Either String (VRFOutput crypto) + + -- | Normalize a VRF output to a value in [0, 1] + normalizeVRFOutput :: + VRFOutput crypto -> + NormalizedVRFOutput + +class CryptoSupportsVRF crypto => CryptoSupportsNaiveGroupVRF crypto + +class + ( Semigroup (VRFGroupVerifyKey crypto) + , Semigroup (VRFGroupOutput crypto) + , Eq (VRFGroupVerifyKey crypto) + , Show (VRFGroupVerifyKey crypto) + , Eq (VRFGroupOutput crypto) + , Show (VRFGroupOutput crypto) + ) => + CryptoSupportsGroupVRF crypto + where + type VRFGroupVerifyKey crypto :: Type + type VRFGroupOutput crypto :: Type + + liftVRFVerifyKey :: Proxy crypto -> VRFVerifyKey crypto -> VRFGroupVerifyKey crypto + liftVRFOutput :: Proxy crypto -> VRFOutput crypto -> VRFGroupOutput crypto + + verifyGroupVRF :: + Proxy crypto -> + VRFGroupVerifyKey crypto -> + VRFElectionInput crypto -> + VRFGroupOutput crypto -> + Either String () + +instance CryptoSupportsNaiveGroupVRF crypto => CryptoSupportsGroupVRF crypto where + type VRFGroupVerifyKey crypto = NonEmpty (VRFVerifyKey crypto) + type VRFGroupOutput crypto = NonEmpty (VRFOutput crypto) + + liftVRFVerifyKey _proxy vk = NE.singleton vk + liftVRFOutput _proxy output = NE.singleton output + + verifyGroupVRF _proxy groupVerifyKey electionId groupOutput + | length groupVerifyKey /= length groupOutput = + Left "Group VRF verification failed: mismatched number of verify keys and outputs" + | otherwise = + -- Verify each individual VRF output against the corresponding verify key + let results = + zipWith + (\vk output -> evalVRF (VRFVerifyContext vk output) electionId) + (NE.toList groupVerifyKey) + (NE.toList groupOutput) + (errors, _) = partitionEithers results + in case errors of + [] -> Right () + _ -> Left $ "Group VRF verification failed: " ++ intercalate "; " errors diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/EveryoneVotes.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/EveryoneVotes.hs new file mode 100644 index 0000000000..43425d10e9 --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/EveryoneVotes.hs @@ -0,0 +1,320 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | "Everyone Votes" committee selection scheme. +-- +-- In this simplified scheme, every registered stake pool operator is a member +-- of the voting committee. There is no sortition or persistent/non-persistent +-- distinction. This is useful as a prototype or baseline implementation. +module Ouroboros.Consensus.Committee.EveryoneVotes + ( -- * Crypto constraint + CryptoSupportsEveryoneVotes (..) + + -- * Committee selection data + , EveryoneVotesCommitteeSelection (..) + , EveryoneVotesCommitteeSelectionError (..) + + -- * Vote type + , EveryoneVotesVote (..) + ) where + +import Cardano.Ledger.BaseTypes.NonZero (NonZero, nonZero) +import Data.Bifunctor (Bifunctor (..)) +import Data.Foldable (Foldable (foldl')) +import Data.Function (on) +import Data.List (sortOn) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as NE +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Proxy (Proxy (..)) +import Data.Semigroup (Semigroup (..)) +import Ouroboros.Consensus.Committee.BitMap (BitMap, bitmapFromIndices, bitmapToIndices) +import Ouroboros.Consensus.Committee.Crypto + ( CryptoSupportsGroupVoteSigning (..) + , CryptoSupportsVoteSigning (..) + , ElectionId + , PrivateKeys + , PublicKeys + , VoteMessage + ) +import Ouroboros.Consensus.Committee.Types + ( CryptoSupportsCommitteeSelection (..) + , LedgerStake (..) + , PoolId + , VoteWeight (..) + , VotesWithSameTarget + , VotingWithAggregation (..) + , VotingWithCommitteeSelection (..) + , getElectionIdFromVotes + , getRawVotes + , getVoteMessageFromVotes + ) +import Ouroboros.Consensus.Committee.WFA + ( Candidate + , ExtWFAStakeDistr (..) + , SeatIndex (..) + , getCandidateInSeat + , seatIndexWithinBounds + ) + +-- | Crypto constraint for the "Everyone Votes" scheme. +-- +-- Only vote signing is required (no VRF). +class CryptoSupportsVoteSigning crypto => CryptoSupportsEveryoneVotes crypto + +-- | Committee selection data for the "Everyone Votes" scheme. +data EveryoneVotesCommitteeSelection crypto = EveryoneVotesCommitteeSelection + { evcsElectionId :: !(ElectionId crypto) + -- ^ Election ID for this committee selection + , evStakeDistr :: !(ExtWFAStakeDistr (PublicKeys crypto)) + -- ^ Reusing the stake distr type from WFA because it's easier in this prototype + , evCandidateSeats :: !(Map PoolId SeatIndex) + } + +deriving instance + (Eq (PublicKeys crypto), Eq (ElectionId crypto)) => Eq (EveryoneVotesCommitteeSelection crypto) + +deriving instance + (Show (PublicKeys crypto), Show (ElectionId crypto)) => + Show (EveryoneVotesCommitteeSelection crypto) + +-- | Errors that can occur during "Everyone Votes" committee selection. +data EveryoneVotesCommitteeSelectionError crypto + = EVMissingPoolId PoolId + | EVInvalidSeatIndex SeatIndex + | EVZeroStake SeatIndex + | EVInvalidVoteSignature String + | EVCertElectionIdMismatch + | EVEmptyCert + | EVInvalidGroupVoteSignature String + deriving (Show, Eq) + +-- | A vote in the "Everyone Votes" scheme. +data EveryoneVotesVote crypto = EveryoneVotesVote + { evSeatIndex :: SeatIndex + , evElectionId :: ElectionId crypto + , evMessage :: VoteMessage crypto + , evSignature :: VoteSignature crypto + } + +deriving instance + ( Eq (ElectionId crypto) + , Eq (VoteMessage crypto) + , Eq (VoteSignature crypto) + ) => + Eq (EveryoneVotesVote crypto) + +deriving instance + ( Show (ElectionId crypto) + , Show (VoteMessage crypto) + , Show (VoteSignature crypto) + ) => + Show (EveryoneVotesVote crypto) + +-- * Instances + +data EveryoneVotesCommitteeMember crypto = EveryoneVotesCommitteeMember + { evcmCandidate :: Candidate (PublicKeys crypto) + , evcmInvariant :: NonZero (LedgerStake) + , evcmVoteWeight :: VoteWeight + } + +deriving instance + Show (PublicKeys crypto) => Show (EveryoneVotesCommitteeMember crypto) + +deriving instance + Eq (PublicKeys crypto) => Eq (EveryoneVotesCommitteeMember crypto) + +instance + CryptoSupportsEveryoneVotes crypto => + CryptoSupportsCommitteeSelection crypto (EveryoneVotesCommitteeSelection crypto) + where + type CryptoOf (EveryoneVotesCommitteeSelection crypto) = crypto + type + CommitteeSelectionError (EveryoneVotesCommitteeSelection crypto) = + EveryoneVotesCommitteeSelectionError crypto + type CommitteeMember (EveryoneVotesCommitteeSelection crypto) = EveryoneVotesCommitteeMember crypto + + checkShouldVote :: + EveryoneVotesCommitteeSelection crypto -> + PoolId -> + PrivateKeys crypto -> + ElectionId crypto -> + Either + (EveryoneVotesCommitteeSelectionError crypto) + (Maybe (EveryoneVotesCommitteeMember crypto)) + checkShouldVote selection ourId _ _ + | Just seatIndex <- Map.lookup ourId (evCandidateSeats selection) = + case lookupCommitteeMember selection seatIndex of + Left (EVZeroStake _) -> pure Nothing + Right member -> pure (Just member) + Left err -> Left err + | otherwise = + Left (EVMissingPoolId ourId) + + committeeMemberWeight :: + EveryoneVotesCommitteeMember crypto -> + VoteWeight + committeeMemberWeight = evcmVoteWeight + +instance + CryptoSupportsEveryoneVotes crypto => + VotingWithCommitteeSelection + crypto + (EveryoneVotesCommitteeSelection crypto) + where + type Vote (EveryoneVotesCommitteeSelection crypto) = EveryoneVotesVote crypto + + forgeVote member ourPrivateKeys electionId message = + let (seatIndex, _, _, _, _) = evcmCandidate member + sig = signVote (getVoteSignaturePrivateKey (Proxy @crypto) ourPrivateKeys) electionId message + in EveryoneVotesVote + { evSeatIndex = seatIndex + , evElectionId = electionId + , evMessage = message + , evSignature = sig + } + + verifyVote selection vote = + let EveryoneVotesVote seatIndex electionId message sig = vote + in do + member <- lookupCommitteeMember selection seatIndex + let (_, _, voterPublicKeys, _, _) = evcmCandidate member + voterSignaturePublicKey = + getVoteSignaturePublicKey (Proxy @crypto) voterPublicKeys + first EVInvalidVoteSignature $ + verifyVoteSignature + voterSignaturePublicKey + electionId + message + sig + pure member + + getElectionIdFromVote (EveryoneVotesVote _ electionId _ _) = electionId + getVoteMessageFromVote (EveryoneVotesVote _ _ message _) = message + +-- | Look up a committee member by seat index. +-- +-- Checks that the seat index is within bounds and that the voter has +-- non-zero stake. Does NOT verify signatures. +lookupCommitteeMember :: + EveryoneVotesCommitteeSelection crypto -> + SeatIndex -> + Either (EveryoneVotesCommitteeSelectionError crypto) (EveryoneVotesCommitteeMember crypto) +lookupCommitteeMember selection seatIndex + | seatIndexWithinBounds seatIndex (evStakeDistr selection) = + let candidate = getCandidateInSeat seatIndex (evStakeDistr selection) + (_, _, _, voterStake, _) = candidate + in case nonZero voterStake of + Nothing -> Left (EVZeroStake seatIndex) + Just nzStake -> + Right + EveryoneVotesCommitteeMember + { evcmCandidate = candidate + , evcmInvariant = nzStake + , evcmVoteWeight = VoteWeight (unLedgerStake voterStake) + } + | otherwise = + Left (EVInvalidSeatIndex seatIndex) + +data EveryoneVotesCert crypto = EveryoneVotesCert + { certElectionId :: ElectionId crypto + , certVoteMessage :: VoteMessage crypto + , certVoters :: BitMap SeatIndex + , groupSignature :: GroupVoteSignature crypto + } + +deriving instance + ( Eq (ElectionId crypto) + , Eq (VoteMessage crypto) + , Eq (GroupVoteSignature crypto) + ) => + Eq (EveryoneVotesCert crypto) + +deriving instance + ( Show (ElectionId crypto) + , Show (VoteMessage crypto) + , Show (GroupVoteSignature crypto) + ) => + Show (EveryoneVotesCert crypto) + +instance + (CryptoSupportsEveryoneVotes crypto, CryptoSupportsGroupVoteSigning crypto) => + VotingWithAggregation crypto (EveryoneVotesCommitteeSelection crypto) + where + type Cert (EveryoneVotesCommitteeSelection crypto) = EveryoneVotesCert crypto + + getElectionIdFromCert = certElectionId + getVoteMessageFromCert = certVoteMessage + + forgeCert :: + VotesWithSameTarget (EveryoneVotesCommitteeSelection crypto) -> + Cert (EveryoneVotesCommitteeSelection crypto) + forgeCert votes = + let certElectionId = getElectionIdFromVotes votes + certVoteMessage = getVoteMessageFromVotes votes + sortedVotes = NE.sortBy (compare `on` evSeatIndex) $ getRawVotes votes + maxIndex = maxWithDefault 0 evSeatIndex (NE.toList sortedVotes) + indices = evSeatIndex <$> NE.toList sortedVotes + certVoters = bitmapFromIndices maxIndex indices + groupSignature = sconcat $ liftVoteSignature (Proxy @crypto) . evSignature <$> sortedVotes + in EveryoneVotesCert + { certElectionId + , certVoteMessage + , certVoters + , groupSignature + } + where + maxWithDefault def f xs = foldl' (\acc x -> max acc (f x)) def xs + + verifyCert :: + EveryoneVotesCommitteeSelection crypto -> + Cert (EveryoneVotesCommitteeSelection crypto) -> + Either + (EveryoneVotesCommitteeSelectionError crypto) + (NonEmpty (EveryoneVotesCommitteeMember crypto)) + verifyCert selection EveryoneVotesCert{certElectionId, certVoteMessage, certVoters, groupSignature} = do + -- Check that the cert's election ID matches the committee selection's election ID + if certElectionId /= evcsElectionId selection + then Left EVCertElectionIdMismatch + else pure () + + let voterIndices = bitmapToIndices certVoters + + -- Look up members + sortedMembers <- + sortOn memberSeatIndex + <$> mapM (\seatIndex -> lookupCommitteeMember selection seatIndex) voterIndices + + members <- case NE.nonEmpty sortedMembers of + Nothing -> Left EVEmptyCert + Just ne -> Right ne + + -- Group signature verification + let signPubKeys = getVoteSignaturePublicKey (Proxy @crypto) . memberPubKeys <$> sortedMembers + () <- case NE.nonEmpty signPubKeys of + Just pubKeys -> + let groupPublicKey = sconcat $ liftVoteSignaturePublicKey (Proxy @crypto) <$> pubKeys + in first EVInvalidGroupVoteSignature $ + verifyGroupVoteSignature + (Proxy @crypto) + groupPublicKey + certElectionId + certVoteMessage + groupSignature + Nothing -> pure () + + Right members + where + memberPubKeys m = let (_, _, pk, _, _) = evcmCandidate m in pk + memberSeatIndex m = let (seatIndex, _, _, _, _) = evcmCandidate m in seatIndex diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/LS.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/LS.hs new file mode 100644 index 0000000000..45d55c58bf --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/LS.hs @@ -0,0 +1,168 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + +-- | Local sortition used by non-persistent members of the voting committee +module Ouroboros.Consensus.Committee.LS + ( -- * Local sortition check + LocalSortitionNumSeats (..) + , localSortitionNumSeats + ) where + +import Cardano.Ledger.BaseTypes (FixedPoint) +import Cardano.Ledger.BaseTypes.NonZero (HasZero (..)) +import Data.Maybe (fromMaybe) +import Data.Word (Word64) +import Ouroboros.Consensus.Committee.Crypto (NormalizedVRFOutput (..)) +import Ouroboros.Consensus.Committee.Types (Cumulative (..), LedgerStake (..)) +import Ouroboros.Consensus.Committee.WFA + ( NonPersistentCommitteeSize (..) + , TotalNonPersistentStake (..) + ) + +-- * Local sortition check + +-- | Number of non-persistent seats granted by local sortition to a voter +newtype LocalSortitionNumSeats = LocalSortitionNumSeats + { unLocalSortitionNumSeats :: Word64 + } + deriving stock (Show, Eq, Ord) + deriving newtype (Num, HasZero) + +-- | Compute how many non-persistent seats can be granted by local sortition to +-- a voter given their normalized VRF output and stake +localSortitionNumSeats :: + -- | Expected number of non-persistent voters in the committee + NonPersistentCommitteeSize -> + -- | Total stake of non-persistent voters + TotalNonPersistentStake -> + -- | Stake of the voter + LedgerStake -> + -- | Normalized VRF output from the participant + NormalizedVRFOutput -> + LocalSortitionNumSeats +localSortitionNumSeats + (NonPersistentCommitteeSize numNonPersistentVoters) + (TotalNonPersistentStake (Cumulative (LedgerStake totalNonPersistentStake))) + (LedgerStake voterStake) + (NormalizedVRFOutput normalizedVRFOutput) + -- None of the non-persistent voters have any stake => nobody gets a seat. + -- NOTE: this check also exists to prevent division by zero below. + | totalNonPersistentStake <= 0 = LocalSortitionNumSeats 0 + -- This voter has no stake (but some others do) => it does not get any seat. + -- NOTE: this is an optimization to avoid the expensive computation below. + | voterStake <= 0 = LocalSortitionNumSeats 0 + -- This voter might be entitled to some seats => run the local sortition. + | otherwise = LocalSortitionNumSeats (fromIntegral expectedSeats) + where + -- Expected number of seats granted by local sortition + lambda :: FixedPoint + lambda = + fromRational $ + fromIntegral numNonPersistentVoters + * voterStake + / totalNonPersistentStake + + -- Compute the "orders" of the Poisson distribution with parameter lambda, + -- which are used as thresholds to determine how many seats we get based on + -- the normalized VRF output + orders :: [FixedPoint] + orders = + (fromRational normalizedVRFOutput / lambda) + : zipWith + (\k prev -> k * prev / lambda) + [2 ..] + orders + + -- Estimate how many seats we get by comparing the normalized VRF output + -- against the thresholds defined by the orders. + -- + -- TODO(peras): evaluate whether the limit used below (3) makes sense in + -- this context. One possible starting point would be to understand why + -- @checkLeaderNatValue@ (in Ledger) also uses 3 as its own limit when + -- computing slot leadership proofs. + expectedSeats :: Int + expectedSeats = + fromMaybe 0 $ + taylorExpCmpFirstNonLower + 3 + orders + (-lambda) + +------------------------------------------------------------------------------- +-- Helpers vendored from: +-- https://github.com/cardano-scaling/leios-wfa-ls-demo/blob/7bbd846d9765191ca83b58477dc1596f64ac80fd/leios-wfa-ls-demo/lib/Cardano/Leios/NonIntegral.hs#L227 +-- +-- TODO: merge these into @Cardano.Ledger.NonIntegral@ in @cardano-ledger@ + +data Step a + = Stop + | -- Here we have `Below n err acc divisor` + Below Int a a a + +-- Returns the index of the first element that is NOT certainly BELOW. +-- It evaluates cmps left-to-right, reusing the Taylor-expansion state +-- (acc/err/divisor/n) across elements so we don't redo work. +-- +-- Behavior: +-- * If cmp_i is proven ABOVE -> return i +-- * If max iterations reached while testing cmp_i -> return i +-- * If every element is proven BELOW -> returns Nothing +-- +-- IMPORTANT: boundX must be e^{|x|} for correct error bounds (see taylorExpCmp). +taylorExpCmpFirstNonLower :: + forall a. + RealFrac a => + -- | boundX = e^{|x|} for correct error estimation + a -> + -- | list of cmp thresholds (checked in order) + [a] -> + -- | x in e^x + a -> + Maybe Int +taylorExpCmpFirstNonLower boundX cmps x = + goList 1000 0 x 1 1 0 cmps + where + -- Traverse the list of cmps, advancing the Taylor state as needed while + -- checking if the current cmp is ABOVE or BELOW. If ABOVE, return the index. + goList :: + Int -> -- maxN + Int -> -- n + a -> -- err + a -> -- acc + a -> -- divisor + Int -> -- current index + [a] -> -- remaining cmps + Maybe Int + goList _ _ _ _ _ _ [] = Nothing + goList maxN n err acc divisor i (cmp : rest) = + case decideOne maxN n err acc divisor cmp of + Stop -> + Just i + Below n' err' acc' divisor' -> + goList maxN n' err' acc' divisor' (i + 1) rest + + -- Decide current cmp by advancing the shared Taylor state as needed. + -- If BELOW is established, returns the *advanced* state to continue with. + -- If ABOVE is established or maxN reached, returns Stop. + decideOne :: + Int -> -- maxN + Int -> -- n + a -> -- err + a -> -- acc + a -> -- divisor + a -> -- cmp + Step a + decideOne maxN n err acc divisor cmp + | maxN == n = Stop + | cmp >= acc' + errorTerm = Stop + | cmp < acc' - errorTerm = Below (n + 1) err' acc' divisor' + | otherwise = decideOne maxN (n + 1) err' acc' divisor' cmp + where + divisor' = divisor + 1 + nextX = err + acc' = acc + nextX + err' = (err * x) / divisor' + errorTerm = abs (err' * boundX) diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/Types.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/Types.hs new file mode 100644 index 0000000000..ec10086f7c --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/Types.hs @@ -0,0 +1,216 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Types common to any generic committee selection scheme +module Ouroboros.Consensus.Committee.Types + ( PoolId (..) + , LedgerStake (..) + , VoteWeight (..) + , TargetCommitteeSize (..) + , Cumulative (..) + + -- * Committee selection interface + , CryptoSupportsCommitteeSelection (..) + , VotingWithCommitteeSelection (..) + , VotingWithAggregation (..) + , VotesWithSameTarget -- Hides inner fields since this is a smart constructor + , getElectionIdFromVotes + , getVoteMessageFromVotes + , getRawVotes + , ensureSameTarget + , TargetMismatch (..) + ) where + +import Cardano.Ledger.BaseTypes.NonZero (HasZero (..)) +import Cardano.Ledger.Core (KeyHash, KeyRole (..)) +import Data.List.NonEmpty (NonEmpty (..)) +import Data.Word (Word64) +import Ouroboros.Consensus.Committee.Crypto + ( CryptoSupportsVoteSigning + , ElectionId + , PrivateKeys + , VoteMessage + ) + +-- | Identifier of a given voter in the committee selection scheme +newtype PoolId = PoolId + { unPoolId :: KeyHash StakePool + } + deriving (Show, Eq, Ord) + +-- | Stake of a voter as reflected by the ledger state +newtype LedgerStake = LedgerStake + { unLedgerStake :: Rational + } + deriving (Show, Eq) + +instance HasZero LedgerStake where + isZero (LedgerStake x) = isZero x + +-- | Voting power of a voter in the committee selection scheme +newtype VoteWeight = VoteWeight + { unVoteWeight :: Rational + } + deriving (Show, Eq) + +-- | Target committee size +newtype TargetCommitteeSize = TargetCommitteeSize + { unTargetCommitteeSize :: Word64 + } + deriving (Show, Eq) + +-- | Wrapper to tag accumulated resources +newtype Cumulative a = Cumulative + { unCumulative :: a + } + deriving (Show, Eq) + +-- * Committee selection interface + +-- | Interface for committee selection schemes. +-- +-- This class is parametrized by the crypto primitives and the committee +-- selection data structure. Instances define how to check whether a party +-- should vote and how to compute the voting weight of a committee member. +class + (CryptoSupportsVoteSigning crypto, CryptoOf csContext ~ crypto) => + CryptoSupportsCommitteeSelection crypto csContext + | csContext -> crypto + where + type CryptoOf csContext + + type CommitteeSelectionError csContext + type CommitteeMember csContext + + -- | Check whether we should vote in a given election. + checkShouldVote :: + csContext -> + PoolId -> + PrivateKeys crypto -> + ElectionId crypto -> + Either (CommitteeSelectionError csContext) (Maybe (CommitteeMember csContext)) + + committeeMemberWeight :: + CommitteeMember csContext -> + VoteWeight + +-- | Interface for votes that can be validated under a committee selection scheme. +class + ( CryptoSupportsCommitteeSelection crypto csContext + , Eq (Vote csContext) + , Show (Vote csContext) + ) => + VotingWithCommitteeSelection crypto csContext + | csContext -> crypto + where + type Vote csContext + + forgeVote :: + CommitteeMember csContext -> + PrivateKeys crypto -> + ElectionId crypto -> + VoteMessage crypto -> + Vote csContext + + -- | Check the validity of a vote in a given election. + verifyVote :: + csContext -> + Vote csContext -> + Either (CommitteeSelectionError csContext) (CommitteeMember csContext) + + getElectionIdFromVote :: Vote csContext -> ElectionId crypto + getVoteMessageFromVote :: Vote csContext -> VoteMessage crypto + +data VotesWithSameTarget csContext = VotesWithSameTarget + { vwstElectionId :: ElectionId (CryptoOf csContext) + , vwstVoteMessage :: VoteMessage (CryptoOf csContext) + , vwstVotes :: NonEmpty (Vote csContext) + } + +getElectionIdFromVotes :: VotesWithSameTarget csContext -> ElectionId (CryptoOf csContext) +getElectionIdFromVotes = vwstElectionId + +getVoteMessageFromVotes :: VotesWithSameTarget csContext -> VoteMessage (CryptoOf csContext) +getVoteMessageFromVotes = vwstVoteMessage + +getRawVotes :: VotesWithSameTarget csContext -> NonEmpty (Vote csContext) +getRawVotes = vwstVotes + +deriving instance + ( Eq (ElectionId (CryptoOf csContext)) + , Eq (VoteMessage (CryptoOf csContext)) + , Eq (Vote csContext) + ) => + Eq (VotesWithSameTarget csContext) + +deriving instance + ( Show (ElectionId (CryptoOf csContext)) + , Show (VoteMessage (CryptoOf csContext)) + , Show (Vote csContext) + ) => + Show (VotesWithSameTarget csContext) + +-- | Mismatch between the target (election ID or vote message) of votes. +data TargetMismatch crypto + = -- | Two votes have different election IDs + ElectionIdMismatch (ElectionId crypto) (ElectionId crypto) + | -- | Two votes have different vote messages + VoteMessageMismatch (VoteMessage crypto) (VoteMessage crypto) + +deriving instance + (Eq (ElectionId crypto), Eq (VoteMessage crypto)) => Eq (TargetMismatch crypto) + +deriving instance + (Show (ElectionId crypto), Show (VoteMessage crypto)) => Show (TargetMismatch crypto) + +ensureSameTarget :: + forall crypto csContext. + VotingWithCommitteeSelection crypto csContext => + NonEmpty (Vote csContext) -> + Either (TargetMismatch crypto) (VotesWithSameTarget csContext) +ensureSameTarget votes@(v :| vs) = + let eId0 = getElectionIdFromVote @crypto @csContext v + msg0 = getVoteMessageFromVote @crypto @csContext v + in go (1 :: Int) eId0 msg0 vs + where + go _ eId msg [] = Right (VotesWithSameTarget eId msg votes) + go n eId1 msg1 (y : ys) = + let eId2 = getElectionIdFromVote @crypto @csContext y + msg2 = getVoteMessageFromVote @crypto @csContext y + in if eId1 /= eId2 + then Left $ ElectionIdMismatch eId1 eId2 + else + if msg1 /= msg2 + then Left $ VoteMessageMismatch msg1 msg2 + else go (n + 1) eId1 msg1 ys + +-- | Interface for aggregating votes into certificates. +class + ( VotingWithCommitteeSelection crypto csContext + , Eq (Cert csContext) + , Show (Cert csContext) + ) => + VotingWithAggregation crypto csContext + | csContext -> crypto + where + type Cert csContext + + getElectionIdFromCert :: Cert csContext -> ElectionId crypto + getVoteMessageFromCert :: Cert csContext -> VoteMessage crypto + + forgeCert :: + VotesWithSameTarget csContext -> + Cert csContext + + verifyCert :: + csContext -> + Cert csContext -> + Either (CommitteeSelectionError csContext) (NonEmpty (CommitteeMember csContext)) diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/WFA.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/WFA.hs new file mode 100644 index 0000000000..aa6ec3dc35 --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/WFA.hs @@ -0,0 +1,362 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- | Deterministic portion of the Weighted Fait-Accompli committee selection scheme +module Ouroboros.Consensus.Committee.WFA + ( -- * Weighted Fait-Accompli committee selection scheme + PersistentCommitteeSize (..) + , NonPersistentCommitteeSize (..) + , TotalPersistentStake (..) + , TotalNonPersistentStake (..) + , weightedFaitAccompliSplitSeats + , isAbovePersistentSeatThreshold + + -- * Cumulative stake distributions + , SeatIndex (..) + , NumPoolsWithPositiveStake (..) + , WFAError (..) + , ExtWFAStakeDistr (..) + , mkExtWFAStakeDistr + , getCandidateInSeat + , seatIndexWithinBounds + , Candidate + ) where + +import Control.Exception (assert) +import Data.Array (Array, Ix, listArray) +import qualified Data.Array as Array +import qualified Data.List as List +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Word (Word64) +import Ouroboros.Consensus.Committee.Types + ( Cumulative (..) + , LedgerStake (..) + , PoolId + , TargetCommitteeSize (..) + ) + +-- * Weighted Fait-Accompli committee selection scheme + +-- | Persistent committee size +newtype PersistentCommitteeSize = PersistentCommitteeSize + { unPersistentCommitteeSize :: Word64 + } + deriving (Show, Eq) + +-- | Non-persistent committee size +newtype NonPersistentCommitteeSize = NonPersistentCommitteeSize + { unNonPersistentCommitteeSize :: Word64 + } + deriving (Show, Eq) + +-- | Total persistent stake +newtype TotalPersistentStake = TotalPersistentStake + { unTotalPersistentStake :: Cumulative LedgerStake + } + deriving (Show, Eq) + +-- | Total non-persistent stake +newtype TotalNonPersistentStake = TotalNonPersistentStake + { unTotalNonPersistentStake :: Cumulative LedgerStake + } + deriving (Show, Eq) + +-- | Errors that can occur when trying to split the stake distribution into +-- persistent and seats via weighted Fait-Accompli. +data WFAError + = -- | The underlying stake distribution is empty + EmptyStakeDistribution + | -- | The target committee size is larger than the number of pools with positive + -- stake in the underlying stake distribution, which would lead to incorrect + -- results (e.g. granting persistent seats to voters with zero stake). + NotEnoughPoolsWithPositiveStake + TargetCommitteeSize + NumPoolsWithPositiveStake + deriving (Show, Eq) + +-- | Split a stake distrubution into persistent and non-persistent committee +-- seats according to the weighted Fait-Accompli scheme. +-- +-- This function returns: +-- * number of persistent seats granted via the weighted Fait-Accompli scheme +-- * number of non-persistent seats expected to vote via local sortition +-- * total persistent stake +-- * total non-persistent stake +weightedFaitAccompliSplitSeats :: + -- | Extended cumulative stake distribution of the potential voters + ExtWFAStakeDistr c -> + -- | Expected total committee size (persistent + non-persistent) + TargetCommitteeSize -> + Either + WFAError + ( PersistentCommitteeSize + , NonPersistentCommitteeSize + , TotalPersistentStake + , TotalNonPersistentStake + ) +weightedFaitAccompliSplitSeats extWFAStakeDistr totalSeats + -- The target committee size must not be not larger than the actual number of + -- pools with positive stake in the underlying stake distribution. Otherwise, + -- it could lead to incorrect/non-desirable results (e.g., granting persistent + -- seats to voters with zero stake). + | notEnoughPoolsWithPositiveStake = + Left + ( NotEnoughPoolsWithPositiveStake + totalSeats + (numPoolsWithPositiveStake extWFAStakeDistr) + ) + | otherwise = + -- We should have /at most/ as many persistent voters as the total + -- committee size, but not more. + assert (numPersistentVoters <= unTargetCommitteeSize totalSeats) $ + Right + ( PersistentCommitteeSize numPersistentVoters + , NonPersistentCommitteeSize numNonPersistentVoters + , TotalPersistentStake (Cumulative (LedgerStake persistentStake)) + , TotalNonPersistentStake (Cumulative (LedgerStake nonPersistentStake)) + ) + where + notEnoughPoolsWithPositiveStake = + unNumPoolsWithPositiveStake (numPoolsWithPositiveStake extWFAStakeDistr) + < unTargetCommitteeSize totalSeats + + stakeDistrArray = + unExtWFAStakeDistr extWFAStakeDistr + + ( numPersistentVoters + , persistentStake + , nonPersistentStake + ) = + traverseSeats (Array.bounds stakeDistrArray) True 0 0 0 + + numNonPersistentVoters = + unTargetCommitteeSize totalSeats + - numPersistentVoters + + traverseSeats + (currSeatIndex, lastSeatIndex) + checkPersistentSeatThreshold + accNumPersistentVoters + accPersistentStake + accNonPersistentStake + -- Reached the end + | currSeatIndex > lastSeatIndex = + ( accNumPersistentVoters + , accPersistentStake + , accNonPersistentStake + ) + -- The current voter is persistent + | isPersistent = + traverseSeats + (succ currSeatIndex, lastSeatIndex) + True + (accNumPersistentVoters + 1) + (accPersistentStake + voterStake) + accNonPersistentStake + -- The current voter is non-persistent + | otherwise = + traverseSeats + (succ currSeatIndex, lastSeatIndex) + False + accNumPersistentVoters + accPersistentStake + (accNonPersistentStake + voterStake) + where + -- Extract the entry in the array corresponding to the current seat index + (_, _, LedgerStake voterStake, cumulativeStake) = + (Array.!) stakeDistrArray currSeatIndex + + -- Check whether the current voter can be granted a persistent seat + isPersistent = + -- NOTE: because the check should behave monotonically, we can skip it + -- entirely after the first non-persistent voter is found. + checkPersistentSeatThreshold + && isAbovePersistentSeatThreshold + totalSeats + currSeatIndex + (LedgerStake voterStake) + cumulativeStake + +-- | Evaluate whether a voter with its give stake and relatile position in the +-- stake distribution can be granted a persistent seat in the voting committee. +isAbovePersistentSeatThreshold :: + -- | Total committee size (persistent + non-persistent) + TargetCommitteeSize -> + -- | Current voter seat index + SeatIndex -> + -- | Current voter stake + LedgerStake -> + -- | Cumulated stake of voters with smaller or equal stake than the current one + Cumulative LedgerStake -> + -- | Whether the current voter has a persistent seat or not + Bool +isAbovePersistentSeatThreshold + (TargetCommitteeSize totalSeats) + (SeatIndex voterSeat) + (LedgerStake voterStake) + (Cumulative (LedgerStake cumulativeStake)) + | cumulativeStake <= 0 = + False -- Avoid division by zero in the left-hand side of the inequality + | voterSeat >= totalSeats = + False -- Avoid underflow in the right-hand side of the inequality + | otherwise = + ( (1 - (voterStake / cumulativeStake)) + ^ (2 :: Integer) + ) + < ( toRational (totalSeats - voterSeat - 1) + / toRational (totalSeats - voterSeat) + ) + +-- * Cumulative stake distributions + +-- | Seat index in the voting committee +newtype SeatIndex = SeatIndex + { unSeatIndex :: Word64 + } + deriving (Show, Eq, Ord, Num, Real, Enum, Ix, Integral) + +-- | Number of pools with positive stake in the underlying stake distribution +newtype NumPoolsWithPositiveStake = NumPoolsWithPositiveStake + { unNumPoolsWithPositiveStake :: Word64 + } + deriving (Show, Eq) + +-- | Extended cumulative stake distribution. +-- +-- Stake distribution in descending order with precomputed right-cumulative +-- stake, i.e., the total stake of voters with smaller or equal stake than the +-- current one (including the current one itself). In addition, this wrapper +-- also allows the inclusion of an arbitrary payload of type @a@. This is useful +-- to keep track of anything else we might need to know about the voters in the +-- committee selection scheme (e.g. their public keys) in a single place. +-- +-- E.g.: given the following stake distribution: +-- +-- @ +-- PoolId 1 -> (50, PK#1) +-- PoolId 2 -> (15, PK#2) +-- PoolId 3 -> (10, PK#3) +-- PoolId 4 -> (20, PK#4) +-- PoolId 5 -> (5, PK#5) +-- @ +-- +-- We would have the following cumulative stake distribution: +-- +-- @ +-- Array.listArray +-- (SeatIndex 0, SeatIndex 4) +-- [ (PoolId 1, PK#1, LedgerStake 50, CumulativeStake 100) +-- , (PoolId 4, PK#4, LedgerStake 20, CumulativeStake 50) +-- , (PoolId 2, PK#2, LedgerStake 15, CumulativeStake 30) +-- , (PoolId 3, PK#3, LedgerStake 10, CumulativeStake 15) +-- , (PoolId 5, PK#5, LedgerStake 5, CumulativeStake 5) +-- ] +-- @ +-- +-- NOTE: this wrapper exists to allow us to share the same cumulative stake +-- distribution across multiple committee selection instances derived from the +-- same underlying stake distribution (e.g. Leios and Peras voting committees +-- for the same epoch). +data ExtWFAStakeDistr a = ExtWFAStakeDistr + { unExtWFAStakeDistr :: + Array + SeatIndex + ( PoolId -- Voter ID of this voter + , a -- Extra payload associated to this voter + , LedgerStake -- Ledger stake of this voter + , Cumulative LedgerStake -- Right-cumulative ledger stake of this voter + ) + , numPoolsWithPositiveStake :: NumPoolsWithPositiveStake + -- ^ Number of pools with positive stake in the underlying stake distribution. + -- This is also precomputed at the beginning of the epoch to prevent invalid + -- weighted Fait-Accompli instantiations with a target committee size larger + -- than the number of pools with positive stake, which would lead to incorrect + -- results (e.g. granting persistent seats to voters with zero stake). + } + deriving (Eq, Show) + +-- | Construct an extended cumulative stake distribution. +-- +-- Returns an error if the underlying stake distribution is empty. +mkExtWFAStakeDistr :: + Map PoolId (LedgerStake, a) -> + Either WFAError (ExtWFAStakeDistr a) +mkExtWFAStakeDistr pools + | Map.null pools = + Left + EmptyStakeDistribution + | otherwise = + Right + ExtWFAStakeDistr + { unExtWFAStakeDistr = stakeDistrArray + , numPoolsWithPositiveStake = numPoolsWithPositiveStakeAcc + } + where + stakeDistrArray = + listArray + ( SeatIndex 0 + , SeatIndex (fromIntegral (Map.size pools) - 1) + ) + cumulativeStakeAndPools + + ((_totalStake, numPoolsWithPositiveStakeAcc), cumulativeStakeAndPools) = + List.mapAccumR + accumStakeAndCountPoolsWithPositiveStake + ( Cumulative (LedgerStake 0) + , NumPoolsWithPositiveStake 0 + ) + . List.sortBy descendingStake + . Map.toList + $ pools + + descendingStake + (_, (LedgerStake x, _)) + (_, (LedgerStake y, _)) = + compare y x + + accumStakeAndCountPoolsWithPositiveStake + (Cumulative (LedgerStake stakeAccR), NumPoolsWithPositiveStake numPoolsAccR) + (poolId, (LedgerStake poolStake, poolPublicKey)) = + let stakeAccR' = + stakeAccR + poolStake + numPoolsAccR' + | poolStake > 0 = numPoolsAccR + 1 + | otherwise = numPoolsAccR + in ( + ( Cumulative (LedgerStake stakeAccR') + , NumPoolsWithPositiveStake numPoolsAccR' + ) + , + ( poolId + , poolPublicKey + , LedgerStake poolStake + , Cumulative (LedgerStake stakeAccR') + ) + ) + +type Candidate a = (SeatIndex, PoolId, a, LedgerStake, Cumulative LedgerStake) + +-- | Retrieve the candidate information associated to a given seat index. +-- +-- PRECONDITION: the seat index must be within bounds in the stake distribution +getCandidateInSeat :: + SeatIndex -> + ExtWFAStakeDistr a -> + Candidate a +getCandidateInSeat seatIndex distr = + let (poolId, payload, stake, cumStake) = (Array.!) (unExtWFAStakeDistr distr) seatIndex + in (seatIndex, poolId, payload, stake, cumStake) + +-- | Check that a seat index is within bounds in a stake distribution +seatIndexWithinBounds :: + SeatIndex -> + ExtWFAStakeDistr a -> + Bool +seatIndexWithinBounds seatIndex distr = + unSeatIndex seatIndex >= unSeatIndex lowerBound + && unSeatIndex seatIndex <= unSeatIndex upperBound + where + (lowerBound, upperBound) = + Array.bounds $ unExtWFAStakeDistr distr diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/WFALS.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/WFALS.hs new file mode 100644 index 0000000000..3b5a4b4886 --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Committee/WFALS.hs @@ -0,0 +1,651 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE StandaloneKindSignatures #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Weighted Fait-Accompli with Local Sortition (wFA^LS) committee selection. +-- +-- This module implements a generic committee selection scheme based the on +-- Weighted Fait-Accompli with Local Sortition (wFA^LS) algorithm +-- from the paper: +-- +-- Peter Gaži, Aggelos Kiayias, and Alexander Russell. 2023. Fait Accompli +-- Committee Selection: Improving the Size-Security Tradeoff of Stake-Based +-- Committees. In Proceedings of the 2023 ACM SIGSAC Conference on Computer and +-- Communications Security (CCS '23). Association for Computing Machinery, New +-- York, NY, USA, 845–858. https://doi.org/10.1145/3576915.3623194 +-- +-- PDF: https://eprint.iacr.org/2023/1273.pdf +-- +-- For this, we combine the deterministic portion of the weighted Fait-Accompli +-- scheme (defined in @Ouroboros.Consensus.Committee.WFA@) with local sortition +-- (defined in @Ouroboros.Consensus.Committee.LS@) as a fallback scheme. +-- +-- NOTE: this module is meant to be imported qualified. +module Ouroboros.Consensus.Committee.WFALS + ( -- * Voting committee membership + WFALSCommitteeMember (..) + , WFALSVote (..) + + -- * Committee membership interface + , CryptoSupportsWFALS + , WFALSCommitteeSelection (..) + , mkWFALSCommitteeSelection + , WFALSCommitteeSelectionError (..) + ) where + +import Cardano.Ledger.BaseTypes (Nonce) +import Cardano.Ledger.BaseTypes.NonZero (NonZero, nonZero) +import Control.Exception (assert) +import Control.Monad (void) +import qualified Data.Array as Array +import Data.Bifunctor (Bifunctor (..)) +import Data.Either (partitionEithers) +import Data.Foldable (Foldable (foldl')) +import Data.Kind (Type) +import Data.List (sortOn) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as NE +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Proxy (Proxy (..)) +import Data.Semigroup (Semigroup (..)) +import Ouroboros.Consensus.Committee.BitMap (BitMap, bitmapFromIndices, bitmapToIndices) +import Ouroboros.Consensus.Committee.Crypto + ( CryptoSupportsGroupVRF (..) + , CryptoSupportsGroupVoteSigning (..) + , CryptoSupportsNaiveGroupVRF + , CryptoSupportsVRF (..) + , CryptoSupportsVoteSigning (..) + , ElectionId + , PrivateKeys + , PublicKeys + , VRFPoolContext (..) + , VoteMessage + ) +import Ouroboros.Consensus.Committee.LS + ( LocalSortitionNumSeats (..) + , localSortitionNumSeats + ) +import Ouroboros.Consensus.Committee.Types + ( CryptoSupportsCommitteeSelection (..) + , Cumulative (..) + , LedgerStake (..) + , PoolId + , TargetCommitteeSize (..) + , VoteWeight (..) + , VotesWithSameTarget + , VotingWithAggregation (..) + , VotingWithCommitteeSelection (..) + , getElectionIdFromVotes + , getRawVotes + , getVoteMessageFromVotes + ) +import Ouroboros.Consensus.Committee.WFA + ( Candidate + , ExtWFAStakeDistr (..) + , NonPersistentCommitteeSize + , PersistentCommitteeSize (..) + , SeatIndex (..) + , TotalNonPersistentStake (..) + , TotalPersistentStake + , WFAError + , getCandidateInSeat + , seatIndexWithinBounds + , weightedFaitAccompliSplitSeats + ) + +-- * Committee membership interface + +-- | View of a committee selection vote. +-- +-- This is a projection of a vote containing only the information needed to +-- validate it against a given committee selection. +type WFALSVote :: Type -> Type +data WFALSVote crypto where + PersistentVote :: + SeatIndex -> + ElectionId crypto -> + VoteMessage crypto -> + VoteSignature crypto -> + WFALSVote crypto + NonPersistentVote :: + SeatIndex -> + ElectionId crypto -> + VoteMessage crypto -> + VRFOutput crypto -> + VoteSignature crypto -> + WFALSVote crypto + +deriving instance + ( Eq (ElectionId crypto) + , Eq (VoteMessage crypto) + , Eq (VoteSignature crypto) + , Eq (VRFOutput crypto) + ) => + Eq (WFALSVote crypto) + +deriving instance + ( Show (ElectionId crypto) + , Show (VoteMessage crypto) + , Show (VoteSignature crypto) + , Show (VRFOutput crypto) + ) => + Show (WFALSVote crypto) + +-- * Voting committee membership + +class + ( CryptoSupportsVoteSigning crypto + , CryptoSupportsVRF crypto + , Eq (VoteSignature crypto) + , Show (VoteSignature crypto) + , Eq (VRFOutput crypto) + , Show (VRFOutput crypto) + ) => + CryptoSupportsWFALS crypto + +data WFALSCommitteeSelection crypto + = WFALSCommitteeSelection + { csElectionId :: !(ElectionId crypto) + -- ^ Election ID for this committee selection + , wfaStakeDistr :: !(ExtWFAStakeDistr (PublicKeys crypto)) + -- ^ Preaccumulated stake distrubution used to compute committee composition + , candidateSeats :: !(Map PoolId SeatIndex) + -- ^ Index of a given candidate in the cumulative stake distribution + , persistentCommitteeSize :: !PersistentCommitteeSize + -- ^ Number of persistent seats granted by the weighted Fait-Accompli scheme + , nonPersistentCommitteeSize :: !NonPersistentCommitteeSize + -- ^ Expected number of non-persistent voters + , totalPersistentStake :: !TotalPersistentStake + -- ^ Total stake of persistent voters + , totalNonPersistentStake :: !TotalNonPersistentStake + -- ^ Total stake of non-persistent voters + , epochNonce :: !Nonce + -- ^ Epoch nonce of the epoch where this committee selection takes place + } + +deriving instance + (Eq (PublicKeys crypto), Eq (ElectionId crypto)) => Eq (WFALSCommitteeSelection crypto) + +deriving instance + (Show (PublicKeys crypto), Show (ElectionId crypto)) => Show (WFALSCommitteeSelection crypto) + +data WFALSCommitteeSelectionError crypto + = -- | A voter ID is missing from the committee selection + MissingPoolId PoolId + | -- | A voter claims to be a persistent member of the committee, but it's not + NotAPersistentMember SeatIndex + | -- | A voter claims to be a non-persistent member of the committee, but it's not + NotANonPersistentMember SeatIndex + | -- | A non-persistent voter was assigned zero seats by local sortition + NonPersistentMemberWithZeroSeats SeatIndex + | -- | The seat index is out of bounds + InvalidSeatIndex SeatIndex + | -- | The VRF evaluation for local sortition failed (e.g. due to invalid proof) + LocalSortitionError String + | -- | The vote signature is invalid + InvalidVoteSignature String + | -- | The cert's election ID does not match the committee selection's election ID + CertElectionIdMismatch + | -- | The cert contains no votes + EmptyCert + | -- | The group VRF verification failed + InvalidGroupVRF String + | -- | The group vote signature verification failed + InvalidGroupVoteSignature String + deriving (Show, Eq) + +data WFALSCommitteeMember crypto + = -- | A persistent member of the voting committee + PersistentCommitteeMember + (Candidate (PublicKeys crypto)) + VoteWeight + | -- | A (realized) non-persistent member of the voting committee. + NonPersistentCommitteeMember + (Candidate (PublicKeys crypto)) + (VRFOutput crypto) + (NonZero LocalSortitionNumSeats) + VoteWeight + +deriving instance + (Show (VRFOutput crypto), Show (PublicKeys crypto)) => Show (WFALSCommitteeMember crypto) + +deriving instance + (Eq (VRFOutput crypto), Eq (PublicKeys crypto)) => Eq (WFALSCommitteeMember crypto) + +committeeMemberCandidate :: WFALSCommitteeMember crypto -> Candidate (PublicKeys crypto) +committeeMemberCandidate = \case + PersistentCommitteeMember candidate _ -> candidate + NonPersistentCommitteeMember candidate _ _ _ -> candidate + +instance + CryptoSupportsWFALS crypto => + CryptoSupportsCommitteeSelection crypto (WFALSCommitteeSelection crypto) + where + type CryptoOf (WFALSCommitteeSelection crypto) = crypto + type CommitteeSelectionError (WFALSCommitteeSelection crypto) = WFALSCommitteeSelectionError crypto + type CommitteeMember (WFALSCommitteeSelection crypto) = WFALSCommitteeMember crypto + + checkShouldVote :: + WFALSCommitteeSelection crypto -> + PoolId -> + PrivateKeys crypto -> + ElectionId crypto -> + Either + (WFALSCommitteeSelectionError crypto) + (Maybe (WFALSCommitteeMember crypto)) + checkShouldVote selection ourId ourPrivateKeys electionId + | Just seatIndex <- Map.lookup ourId (candidateSeats selection) = + case lookupPersistentCommitteeMember selection seatIndex of + Right member -> pure (Just member) + Left (NotAPersistentMember _) -> do + let ourVRFSigningKey = + getVRFSigningKey (Proxy @crypto) ourPrivateKeys + vrfContext = + VRFSignContext ourVRFSigningKey + vrfOutput <- checkVRFOutput vrfContext electionId selection + case lookupNonPersistentCommitteeMember selection seatIndex vrfOutput of + -- For checkShouldVote, we shouldn't error out if local sortition gives us zero seats. Instead, we should just return Nothing to indicate that we're not a member of the committee. + Left (NonPersistentMemberWithZeroSeats _) -> pure Nothing + Right res -> pure (Just res) + Left err -> Left err + Left err -> Left err + | otherwise = + Left (MissingPoolId ourId) + + committeeMemberWeight :: + WFALSCommitteeMember crypto -> + VoteWeight + committeeMemberWeight = \case + PersistentCommitteeMember _ weight -> weight + NonPersistentCommitteeMember _ _ _ weight -> weight + +instance + CryptoSupportsWFALS crypto => + VotingWithCommitteeSelection crypto (WFALSCommitteeSelection crypto) + where + type Vote (WFALSCommitteeSelection crypto) = WFALSVote crypto + + forgeVote :: + WFALSCommitteeMember crypto -> + PrivateKeys crypto -> + ElectionId crypto -> + VoteMessage crypto -> + WFALSVote crypto + forgeVote member ourPrivateKeys electionId message = + let sig = signVote (getVoteSignaturePrivateKey (Proxy @crypto) ourPrivateKeys) electionId message + in case member of + PersistentCommitteeMember + candidate + _weight -> + let (seatIndex, _, _, _, _) = candidate + in PersistentVote seatIndex electionId message sig + NonPersistentCommitteeMember + candidate + vrfOutput + _numSeats + _weight -> + let (seatIndex, _, _, _, _) = candidate + in NonPersistentVote seatIndex electionId message vrfOutput sig + + verifyVote :: + WFALSCommitteeSelection crypto -> + WFALSVote crypto -> + Either + (WFALSCommitteeSelectionError crypto) + (WFALSCommitteeMember crypto) + verifyVote selection = + \case + PersistentVote seatIndex electionId message sig -> do + member <- lookupPersistentCommitteeMember selection seatIndex + let (_, _, voterPublicKeys, _, _) = committeeMemberCandidate member + voterSignaturePublicKeys = + getVoteSignaturePublicKey (Proxy @crypto) voterPublicKeys + checkVoteSignature voterSignaturePublicKeys electionId message sig + pure member + NonPersistentVote seatIndex electionId message vrfOutput sig -> do + member <- lookupNonPersistentCommitteeMember selection seatIndex vrfOutput + let (_, _, voterPublicKeys, _, _) = committeeMemberCandidate member + voterSignaturePublicKeys = + getVoteSignaturePublicKey (Proxy @crypto) voterPublicKeys + let voterVRFVerifyKey = + getVRFVerifyKey (Proxy @crypto) voterPublicKeys + let vrfContext = + VRFVerifyContext voterVRFVerifyKey vrfOutput + checkVoteSignature voterSignaturePublicKeys electionId message sig + void $ checkVRFOutput vrfContext electionId selection + pure member + + getElectionIdFromVote = \case + PersistentVote _ electionId _ _ -> electionId + NonPersistentVote _ electionId _ _ _ -> electionId + + getVoteMessageFromVote = \case + PersistentVote _ _ message _ -> message + NonPersistentVote _ _ message _ _ -> message + +-- | Construct a 'CommitteeSelection' for a given epoch +mkWFALSCommitteeSelection :: + -- | Epoch nonce + Nonce -> + -- | Election ID + ElectionId crypto -> + -- | Expected committee size + TargetCommitteeSize -> + -- | Extended cumulative stake distribution of the potential voters + ExtWFAStakeDistr (PublicKeys crypto) -> + Either WFAError (WFALSCommitteeSelection crypto) +mkWFALSCommitteeSelection nonce electionId totalSeats stakeDistr = do + ( numPersistentVoters + , numNonPersistentVoters + , persistentStake + , nonPersistentStake + ) <- + weightedFaitAccompliSplitSeats stakeDistr totalSeats + + let seats = + Map.fromList + [ (poolId, seatIndex) + | (seatIndex, (poolId, _, _, _)) <- + Array.assocs (unExtWFAStakeDistr stakeDistr) + ] + + pure $ + WFALSCommitteeSelection + { csElectionId = electionId + , wfaStakeDistr = stakeDistr + , candidateSeats = seats + , persistentCommitteeSize = numPersistentVoters + , nonPersistentCommitteeSize = numNonPersistentVoters + , totalPersistentStake = persistentStake + , totalNonPersistentStake = nonPersistentStake + , epochNonce = nonce + } + +-- | Check if a voter is a persistent member of a committee +isPersistentMember :: + SeatIndex -> + WFALSCommitteeSelection crypto -> + Bool +isPersistentMember seatIndex selection = + unSeatIndex seatIndex + < unPersistentCommitteeSize (persistentCommitteeSize selection) + +-- | Look up a persistent committee member by seat index. +-- +-- Checks that the seat index is within bounds and that the voter is a +-- persistent member of the committee. Does NOT verify signatures. +lookupPersistentCommitteeMember :: + WFALSCommitteeSelection crypto -> + SeatIndex -> + Either (WFALSCommitteeSelectionError crypto) (WFALSCommitteeMember crypto) +lookupPersistentCommitteeMember selection seatIndex + | not (seatIndexWithinBounds seatIndex (wfaStakeDistr selection)) = + Left (InvalidSeatIndex seatIndex) + | isPersistentMember seatIndex selection = + let candidate = getCandidateInSeat seatIndex (wfaStakeDistr selection) + (_, _, _, LedgerStake stakeVal, _) = candidate + in Right $ PersistentCommitteeMember candidate (VoteWeight stakeVal) + | otherwise = + Left (NotAPersistentMember seatIndex) + +-- Checks that the seat index is within bounds and that the voter is a +-- non-persistent member of the committee based on the presented VRFOutput. +-- +-- Returns 'NonPersistentMemberWithZeroSeats' if local sortition assigns zero seats. +-- Does NOT verify signatures or VRF +-- output, so the user must ensure that the provided VRF output is correct +-- outside of this function. +lookupNonPersistentCommitteeMember :: + CryptoSupportsVRF crypto => + WFALSCommitteeSelection crypto -> + SeatIndex -> + VRFOutput crypto -> + Either (WFALSCommitteeSelectionError crypto) (WFALSCommitteeMember crypto) +lookupNonPersistentCommitteeMember selection seatIndex vrfOutput + | not (seatIndexWithinBounds seatIndex (wfaStakeDistr selection)) = + Left (InvalidSeatIndex seatIndex) + | not (isPersistentMember seatIndex selection) = + let candidate = getCandidateInSeat seatIndex (wfaStakeDistr selection) + (_, _, _, voterStake, _) = candidate + numSeats = + localSortitionNumSeats + (nonPersistentCommitteeSize selection) + (totalNonPersistentStake selection) + voterStake + (normalizeVRFOutput vrfOutput) + in case nonZero numSeats of + Nothing -> Left (NonPersistentMemberWithZeroSeats seatIndex) + Just nzNumSeats -> + let LedgerStake voterStakeVal = voterStake + TotalNonPersistentStake (Cumulative (LedgerStake nonPersistentStake)) = + totalNonPersistentStake selection + voterWeight = + VoteWeight $ + fromIntegral (unLocalSortitionNumSeats numSeats) + * voterStakeVal + / nonPersistentStake + in Right $ + NonPersistentCommitteeMember + candidate + vrfOutput + nzNumSeats + voterWeight + | otherwise = + Left (NotANonPersistentMember seatIndex) + +-- | Check the validity of a vote signature +checkVoteSignature :: + forall crypto. + CryptoSupportsVoteSigning crypto => + VoteSignaturePublicKey crypto -> + ElectionId crypto -> + VoteMessage crypto -> + VoteSignature crypto -> + Either (CommitteeSelectionError (WFALSCommitteeSelection crypto)) () +checkVoteSignature voterPublicKey electionId message sig = + first InvalidVoteSignature $ do + verifyVoteSignature + voterPublicKey + electionId + message + sig + +-- | Get the VRF output associated to a given context +checkVRFOutput :: + forall crypto. + CryptoSupportsVRF crypto => + VRFPoolContext crypto -> + ElectionId crypto -> + WFALSCommitteeSelection crypto -> + Either (CommitteeSelectionError (WFALSCommitteeSelection crypto)) (VRFOutput crypto) +checkVRFOutput context electionId selection = + first LocalSortitionError $ do + evalVRF + context + ( mkVRFElectionInput + @crypto + (epochNonce selection) + electionId + ) + +partitionVotes :: [WFALSVote crypto] -> ([WFALSVote crypto], [WFALSVote crypto]) +partitionVotes = + partitionEithers + . fmap + ( \case + v@PersistentVote{} -> Left v + v@NonPersistentVote{} -> Right v + ) + +data WFALSCert crypto = WFALSCert + { certElectionId :: ElectionId crypto + , certVoteMessage :: VoteMessage crypto + , persistentVoters :: BitMap SeatIndex + , nonPersistentVotersToEligibility :: Map SeatIndex (VRFOutput crypto) + , groupSignature :: GroupVoteSignature crypto + } + +deriving instance + ( Eq (ElectionId crypto) + , Eq (VoteMessage crypto) + , Eq (VRFOutput crypto) + , Eq (GroupVoteSignature crypto) + ) => + Eq (WFALSCert crypto) + +deriving instance + ( Show (ElectionId crypto) + , Show (VoteMessage crypto) + , Show (VRFOutput crypto) + , Show (GroupVoteSignature crypto) + ) => + Show (WFALSCert crypto) + +instance + (CryptoSupportsWFALS crypto, CryptoSupportsGroupVoteSigning crypto, CryptoSupportsGroupVRF crypto) => + VotingWithAggregation crypto (WFALSCommitteeSelection crypto) + where + type Cert (WFALSCommitteeSelection crypto) = WFALSCert crypto + + getElectionIdFromCert = certElectionId + getVoteMessageFromCert = certVoteMessage + + forgeCert :: + VotesWithSameTarget (WFALSCommitteeSelection crypto) -> + Cert (WFALSCommitteeSelection crypto) + forgeCert votes = + let certElectionId = getElectionIdFromVotes votes + certVoteMessage = getVoteMessageFromVotes votes + (pvs, npvs) = partitionVotes (NE.toList $ getRawVotes votes) + sortedPvs = sortOn voteSeatIndex pvs + sortedNpvs = sortOn voteSeatIndex npvs + maxPvIndex = maxWithDefault 0 voteSeatIndex sortedPvs + pvIndices = voteSeatIndex <$> sortedPvs + persistentVoters = bitmapFromIndices maxPvIndex pvIndices + nonPersistentVotersToEligibility = Map.fromList $ npvIdAndVRFOutput <$> sortedNpvs + orderedVotes = + -- ensure deterministic ordering for group signature + let allVotes = sortedPvs ++ sortedNpvs + in assert (sortOn voteSeatIndex allVotes == allVotes) $ + case NE.nonEmpty allVotes of + Just ne -> ne + Nothing -> + error "We've just re-ordered a `NonEmpty` list of votes, so the result should still be `NonEmpty`" + groupSignature = sconcat $ liftVoteSignature (Proxy @crypto) . voterSignature <$> orderedVotes + in WFALSCert + { certElectionId + , certVoteMessage + , persistentVoters + , nonPersistentVotersToEligibility + , groupSignature + } + where + voterSignature (PersistentVote _ _ _ sig) = sig + voterSignature (NonPersistentVote _ _ _ _ sig) = sig + + npvIdAndVRFOutput (NonPersistentVote seatIndex _ _ vrfOutput _) = (seatIndex, vrfOutput) + npvIdAndVRFOutput _ = error "This function should only be called on non-persistent votes" + + voteSeatIndex (PersistentVote seatIndex _ _ _) = seatIndex + voteSeatIndex (NonPersistentVote seatIndex _ _ _ _) = seatIndex + + maxWithDefault def f xs = foldl' (\acc x -> max acc (f x)) def xs + + verifyCert :: + WFALSCommitteeSelection crypto -> + Cert (WFALSCommitteeSelection crypto) -> + Either (WFALSCommitteeSelectionError crypto) (NonEmpty (WFALSCommitteeMember crypto)) + verifyCert selection WFALSCert + { certElectionId + , certVoteMessage + , persistentVoters + , nonPersistentVotersToEligibility + , groupSignature + } = do + -- Check that the cert's election ID matches the committee selection's election ID + if certElectionId /= csElectionId selection + then Left CertElectionIdMismatch + else pure () + + let pvIndices = bitmapToIndices persistentVoters + + -- Look up persistent members + sortedPvMembers <- + sortOn memberSeatIndex + <$> mapM (\seatIndex -> lookupPersistentCommitteeMember selection seatIndex) pvIndices + + -- Look up non-persistent members + sortedNpvMembers <- + sortOn memberSeatIndex + <$> mapM + (\(seatIndex, vrfOut) -> lookupNonPersistentCommitteeMember selection seatIndex vrfOut) + (Map.toList nonPersistentVotersToEligibility) + + let allMembers = sortedPvMembers ++ sortedNpvMembers + members <- assert (sortOn memberSeatIndex allMembers == allMembers) $ + case NE.nonEmpty allMembers of + Nothing -> Left EmptyCert + Just ne -> Right ne + + -- Extract keys for verification + let + sortedPvSignPubKeys = getVoteSignaturePublicKey (Proxy @crypto) . memberPubKeys <$> sortedPvMembers + + sortedNpvPubKeys = memberPubKeys <$> sortedNpvMembers + sortedNpvSignPubKeys = getVoteSignaturePublicKey (Proxy @crypto) <$> sortedNpvPubKeys + sortedNpvVRFVerifyKeys = getVRFVerifyKey (Proxy @crypto) <$> sortedNpvPubKeys + + sortedNpvVRFOutputs = nonPersistentMemberVRFOutput <$> sortedNpvMembers + + vrfElectionInput = mkVRFElectionInput @crypto (epochNonce selection) certElectionId + + -- Group VRF verification + () <- assert (length sortedNpvVRFVerifyKeys == length sortedNpvVRFOutputs) $ + case (NE.nonEmpty sortedNpvVRFVerifyKeys, NE.nonEmpty sortedNpvVRFOutputs) of + (Just vrfVerifyKeys, Just vrfOutputs) -> + -- We do group verification of the VRF output + -- Crypto schemes can use the trivial CryptoSupportsNaiveGroupVRF instance, which under the hood just verifies each VRF output individually, if they want to opt out of this optimization + let groupVerifyKey = sconcat $ liftVRFVerifyKey (Proxy @crypto) <$> vrfVerifyKeys + groupVRFOutput = sconcat $ liftVRFOutput (Proxy @crypto) <$> vrfOutputs + in first InvalidGroupVRF $ + verifyGroupVRF (Proxy @crypto) groupVerifyKey vrfElectionInput groupVRFOutput + (Nothing, Nothing) -> pure () + _ -> + error + "The two lists have initially the same length, so they should both be empty or both be non-empty" + + -- Group signature verification + let sortedSignPubKeys = sortedPvSignPubKeys ++ sortedNpvSignPubKeys + () <- assert (length sortedSignPubKeys == length allMembers) $ + case NE.nonEmpty sortedSignPubKeys of + Just signPubKeys -> + let groupPublicKey = sconcat $ liftVoteSignaturePublicKey (Proxy @crypto) <$> signPubKeys + in first InvalidGroupVoteSignature $ + verifyGroupVoteSignature + (Proxy @crypto) + groupPublicKey + certElectionId + certVoteMessage + groupSignature + Nothing -> pure () + + Right members + where + memberPubKeys m = let (_, _, pk, _, _) = committeeMemberCandidate m in pk + memberSeatIndex m = let (seatIndex, _, _, _, _) = committeeMemberCandidate m in seatIndex + + nonPersistentMemberVRFOutput (NonPersistentCommitteeMember _ vrfOutput _ _) = vrfOutput + nonPersistentMemberVRFOutput _ = error "This function should only be called on non-persistent members" diff --git a/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Peras/Crypto.hs b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Peras/Crypto.hs new file mode 100644 index 0000000000..afff388753 --- /dev/null +++ b/ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Peras/Crypto.hs @@ -0,0 +1,357 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneKindSignatures #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +module Ouroboros.Consensus.Peras.Crypto + ( -- * Crypto types for Peras + PerasVoteCrypto + , PerasKeyRole (..) + , PerasKeyScope + , PerasBLSPublicKey + , PerasBLSPrivateKey + , PerasSignature + , PerasProofOfPossession + , HasPerasBLSContext (..) + , perasSignWithRole + , perasVerifyWithRole + , perasCreateProofOfPossession + , perasVerifyProofOfPossession + ) where + +import Cardano.Crypto.DSIGN + ( BLS12381MinSigDSIGN + , BLS12381SignContext (..) + , DSIGNAggregatable (..) + , DSIGNAlgorithm (..) + ) +import Cardano.Crypto.Hash (Hash) +import qualified Cardano.Crypto.Hash as Hash +import Cardano.Crypto.Util (SignableRepresentation, bytesToNatural) +import Cardano.Ledger.BaseTypes (Nonce (..)) +import Cardano.Ledger.Binary (runByteBuilder) +import Cardano.Ledger.Hashes (HASH, KeyHash (..)) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Builder as BS +import qualified Data.ByteString.Builder.Extra as BS +import Data.Coerce (coerce) +import Data.Kind (Type) +import Data.Proxy (Proxy (..)) +import Data.Ratio ((%)) +import GHC.Exts (Any) +import Ouroboros.Consensus.Block.Abstract + ( ConvertRawHash + , Point (..) + , toRawHash + ) +import Ouroboros.Consensus.Block.SupportsPeras + ( BlockSupportsPeras (..) + , PerasRoundNo (..) + , PerasVote (..) + ) +import Ouroboros.Consensus.Committee.Crypto + ( CryptoSupportsVRF + , CryptoSupportsVoteSigning (..) + , ElectionId + , NormalizedVRFOutput (..) + , VRFPoolContext (..) + ) +import qualified Ouroboros.Consensus.Committee.Crypto as CommitteeSelection +import Ouroboros.Consensus.Committee.Types (PoolId (..)) +import Ouroboros.Consensus.Committee.WFALS + ( CryptoSupportsWFALS + ) + +-- * Generic Peras vote crypto types + +-- | Key roles for Peras +data PerasKeyRole + = -- | Key role for signing Peras votes + SIGN + | -- | Key role for local sortition in Peras elections + VRF + | -- | Key role for Proof of Possession + POP + +-- | Key scope for Peras, later instantiated with network id (e.g MAINNET) +type PerasKeyScope = ByteString + +-- * BLS crypto interface + +-- | Public key type for Peras, parameterized by key role +type PerasBLSPublicKey :: PerasKeyRole -> Type +data PerasBLSPublicKey r = PerasBLSPublicKey + { unPerasBLSPublicKey :: VerKeyDSIGN BLS12381MinSigDSIGN + , perasBLSPublicKeyScope :: PerasKeyScope + } + deriving (Eq, Show) + +coercePerasBLSPublicKey :: + forall r2 r1. + PerasBLSPublicKey r1 -> + PerasBLSPublicKey r2 +coercePerasBLSPublicKey = coerce + +-- | Private key type for Peras, parameterized by key role +type PerasBLSPrivateKey :: PerasKeyRole -> Type +data PerasBLSPrivateKey r = PerasBLSPrivateKey + { unPerasBLSPrivateKey :: SignKeyDSIGN BLS12381MinSigDSIGN + , perasBLSPrivateKeyScope :: PerasKeyScope + } + deriving (Eq, Show) + +coercePerasBLSPrivateKey :: + forall r2 r1. + PerasBLSPrivateKey r1 -> + PerasBLSPrivateKey r2 +coercePerasBLSPrivateKey = coerce + +-- | Signature type for Peras, parameterized by key role +type PerasSignature :: PerasKeyRole -> Type +newtype PerasSignature r = PerasSignature + { unPerasSignature :: SigDSIGN BLS12381MinSigDSIGN + } + deriving (Eq, Show) + +-- | Proof of Possession type for Peras +newtype PerasProofOfPossession = PerasProofOfPossession + { unPerasProofOfPossession :: PossessionProofDSIGN BLS12381MinSigDSIGN + } + deriving (Eq, Show) + +-- Basic over G1: +-- https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-06.html#section-4.2.1-1 +minSigSignatureDST :: BLS12381SignContext +minSigSignatureDST = + BLS12381SignContext + { blsSignContextDst = Just "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" + , blsSignContextAug = Nothing + } + +-- PoP over G1: +-- https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-06.html#section-4.2.3-1 +minSigPoPDST :: BLS12381SignContext +minSigPoPDST = + BLS12381SignContext + { blsSignContextDst = Just "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_" + , blsSignContextAug = Nothing + } + +-- | Role-separated BLS contexts for Peras signatures +class HasPerasBLSContext (r :: PerasKeyRole) where + blsCtx :: Proxy r -> PerasKeyScope -> BLS12381SignContext + +instance HasPerasBLSContext SIGN where + blsCtx _ keyScope = + minSigSignatureDST + { blsSignContextAug = + Just ("PERAS:VOTE:" <> keyScope <> ":V0") + } + +instance HasPerasBLSContext VRF where + blsCtx _ keyScope = + minSigSignatureDST + { blsSignContextAug = + Just ("PERAS:VRF:" <> keyScope <> ":V0") + } + +instance HasPerasBLSContext POP where + blsCtx _ keyScope = + minSigPoPDST + { blsSignContextAug = + Just ("PERAS:POP:" <> keyScope <> ":V0") + } + +-- | Sign a message with a Peras private key, producing a Peras signature +perasSignWithRole :: + forall r msg. + ( SignableRepresentation msg + , HasPerasBLSContext r + ) => + PerasBLSPrivateKey r -> + msg -> + PerasSignature r +perasSignWithRole sk msg = + PerasSignature + { unPerasSignature = + signDSIGN + (blsCtx (Proxy @r) (perasBLSPrivateKeyScope sk)) + msg + (unPerasBLSPrivateKey sk) + } + +-- | Verify a Peras signature on a message with a Peras public key +perasVerifyWithRole :: + forall r msg. + ( SignableRepresentation msg + , HasPerasBLSContext r + ) => + PerasBLSPublicKey r -> + msg -> + PerasSignature r -> + Either String () +perasVerifyWithRole pk msg (PerasSignature sig) = + verifyDSIGN + (blsCtx (Proxy @r) (perasBLSPublicKeyScope pk)) + (unPerasBLSPublicKey pk) + msg + sig + +-- | Create a proof of possession signature for a Peras private key +perasCreateProofOfPossession :: + PerasBLSPrivateKey POP -> + PoolId -> + PerasProofOfPossession +perasCreateProofOfPossession sk poolId = + PerasProofOfPossession + { unPerasProofOfPossession = + createPossessionProofDSIGN + extCtx + (unPerasBLSPrivateKey sk) + } + where + poolBytes = Hash.hashToBytes (unKeyHash (unPoolId poolId)) + baseCtx = blsCtx (Proxy @POP) (perasBLSPrivateKeyScope sk) + extCtx = baseCtx{blsSignContextAug = blsSignContextAug baseCtx <> Just poolBytes} + +-- | Verify a proof of possession signature for a Peras public key +perasVerifyProofOfPossession :: + PerasBLSPublicKey POP -> + PoolId -> + PerasProofOfPossession -> + Either String () +perasVerifyProofOfPossession pk poolId pop = + verifyPossessionProofDSIGN + extCtx + (unPerasBLSPublicKey pk) + (unPerasProofOfPossession pop) + where + poolBytes = Hash.hashToBytes (unKeyHash (unPoolId poolId)) + baseCtx = blsCtx (Proxy @POP) (perasBLSPublicKeyScope pk) + extCtx = baseCtx{blsSignContextAug = blsSignContextAug baseCtx <> Just poolBytes} + +-- | Hash the message to be signed for a Peras vote +hashVoteSignature :: + forall blk. + ConvertRawHash blk => + PerasRoundNo -> + Point blk -> + Hash HASH (SigDSIGN BLS12381MinSigDSIGN) +hashVoteSignature roundNo point = + Hash.castHash + . Hash.hashWith id + . runByteBuilder (8 + 32) + $ roundNoBytes <> pointBytes + where + roundNoBytes = + BS.word64BE (unPerasRoundNo roundNo) + pointBytes = + case point of + GenesisPoint -> mempty + BlockPoint _ h -> BS.byteStringCopy (toRawHash (Proxy @blk) h) + +-- | Hash the input for the VRF used in Peras elections +hashVRFInput :: + PerasRoundNo -> + Nonce -> + Hash HASH (SigDSIGN BLS12381MinSigDSIGN) +hashVRFInput roundNo epochNonce = + Hash.castHash + . Hash.hashWith id + . runByteBuilder (8 + 32) + $ roundNoBytes <> epochNonceBytes + where + roundNoBytes = + BS.word64BE (unPerasRoundNo roundNo) + epochNonceBytes = + case epochNonce of + NeutralNonce -> mempty + Nonce h -> BS.byteStringCopy (Hash.hashToBytes h) + +-- * Crypto types for votes + +data PerasVoteCrypto blk + +-- | Peras elections are identified by their round number +type instance ElectionId (PerasVoteCrypto blk) = PerasRoundNo + +-- ** Vote crypto for Peras (degenerate instance for now) + +instance ConvertRawHash blk => CryptoSupportsVoteSigning (PerasVoteCrypto blk) where + type VoteSignaturePrivateKey (PerasVoteCrypto blk) = PerasBLSPrivateKey SIGN + type VoteSignaturePublicKey (PerasVoteCrypto blk) = PerasBLSPublicKey SIGN + + newtype VoteMessage (PerasVoteCrypto blk) + = PerasVoteMessage (Point blk) + deriving (Eq, Show) + + newtype VoteSignature (PerasVoteCrypto blk) + = PerasVoteSignature (PerasSignature SIGN) + deriving (Eq, Show) + + signVote sk roundNo (PerasVoteMessage point) = + PerasVoteSignature $ + perasSignWithRole @SIGN sk $ + hashVoteSignature roundNo point + + verifyVoteSignature pk roundNo (PerasVoteMessage point) (PerasVoteSignature sig) = + perasVerifyWithRole @SIGN + pk + (hashVoteSignature roundNo point) + sig + +instance ConvertRawHash blk => CryptoSupportsWFALS (PerasVoteCrypto blk) where + type PrivateKey (PerasVoteCrypto blk) = PerasBLSPrivateKey Any + type PublicKey (PerasVoteCrypto blk) = PerasBLSPublicKey Any + + getVoteSignaturePublicKey _ = coerce + getVoteSignaturePrivateKey _ = coerce + getVRFVerifyKey _ = coerce + getVRFSigningKey _ = coerce + +-- ** VRF crypto for Peras (degenerate instance for now) + +instance CryptoSupportsVRF (PerasVoteCrypto blk) where + type VRFVerifyKey (PerasVoteCrypto blk) = PerasBLSPublicKey VRF + type VRFSigningKey (PerasVoteCrypto blk) = PerasBLSPrivateKey VRF + + newtype VRFElectionInput (PerasVoteCrypto blk) + = PerasVoteVRFElectionInput (Hash HASH (SigDSIGN BLS12381MinSigDSIGN)) + deriving (Eq, Show) + + newtype VRFOutput (PerasVoteCrypto blk) + = PerasVoteVRFOutput (PerasSignature VRF) + deriving (Eq, Show) + + mkVRFElectionInput epochNonce roundNo = + PerasVoteVRFElectionInput $ + hashVRFInput roundNo epochNonce + + evalVRF context (PerasVoteVRFElectionInput input) = + case context of + VRFSignContext sk -> do + let sig = perasSignWithRole @VRF (coercePerasBLSPrivateKey @VRF sk) input + pure $ PerasVoteVRFOutput sig + VRFVerifyContext pk (PerasVoteVRFOutput sig) -> do + perasVerifyWithRole @VRF (coercePerasBLSPublicKey @VRF pk) input sig + pure $ PerasVoteVRFOutput sig + + normalizeVRFOutput (PerasVoteVRFOutput sig) = + NormalizedVRFOutput $ + toInteger vrfOutputNatural % vrfOutputMax + where + -- TODO(peras): we need to settle on a concrete hash function to use here. + vrfOutputNatural = + bytesToNatural $ + Hash.hashToBytes $ + Hash.hashWith @HASH rawSerialiseSigDSIGN $ + unPerasSignature sig + vrfOutputMax = + 2 ^ ((8 :: Integer) * hashSize) - 1 + hashSize = + fromIntegral @Word (Hash.hashSize (Proxy :: Proxy HASH)) diff --git a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS.hs b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS.hs index 3b801d9582..2d49f68afa 100644 --- a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS.hs +++ b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS.hs @@ -1,15 +1,18 @@ -module Test.Consensus.Committee.WFALS - ( tests - ) -where +{-# LANGUAGE LambdaCase #-} +module Test.Consensus.Committee.WFALS (tests) where + +import qualified Cardano.Crypto.DSIGN.Class as SL +import qualified Cardano.Crypto.Seed as SL +import qualified Cardano.Ledger.Keys as SL import qualified Data.Map.Strict as Map +import Data.String (IsString (..)) +import qualified Ouroboros.Consensus.Committee.Types as WFA +import qualified Ouroboros.Consensus.Committee.WFA as WFA import Test.Consensus.Committee.WFALS.Conformance (conformsToRustImplementation) import qualified Test.Consensus.Committee.WFALS.Model as Model import qualified Test.Consensus.Committee.WFALS.Model.Test as Model import Test.Tasty (TestTree, testGroup) -import Test.Tasty.QuickCheck (testProperty) -import Test.Util.TestEnv (adjustQuickCheckTests) tests :: TestTree tests = @@ -17,20 +20,79 @@ tests = "weighted Fait-Accompli committee selection tests" [ Model.tests , modelConformsToRustImplementation + , realImplementationConformsToRustImplementation ] -- | Check that the model implementation matches the Rust one modelConformsToRustImplementation :: TestTree modelConformsToRustImplementation = - adjustQuickCheckTests (* 10) $ - testProperty "model conforms to Rust implementation" $ - conformsToRustImplementation model + conformsToRustImplementation + "model conforms to Rust implementation" + mkStakeDistr + model where + mkStakeDistr = + Map.map Model.rationalToStake + model stakeDistr targetCommitteeSize = let (persistentSeats, numNonPersistentSeats, _) = Model.weightedFaitAccompliPersistentSeats (fromIntegral targetCommitteeSize) - (Map.map Model.rationalToStake stakeDistr) - in ( Map.size persistentSeats + stakeDistr + in ( fromIntegral (Map.size persistentSeats) , fromIntegral numNonPersistentSeats ) + +-- | Check that the real implementation matches the Rust one +realImplementationConformsToRustImplementation :: TestTree +realImplementationConformsToRustImplementation = + conformsToRustImplementation + "real implementation conforms to Rust implementation" + mkStakeDistr + impl + where + -- NOTE: we don't seem to have an easy way to convert the input hash into its + -- corresponding 'KeyHash StakePool', so here we are just recreating a new one + -- derived from the input string. This is fine for our purposes since we don't + -- inspect the actual pool IDs in the implementation, and we only rely on them + -- being unique, which they should be as long as the input strings are unique. + mkStakeDistr = + expectRight + ( \err -> + error ("could not build a strake distribution: " <> show err) + ) + . WFA.mkExtWFAStakeDistr + . Map.mapKeys + ( \str -> + WFA.PoolId + . SL.hashKey + . SL.VKey + . SL.deriveVerKeyDSIGN + . SL.genKeyDSIGN + . SL.mkSeedFromBytes + . fromString + $ str + ) + . Map.map + ( \stake -> + (WFA.LedgerStake stake, ()) + ) + + impl stakeDistr targetCommitteeSize = + let + totalSeats = + WFA.TargetCommitteeSize (fromIntegral targetCommitteeSize) + (persistentSeats, nonPersistentSeats, _, _) = + expectRight + ( \err -> + error ("weightedFaitAccompliSplitSeats failed: " <> show err) + ) + $ WFA.weightedFaitAccompliSplitSeats stakeDistr totalSeats + in + ( fromIntegral (WFA.unPersistentCommitteeSize persistentSeats) + , fromIntegral (WFA.unNonPersistentCommitteeSize nonPersistentSeats) + ) + + expectRight onLeft = \case + Left err -> onLeft err + Right a -> a diff --git a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Conformance.hs b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Conformance.hs index eeeb9994b4..23404b210d 100644 --- a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Conformance.hs +++ b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Conformance.hs @@ -5,7 +5,13 @@ {-# LANGUAGE TypeApplications #-} module Test.Consensus.Committee.WFALS.Conformance - ( conformsToRustImplementation + ( CommitteeSize + , NumPersistent + , NumNonPersistent + , PoolId + , Stake + , StakeDistr + , conformsToRustImplementation ) where @@ -19,14 +25,14 @@ import Data.Array (Array) import qualified Data.Array as Array import qualified Data.FileEmbed as FileEmbed import Data.Map.Strict (Map) +import Data.Word (Word64) import System.FilePath (()) -import Test.QuickCheck (Property) -import Test.QuickCheck.Gen (Gen, choose) -import Test.QuickCheck.Property (counterexample, forAll, (===)) +import Test.Tasty (TestTree) +import Test.Tasty.HUnit (assertEqual, testCase) -type CommitteeSize = Int -type NumPersistent = Int -type NumNonPersistent = Int +type CommitteeSize = Word64 +type NumPersistent = Word64 +type NumNonPersistent = Word64 type PoolId = String type Stake = Rational type StakeDistr = Map PoolId Stake @@ -63,8 +69,8 @@ rustResults = Array.listArray (0, length pairs - 1) pairs -- | Embedded stake distribution -stakeDistr :: StakeDistr -stakeDistr = +exampleStakeDistr :: StakeDistr +exampleStakeDistr = either error id $ eitherDecodeStrict $ $( FileEmbed.embedFile $ @@ -75,41 +81,43 @@ stakeDistr = "stake_distr.json" ) --- | Sample a value from an array -sampleArray :: Array Int a -> Gen a -sampleArray array = do - i <- choose (Array.bounds array) - pure $ (Array.!) array i - -- | Check that a weighted fait accompli committee selection implementation -- conforms to the Rust implementation by comparing the number persistent and -- non-persistent committee members it selects for a given target committee size. conformsToRustImplementation :: - ( Map PoolId Stake -> + String -> + (Map PoolId Stake -> stakeDistr) -> + ( stakeDistr -> CommitteeSize -> ( NumPersistent , NumNonPersistent ) ) -> - Property -conformsToRustImplementation wfals = do - forAll (sampleArray rustResults) $ - \RustResult - { targetCommitteeSize - , numPersistent - , numNonPersistent - } -> do - let (actualNumPersistent, actualNumNonPersistent) = - wfals stakeDistr targetCommitteeSize - counterexample - ( unlines - [ "Target committee size: " - <> show targetCommitteeSize - , "Expected (persistent, non-persistent): " - <> show (numPersistent, numNonPersistent) - , "Actual (persistent, non-persistent): " - <> show (actualNumPersistent, actualNumNonPersistent) - ] - ) - $ (actualNumPersistent, actualNumNonPersistent) - === (numPersistent, numNonPersistent) + TestTree +conformsToRustImplementation name mkStakeDistr wfa = do + testCase name (go (Array.bounds rustResults)) + where + stakeDistr = mkStakeDistr exampleStakeDistr + + go (currStep, lastStep) + | currStep > lastStep = + pure () + | otherwise = do + step ((Array.!) rustResults currStep) + go (succ currStep, lastStep) + + step RustResult{targetCommitteeSize, numPersistent, numNonPersistent} = do + let (actualNumPersistent, actualNumNonPersistent) = + wfa stakeDistr targetCommitteeSize + assertEqual + ( unlines + [ "Target committee size: " + <> show targetCommitteeSize + , "Expected (persistent, non-persistent): " + <> show (numPersistent, numNonPersistent) + , "Actual (persistent, non-persistent): " + <> show (actualNumPersistent, actualNumNonPersistent) + ] + ) + (numPersistent, numNonPersistent) + (actualNumPersistent, actualNumNonPersistent) diff --git a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model.hs b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model.hs index 8394b14ad8..083d281a12 100644 --- a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model.hs +++ b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model.hs @@ -222,9 +222,19 @@ weightedFaitAccompliPersistentSeats globalNumSeats stakeDistr error "Stake distribution cannot be empty" | sum stakeDistr == 0 = error "Total stake must be positive" + | numPoolsWithPositiveStake < globalNumSeats = + error "Not enough voters with positive stake to fill all expected seats" | otherwise = (persistentSeats, numNonPersistentSeats, residualStakeDistr) where + -- Number of voters with positive stake in the input distribution + numPoolsWithPositiveStake = + fromIntegral + . length + . filter (> 0) + . Map.elems + $ stakeDistr + -- Persistent seats selected deterministically based on their ledger stake. -- NOTE: their voting stake is *exactly* their ledger stake. persistentSeats = diff --git a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model/Test.hs b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model/Test.hs index 5393f249bb..61d04f13bb 100644 --- a/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model/Test.hs +++ b/ouroboros-consensus/test/consensus-test/Test/Consensus/Committee/WFALS/Model/Test.hs @@ -82,16 +82,22 @@ genStakeDistr size = do -- * Property helpers -- | Helper to generate stake distributions along with a number of seats that --- could possibly exceed the number of nodes in the stake distribution. +-- lies within the accepatable range [1, #{nodes with positive stake}] forAllStakeDistrAndNumSeats :: (StakeDistr Ledger Global -> NumSeats Global -> Property) -> Property forAllStakeDistrAndNumSeats p = forAll (sized genStakeDistr) $ \stakeDistr -> do - let numNodes = fromIntegral (Map.size stakeDistr) - -- generate a number of seats that could possibly exceed the number of nodes - let genNumSeats = fromInteger <$> choose (1, numNodes + 1) - forAll genNumSeats $ \numSeats -> p stakeDistr numSeats + let numPositiveStakeNodes = + fromIntegral + . length + . filter ((> 0) . stakeToRational) + . Map.elems + $ stakeDistr + let genNumSeats = + fromInteger <$> choose (1, numPositiveStakeNodes) + forAll genNumSeats $ \numSeats -> + p stakeDistr numSeats -- | Tabulate the target number of seats tabulateTargetNumSeats :: NumSeats Global -> Property -> Property diff --git a/ouroboros-consensus/test/consensus-test/data/rust_results.json b/ouroboros-consensus/test/consensus-test/data/rust_results.json index 647b6e166a..a845e562d5 100644 --- a/ouroboros-consensus/test/consensus-test/data/rust_results.json +++ b/ouroboros-consensus/test/consensus-test/data/rust_results.json @@ -10258,4745 +10258,5 @@ "target": 2052, "persistent": 2051, "nonpersistent": 1 - }, - { - "target": 2053, - "persistent": 2052, - "nonpersistent": 1 - }, - { - "target": 2054, - "persistent": 2052, - "nonpersistent": 2 - }, - { - "target": 2055, - "persistent": 2052, - "nonpersistent": 3 - }, - { - "target": 2056, - "persistent": 2052, - "nonpersistent": 4 - }, - { - "target": 2057, - "persistent": 2052, - "nonpersistent": 5 - }, - { - "target": 2058, - "persistent": 2052, - "nonpersistent": 6 - }, - { - "target": 2059, - "persistent": 2052, - "nonpersistent": 7 - }, - { - "target": 2060, - "persistent": 2052, - "nonpersistent": 8 - }, - { - "target": 2061, - "persistent": 2052, - "nonpersistent": 9 - }, - { - "target": 2062, - "persistent": 2052, - "nonpersistent": 10 - }, - { - "target": 2063, - "persistent": 2052, - "nonpersistent": 11 - }, - { - "target": 2064, - "persistent": 2052, - "nonpersistent": 12 - }, - { - "target": 2065, - "persistent": 2052, - "nonpersistent": 13 - }, - { - "target": 2066, - "persistent": 2052, - "nonpersistent": 14 - }, - { - "target": 2067, - "persistent": 2052, - "nonpersistent": 15 - }, - { - "target": 2068, - "persistent": 2052, - "nonpersistent": 16 - }, - { - "target": 2069, - "persistent": 2052, - "nonpersistent": 17 - }, - { - "target": 2070, - "persistent": 2052, - "nonpersistent": 18 - }, - { - "target": 2071, - "persistent": 2052, - "nonpersistent": 19 - }, - { - "target": 2072, - "persistent": 2052, - "nonpersistent": 20 - }, - { - "target": 2073, - "persistent": 2052, - "nonpersistent": 21 - }, - { - "target": 2074, - "persistent": 2052, - "nonpersistent": 22 - }, - { - "target": 2075, - "persistent": 2052, - "nonpersistent": 23 - }, - { - "target": 2076, - "persistent": 2052, - "nonpersistent": 24 - }, - { - "target": 2077, - "persistent": 2052, - "nonpersistent": 25 - }, - { - "target": 2078, - "persistent": 2052, - "nonpersistent": 26 - }, - { - "target": 2079, - "persistent": 2052, - "nonpersistent": 27 - }, - { - "target": 2080, - "persistent": 2052, - "nonpersistent": 28 - }, - { - "target": 2081, - "persistent": 2052, - "nonpersistent": 29 - }, - { - "target": 2082, - "persistent": 2052, - "nonpersistent": 30 - }, - { - "target": 2083, - "persistent": 2052, - "nonpersistent": 31 - }, - { - "target": 2084, - "persistent": 2052, - "nonpersistent": 32 - }, - { - "target": 2085, - "persistent": 2052, - "nonpersistent": 33 - }, - { - "target": 2086, - "persistent": 2052, - "nonpersistent": 34 - }, - { - "target": 2087, - "persistent": 2052, - "nonpersistent": 35 - }, - { - "target": 2088, - "persistent": 2052, - "nonpersistent": 36 - }, - { - "target": 2089, - "persistent": 2052, - "nonpersistent": 37 - }, - { - "target": 2090, - "persistent": 2052, - "nonpersistent": 38 - }, - { - "target": 2091, - "persistent": 2052, - "nonpersistent": 39 - }, - { - "target": 2092, - "persistent": 2052, - "nonpersistent": 40 - }, - { - "target": 2093, - "persistent": 2052, - "nonpersistent": 41 - }, - { - "target": 2094, - "persistent": 2052, - "nonpersistent": 42 - }, - { - "target": 2095, - "persistent": 2052, - "nonpersistent": 43 - }, - { - "target": 2096, - "persistent": 2052, - "nonpersistent": 44 - }, - { - "target": 2097, - "persistent": 2052, - "nonpersistent": 45 - }, - { - "target": 2098, - "persistent": 2052, - "nonpersistent": 46 - }, - { - "target": 2099, - "persistent": 2052, - "nonpersistent": 47 - }, - { - "target": 2100, - "persistent": 2052, - "nonpersistent": 48 - }, - { - "target": 2101, - "persistent": 2052, - "nonpersistent": 49 - }, - { - "target": 2102, - "persistent": 2052, - "nonpersistent": 50 - }, - { - "target": 2103, - "persistent": 2052, - "nonpersistent": 51 - }, - { - "target": 2104, - "persistent": 2052, - "nonpersistent": 52 - }, - { - "target": 2105, - "persistent": 2052, - "nonpersistent": 53 - }, - { - "target": 2106, - "persistent": 2052, - "nonpersistent": 54 - }, - { - "target": 2107, - "persistent": 2052, - "nonpersistent": 55 - }, - { - "target": 2108, - "persistent": 2052, - "nonpersistent": 56 - }, - { - "target": 2109, - "persistent": 2052, - "nonpersistent": 57 - }, - { - "target": 2110, - "persistent": 2052, - "nonpersistent": 58 - }, - { - "target": 2111, - "persistent": 2052, - "nonpersistent": 59 - }, - { - "target": 2112, - "persistent": 2052, - "nonpersistent": 60 - }, - { - "target": 2113, - "persistent": 2052, - "nonpersistent": 61 - }, - { - "target": 2114, - "persistent": 2052, - "nonpersistent": 62 - }, - { - "target": 2115, - "persistent": 2052, - "nonpersistent": 63 - }, - { - "target": 2116, - "persistent": 2052, - "nonpersistent": 64 - }, - { - "target": 2117, - "persistent": 2052, - "nonpersistent": 65 - }, - { - "target": 2118, - "persistent": 2052, - "nonpersistent": 66 - }, - { - "target": 2119, - "persistent": 2052, - "nonpersistent": 67 - }, - { - "target": 2120, - "persistent": 2052, - "nonpersistent": 68 - }, - { - "target": 2121, - "persistent": 2052, - "nonpersistent": 69 - }, - { - "target": 2122, - "persistent": 2052, - "nonpersistent": 70 - }, - { - "target": 2123, - "persistent": 2052, - "nonpersistent": 71 - }, - { - "target": 2124, - "persistent": 2052, - "nonpersistent": 72 - }, - { - "target": 2125, - "persistent": 2052, - "nonpersistent": 73 - }, - { - "target": 2126, - "persistent": 2052, - "nonpersistent": 74 - }, - { - "target": 2127, - "persistent": 2052, - "nonpersistent": 75 - }, - { - "target": 2128, - "persistent": 2052, - "nonpersistent": 76 - }, - { - "target": 2129, - "persistent": 2052, - "nonpersistent": 77 - }, - { - "target": 2130, - "persistent": 2052, - "nonpersistent": 78 - }, - { - "target": 2131, - "persistent": 2052, - "nonpersistent": 79 - }, - { - "target": 2132, - "persistent": 2052, - "nonpersistent": 80 - }, - { - "target": 2133, - "persistent": 2052, - "nonpersistent": 81 - }, - { - "target": 2134, - "persistent": 2052, - "nonpersistent": 82 - }, - { - "target": 2135, - "persistent": 2052, - "nonpersistent": 83 - }, - { - "target": 2136, - "persistent": 2052, - "nonpersistent": 84 - }, - { - "target": 2137, - "persistent": 2052, - "nonpersistent": 85 - }, - { - "target": 2138, - "persistent": 2052, - "nonpersistent": 86 - }, - { - "target": 2139, - "persistent": 2052, - "nonpersistent": 87 - }, - { - "target": 2140, - "persistent": 2052, - "nonpersistent": 88 - }, - { - "target": 2141, - "persistent": 2052, - "nonpersistent": 89 - }, - { - "target": 2142, - "persistent": 2052, - "nonpersistent": 90 - }, - { - "target": 2143, - "persistent": 2052, - "nonpersistent": 91 - }, - { - "target": 2144, - "persistent": 2052, - "nonpersistent": 92 - }, - { - "target": 2145, - "persistent": 2052, - "nonpersistent": 93 - }, - { - "target": 2146, - "persistent": 2052, - "nonpersistent": 94 - }, - { - "target": 2147, - "persistent": 2052, - "nonpersistent": 95 - }, - { - "target": 2148, - "persistent": 2052, - "nonpersistent": 96 - }, - { - "target": 2149, - "persistent": 2052, - "nonpersistent": 97 - }, - { - "target": 2150, - "persistent": 2052, - "nonpersistent": 98 - }, - { - "target": 2151, - "persistent": 2052, - "nonpersistent": 99 - }, - { - "target": 2152, - "persistent": 2052, - "nonpersistent": 100 - }, - { - "target": 2153, - "persistent": 2052, - "nonpersistent": 101 - }, - { - "target": 2154, - "persistent": 2052, - "nonpersistent": 102 - }, - { - "target": 2155, - "persistent": 2052, - "nonpersistent": 103 - }, - { - "target": 2156, - "persistent": 2052, - "nonpersistent": 104 - }, - { - "target": 2157, - "persistent": 2052, - "nonpersistent": 105 - }, - { - "target": 2158, - "persistent": 2052, - "nonpersistent": 106 - }, - { - "target": 2159, - "persistent": 2052, - "nonpersistent": 107 - }, - { - "target": 2160, - "persistent": 2052, - "nonpersistent": 108 - }, - { - "target": 2161, - "persistent": 2052, - "nonpersistent": 109 - }, - { - "target": 2162, - "persistent": 2052, - "nonpersistent": 110 - }, - { - "target": 2163, - "persistent": 2052, - "nonpersistent": 111 - }, - { - "target": 2164, - "persistent": 2052, - "nonpersistent": 112 - }, - { - "target": 2165, - "persistent": 2052, - "nonpersistent": 113 - }, - { - "target": 2166, - "persistent": 2052, - "nonpersistent": 114 - }, - { - "target": 2167, - "persistent": 2052, - "nonpersistent": 115 - }, - { - "target": 2168, - "persistent": 2052, - "nonpersistent": 116 - }, - { - "target": 2169, - "persistent": 2052, - "nonpersistent": 117 - }, - { - "target": 2170, - "persistent": 2052, - "nonpersistent": 118 - }, - { - "target": 2171, - "persistent": 2052, - "nonpersistent": 119 - }, - { - "target": 2172, - "persistent": 2052, - "nonpersistent": 120 - }, - { - "target": 2173, - "persistent": 2052, - "nonpersistent": 121 - }, - { - "target": 2174, - "persistent": 2052, - "nonpersistent": 122 - }, - { - "target": 2175, - "persistent": 2052, - "nonpersistent": 123 - }, - { - "target": 2176, - "persistent": 2052, - "nonpersistent": 124 - }, - { - "target": 2177, - "persistent": 2052, - "nonpersistent": 125 - }, - { - "target": 2178, - "persistent": 2052, - "nonpersistent": 126 - }, - { - "target": 2179, - "persistent": 2052, - "nonpersistent": 127 - }, - { - "target": 2180, - "persistent": 2052, - "nonpersistent": 128 - }, - { - "target": 2181, - "persistent": 2052, - "nonpersistent": 129 - }, - { - "target": 2182, - "persistent": 2052, - "nonpersistent": 130 - }, - { - "target": 2183, - "persistent": 2052, - "nonpersistent": 131 - }, - { - "target": 2184, - "persistent": 2052, - "nonpersistent": 132 - }, - { - "target": 2185, - "persistent": 2052, - "nonpersistent": 133 - }, - { - "target": 2186, - "persistent": 2052, - "nonpersistent": 134 - }, - { - "target": 2187, - "persistent": 2052, - "nonpersistent": 135 - }, - { - "target": 2188, - "persistent": 2052, - "nonpersistent": 136 - }, - { - "target": 2189, - "persistent": 2052, - "nonpersistent": 137 - }, - { - "target": 2190, - "persistent": 2052, - "nonpersistent": 138 - }, - { - "target": 2191, - "persistent": 2052, - "nonpersistent": 139 - }, - { - "target": 2192, - "persistent": 2052, - "nonpersistent": 140 - }, - { - "target": 2193, - "persistent": 2052, - "nonpersistent": 141 - }, - { - "target": 2194, - "persistent": 2052, - "nonpersistent": 142 - }, - { - "target": 2195, - "persistent": 2052, - "nonpersistent": 143 - }, - { - "target": 2196, - "persistent": 2052, - "nonpersistent": 144 - }, - { - "target": 2197, - "persistent": 2052, - "nonpersistent": 145 - }, - { - "target": 2198, - "persistent": 2052, - "nonpersistent": 146 - }, - { - "target": 2199, - "persistent": 2052, - "nonpersistent": 147 - }, - { - "target": 2200, - "persistent": 2052, - "nonpersistent": 148 - }, - { - "target": 2201, - "persistent": 2052, - "nonpersistent": 149 - }, - { - "target": 2202, - "persistent": 2052, - "nonpersistent": 150 - }, - { - "target": 2203, - "persistent": 2052, - "nonpersistent": 151 - }, - { - "target": 2204, - "persistent": 2052, - "nonpersistent": 152 - }, - { - "target": 2205, - "persistent": 2052, - "nonpersistent": 153 - }, - { - "target": 2206, - "persistent": 2052, - "nonpersistent": 154 - }, - { - "target": 2207, - "persistent": 2052, - "nonpersistent": 155 - }, - { - "target": 2208, - "persistent": 2052, - "nonpersistent": 156 - }, - { - "target": 2209, - "persistent": 2052, - "nonpersistent": 157 - }, - { - "target": 2210, - "persistent": 2052, - "nonpersistent": 158 - }, - { - "target": 2211, - "persistent": 2052, - "nonpersistent": 159 - }, - { - "target": 2212, - "persistent": 2052, - "nonpersistent": 160 - }, - { - "target": 2213, - "persistent": 2052, - "nonpersistent": 161 - }, - { - "target": 2214, - "persistent": 2052, - "nonpersistent": 162 - }, - { - "target": 2215, - "persistent": 2052, - "nonpersistent": 163 - }, - { - "target": 2216, - "persistent": 2052, - "nonpersistent": 164 - }, - { - "target": 2217, - "persistent": 2052, - "nonpersistent": 165 - }, - { - "target": 2218, - "persistent": 2052, - "nonpersistent": 166 - }, - { - "target": 2219, - "persistent": 2052, - "nonpersistent": 167 - }, - { - "target": 2220, - "persistent": 2052, - "nonpersistent": 168 - }, - { - "target": 2221, - "persistent": 2052, - "nonpersistent": 169 - }, - { - "target": 2222, - "persistent": 2052, - "nonpersistent": 170 - }, - { - "target": 2223, - "persistent": 2052, - "nonpersistent": 171 - }, - { - "target": 2224, - "persistent": 2052, - "nonpersistent": 172 - }, - { - "target": 2225, - "persistent": 2052, - "nonpersistent": 173 - }, - { - "target": 2226, - "persistent": 2052, - "nonpersistent": 174 - }, - { - "target": 2227, - "persistent": 2052, - "nonpersistent": 175 - }, - { - "target": 2228, - "persistent": 2052, - "nonpersistent": 176 - }, - { - "target": 2229, - "persistent": 2052, - "nonpersistent": 177 - }, - { - "target": 2230, - "persistent": 2052, - "nonpersistent": 178 - }, - { - "target": 2231, - "persistent": 2052, - "nonpersistent": 179 - }, - { - "target": 2232, - "persistent": 2052, - "nonpersistent": 180 - }, - { - "target": 2233, - "persistent": 2052, - "nonpersistent": 181 - }, - { - "target": 2234, - "persistent": 2052, - "nonpersistent": 182 - }, - { - "target": 2235, - "persistent": 2052, - "nonpersistent": 183 - }, - { - "target": 2236, - "persistent": 2052, - "nonpersistent": 184 - }, - { - "target": 2237, - "persistent": 2052, - "nonpersistent": 185 - }, - { - "target": 2238, - "persistent": 2052, - "nonpersistent": 186 - }, - { - "target": 2239, - "persistent": 2052, - "nonpersistent": 187 - }, - { - "target": 2240, - "persistent": 2052, - "nonpersistent": 188 - }, - { - "target": 2241, - "persistent": 2052, - "nonpersistent": 189 - }, - { - "target": 2242, - "persistent": 2052, - "nonpersistent": 190 - }, - { - "target": 2243, - "persistent": 2052, - "nonpersistent": 191 - }, - { - "target": 2244, - "persistent": 2052, - "nonpersistent": 192 - }, - { - "target": 2245, - "persistent": 2052, - "nonpersistent": 193 - }, - { - "target": 2246, - "persistent": 2052, - "nonpersistent": 194 - }, - { - "target": 2247, - "persistent": 2052, - "nonpersistent": 195 - }, - { - "target": 2248, - "persistent": 2052, - "nonpersistent": 196 - }, - { - "target": 2249, - "persistent": 2052, - "nonpersistent": 197 - }, - { - "target": 2250, - "persistent": 2052, - "nonpersistent": 198 - }, - { - "target": 2251, - "persistent": 2052, - "nonpersistent": 199 - }, - { - "target": 2252, - "persistent": 2052, - "nonpersistent": 200 - }, - { - "target": 2253, - "persistent": 2052, - "nonpersistent": 201 - }, - { - "target": 2254, - "persistent": 2052, - "nonpersistent": 202 - }, - { - "target": 2255, - "persistent": 2052, - "nonpersistent": 203 - }, - { - "target": 2256, - "persistent": 2052, - "nonpersistent": 204 - }, - { - "target": 2257, - "persistent": 2052, - "nonpersistent": 205 - }, - { - "target": 2258, - "persistent": 2052, - "nonpersistent": 206 - }, - { - "target": 2259, - "persistent": 2052, - "nonpersistent": 207 - }, - { - "target": 2260, - "persistent": 2052, - "nonpersistent": 208 - }, - { - "target": 2261, - "persistent": 2052, - "nonpersistent": 209 - }, - { - "target": 2262, - "persistent": 2052, - "nonpersistent": 210 - }, - { - "target": 2263, - "persistent": 2052, - "nonpersistent": 211 - }, - { - "target": 2264, - "persistent": 2052, - "nonpersistent": 212 - }, - { - "target": 2265, - "persistent": 2052, - "nonpersistent": 213 - }, - { - "target": 2266, - "persistent": 2052, - "nonpersistent": 214 - }, - { - "target": 2267, - "persistent": 2052, - "nonpersistent": 215 - }, - { - "target": 2268, - "persistent": 2052, - "nonpersistent": 216 - }, - { - "target": 2269, - "persistent": 2052, - "nonpersistent": 217 - }, - { - "target": 2270, - "persistent": 2052, - "nonpersistent": 218 - }, - { - "target": 2271, - "persistent": 2052, - "nonpersistent": 219 - }, - { - "target": 2272, - "persistent": 2052, - "nonpersistent": 220 - }, - { - "target": 2273, - "persistent": 2052, - "nonpersistent": 221 - }, - { - "target": 2274, - "persistent": 2052, - "nonpersistent": 222 - }, - { - "target": 2275, - "persistent": 2052, - "nonpersistent": 223 - }, - { - "target": 2276, - "persistent": 2052, - "nonpersistent": 224 - }, - { - "target": 2277, - "persistent": 2052, - "nonpersistent": 225 - }, - { - "target": 2278, - "persistent": 2052, - "nonpersistent": 226 - }, - { - "target": 2279, - "persistent": 2052, - "nonpersistent": 227 - }, - { - "target": 2280, - "persistent": 2052, - "nonpersistent": 228 - }, - { - "target": 2281, - "persistent": 2052, - "nonpersistent": 229 - }, - { - "target": 2282, - "persistent": 2052, - "nonpersistent": 230 - }, - { - "target": 2283, - "persistent": 2052, - "nonpersistent": 231 - }, - { - "target": 2284, - "persistent": 2052, - "nonpersistent": 232 - }, - { - "target": 2285, - "persistent": 2052, - "nonpersistent": 233 - }, - { - "target": 2286, - "persistent": 2052, - "nonpersistent": 234 - }, - { - "target": 2287, - "persistent": 2052, - "nonpersistent": 235 - }, - { - "target": 2288, - "persistent": 2052, - "nonpersistent": 236 - }, - { - "target": 2289, - "persistent": 2052, - "nonpersistent": 237 - }, - { - "target": 2290, - "persistent": 2052, - "nonpersistent": 238 - }, - { - "target": 2291, - "persistent": 2052, - "nonpersistent": 239 - }, - { - "target": 2292, - "persistent": 2052, - "nonpersistent": 240 - }, - { - "target": 2293, - "persistent": 2052, - "nonpersistent": 241 - }, - { - "target": 2294, - "persistent": 2052, - "nonpersistent": 242 - }, - { - "target": 2295, - "persistent": 2052, - "nonpersistent": 243 - }, - { - "target": 2296, - "persistent": 2052, - "nonpersistent": 244 - }, - { - "target": 2297, - "persistent": 2052, - "nonpersistent": 245 - }, - { - "target": 2298, - "persistent": 2052, - "nonpersistent": 246 - }, - { - "target": 2299, - "persistent": 2052, - "nonpersistent": 247 - }, - { - "target": 2300, - "persistent": 2052, - "nonpersistent": 248 - }, - { - "target": 2301, - "persistent": 2052, - "nonpersistent": 249 - }, - { - "target": 2302, - "persistent": 2052, - "nonpersistent": 250 - }, - { - "target": 2303, - "persistent": 2052, - "nonpersistent": 251 - }, - { - "target": 2304, - "persistent": 2052, - "nonpersistent": 252 - }, - { - "target": 2305, - "persistent": 2052, - "nonpersistent": 253 - }, - { - "target": 2306, - "persistent": 2052, - "nonpersistent": 254 - }, - { - "target": 2307, - "persistent": 2052, - "nonpersistent": 255 - }, - { - "target": 2308, - "persistent": 2052, - "nonpersistent": 256 - }, - { - "target": 2309, - "persistent": 2052, - "nonpersistent": 257 - }, - { - "target": 2310, - "persistent": 2052, - "nonpersistent": 258 - }, - { - "target": 2311, - "persistent": 2052, - "nonpersistent": 259 - }, - { - "target": 2312, - "persistent": 2052, - "nonpersistent": 260 - }, - { - "target": 2313, - "persistent": 2052, - "nonpersistent": 261 - }, - { - "target": 2314, - "persistent": 2052, - "nonpersistent": 262 - }, - { - "target": 2315, - "persistent": 2052, - "nonpersistent": 263 - }, - { - "target": 2316, - "persistent": 2052, - "nonpersistent": 264 - }, - { - "target": 2317, - "persistent": 2052, - "nonpersistent": 265 - }, - { - "target": 2318, - "persistent": 2052, - "nonpersistent": 266 - }, - { - "target": 2319, - "persistent": 2052, - "nonpersistent": 267 - }, - { - "target": 2320, - "persistent": 2052, - "nonpersistent": 268 - }, - { - "target": 2321, - "persistent": 2052, - "nonpersistent": 269 - }, - { - "target": 2322, - "persistent": 2052, - "nonpersistent": 270 - }, - { - "target": 2323, - "persistent": 2052, - "nonpersistent": 271 - }, - { - "target": 2324, - "persistent": 2052, - "nonpersistent": 272 - }, - { - "target": 2325, - "persistent": 2052, - "nonpersistent": 273 - }, - { - "target": 2326, - "persistent": 2052, - "nonpersistent": 274 - }, - { - "target": 2327, - "persistent": 2052, - "nonpersistent": 275 - }, - { - "target": 2328, - "persistent": 2052, - "nonpersistent": 276 - }, - { - "target": 2329, - "persistent": 2052, - "nonpersistent": 277 - }, - { - "target": 2330, - "persistent": 2052, - "nonpersistent": 278 - }, - { - "target": 2331, - "persistent": 2052, - "nonpersistent": 279 - }, - { - "target": 2332, - "persistent": 2052, - "nonpersistent": 280 - }, - { - "target": 2333, - "persistent": 2052, - "nonpersistent": 281 - }, - { - "target": 2334, - "persistent": 2052, - "nonpersistent": 282 - }, - { - "target": 2335, - "persistent": 2052, - "nonpersistent": 283 - }, - { - "target": 2336, - "persistent": 2052, - "nonpersistent": 284 - }, - { - "target": 2337, - "persistent": 2052, - "nonpersistent": 285 - }, - { - "target": 2338, - "persistent": 2052, - "nonpersistent": 286 - }, - { - "target": 2339, - "persistent": 2052, - "nonpersistent": 287 - }, - { - "target": 2340, - "persistent": 2052, - "nonpersistent": 288 - }, - { - "target": 2341, - "persistent": 2052, - "nonpersistent": 289 - }, - { - "target": 2342, - "persistent": 2052, - "nonpersistent": 290 - }, - { - "target": 2343, - "persistent": 2052, - "nonpersistent": 291 - }, - { - "target": 2344, - "persistent": 2052, - "nonpersistent": 292 - }, - { - "target": 2345, - "persistent": 2052, - "nonpersistent": 293 - }, - { - "target": 2346, - "persistent": 2052, - "nonpersistent": 294 - }, - { - "target": 2347, - "persistent": 2052, - "nonpersistent": 295 - }, - { - "target": 2348, - "persistent": 2052, - "nonpersistent": 296 - }, - { - "target": 2349, - "persistent": 2052, - "nonpersistent": 297 - }, - { - "target": 2350, - "persistent": 2052, - "nonpersistent": 298 - }, - { - "target": 2351, - "persistent": 2052, - "nonpersistent": 299 - }, - { - "target": 2352, - "persistent": 2052, - "nonpersistent": 300 - }, - { - "target": 2353, - "persistent": 2052, - "nonpersistent": 301 - }, - { - "target": 2354, - "persistent": 2052, - "nonpersistent": 302 - }, - { - "target": 2355, - "persistent": 2052, - "nonpersistent": 303 - }, - { - "target": 2356, - "persistent": 2052, - "nonpersistent": 304 - }, - { - "target": 2357, - "persistent": 2052, - "nonpersistent": 305 - }, - { - "target": 2358, - "persistent": 2052, - "nonpersistent": 306 - }, - { - "target": 2359, - "persistent": 2052, - "nonpersistent": 307 - }, - { - "target": 2360, - "persistent": 2052, - "nonpersistent": 308 - }, - { - "target": 2361, - "persistent": 2052, - "nonpersistent": 309 - }, - { - "target": 2362, - "persistent": 2052, - "nonpersistent": 310 - }, - { - "target": 2363, - "persistent": 2052, - "nonpersistent": 311 - }, - { - "target": 2364, - "persistent": 2052, - "nonpersistent": 312 - }, - { - "target": 2365, - "persistent": 2052, - "nonpersistent": 313 - }, - { - "target": 2366, - "persistent": 2052, - "nonpersistent": 314 - }, - { - "target": 2367, - "persistent": 2052, - "nonpersistent": 315 - }, - { - "target": 2368, - "persistent": 2052, - "nonpersistent": 316 - }, - { - "target": 2369, - "persistent": 2052, - "nonpersistent": 317 - }, - { - "target": 2370, - "persistent": 2052, - "nonpersistent": 318 - }, - { - "target": 2371, - "persistent": 2052, - "nonpersistent": 319 - }, - { - "target": 2372, - "persistent": 2052, - "nonpersistent": 320 - }, - { - "target": 2373, - "persistent": 2052, - "nonpersistent": 321 - }, - { - "target": 2374, - "persistent": 2052, - "nonpersistent": 322 - }, - { - "target": 2375, - "persistent": 2052, - "nonpersistent": 323 - }, - { - "target": 2376, - "persistent": 2052, - "nonpersistent": 324 - }, - { - "target": 2377, - "persistent": 2052, - "nonpersistent": 325 - }, - { - "target": 2378, - "persistent": 2052, - "nonpersistent": 326 - }, - { - "target": 2379, - "persistent": 2052, - "nonpersistent": 327 - }, - { - "target": 2380, - "persistent": 2052, - "nonpersistent": 328 - }, - { - "target": 2381, - "persistent": 2052, - "nonpersistent": 329 - }, - { - "target": 2382, - "persistent": 2052, - "nonpersistent": 330 - }, - { - "target": 2383, - "persistent": 2052, - "nonpersistent": 331 - }, - { - "target": 2384, - "persistent": 2052, - "nonpersistent": 332 - }, - { - "target": 2385, - "persistent": 2052, - "nonpersistent": 333 - }, - { - "target": 2386, - "persistent": 2052, - "nonpersistent": 334 - }, - { - "target": 2387, - "persistent": 2052, - "nonpersistent": 335 - }, - { - "target": 2388, - "persistent": 2052, - "nonpersistent": 336 - }, - { - "target": 2389, - "persistent": 2052, - "nonpersistent": 337 - }, - { - "target": 2390, - "persistent": 2052, - "nonpersistent": 338 - }, - { - "target": 2391, - "persistent": 2052, - "nonpersistent": 339 - }, - { - "target": 2392, - "persistent": 2052, - "nonpersistent": 340 - }, - { - "target": 2393, - "persistent": 2052, - "nonpersistent": 341 - }, - { - "target": 2394, - "persistent": 2052, - "nonpersistent": 342 - }, - { - "target": 2395, - "persistent": 2052, - "nonpersistent": 343 - }, - { - "target": 2396, - "persistent": 2052, - "nonpersistent": 344 - }, - { - "target": 2397, - "persistent": 2052, - "nonpersistent": 345 - }, - { - "target": 2398, - "persistent": 2052, - "nonpersistent": 346 - }, - { - "target": 2399, - "persistent": 2052, - "nonpersistent": 347 - }, - { - "target": 2400, - "persistent": 2052, - "nonpersistent": 348 - }, - { - "target": 2401, - "persistent": 2052, - "nonpersistent": 349 - }, - { - "target": 2402, - "persistent": 2052, - "nonpersistent": 350 - }, - { - "target": 2403, - "persistent": 2052, - "nonpersistent": 351 - }, - { - "target": 2404, - "persistent": 2052, - "nonpersistent": 352 - }, - { - "target": 2405, - "persistent": 2052, - "nonpersistent": 353 - }, - { - "target": 2406, - "persistent": 2052, - "nonpersistent": 354 - }, - { - "target": 2407, - "persistent": 2052, - "nonpersistent": 355 - }, - { - "target": 2408, - "persistent": 2052, - "nonpersistent": 356 - }, - { - "target": 2409, - "persistent": 2052, - "nonpersistent": 357 - }, - { - "target": 2410, - "persistent": 2052, - "nonpersistent": 358 - }, - { - "target": 2411, - "persistent": 2052, - "nonpersistent": 359 - }, - { - "target": 2412, - "persistent": 2052, - "nonpersistent": 360 - }, - { - "target": 2413, - "persistent": 2052, - "nonpersistent": 361 - }, - { - "target": 2414, - "persistent": 2052, - "nonpersistent": 362 - }, - { - "target": 2415, - "persistent": 2052, - "nonpersistent": 363 - }, - { - "target": 2416, - "persistent": 2052, - "nonpersistent": 364 - }, - { - "target": 2417, - "persistent": 2052, - "nonpersistent": 365 - }, - { - "target": 2418, - "persistent": 2052, - "nonpersistent": 366 - }, - { - "target": 2419, - "persistent": 2052, - "nonpersistent": 367 - }, - { - "target": 2420, - "persistent": 2052, - "nonpersistent": 368 - }, - { - "target": 2421, - "persistent": 2052, - "nonpersistent": 369 - }, - { - "target": 2422, - "persistent": 2052, - "nonpersistent": 370 - }, - { - "target": 2423, - "persistent": 2052, - "nonpersistent": 371 - }, - { - "target": 2424, - "persistent": 2052, - "nonpersistent": 372 - }, - { - "target": 2425, - "persistent": 2052, - "nonpersistent": 373 - }, - { - "target": 2426, - "persistent": 2052, - "nonpersistent": 374 - }, - { - "target": 2427, - "persistent": 2052, - "nonpersistent": 375 - }, - { - "target": 2428, - "persistent": 2052, - "nonpersistent": 376 - }, - { - "target": 2429, - "persistent": 2052, - "nonpersistent": 377 - }, - { - "target": 2430, - "persistent": 2052, - "nonpersistent": 378 - }, - { - "target": 2431, - "persistent": 2052, - "nonpersistent": 379 - }, - { - "target": 2432, - "persistent": 2052, - "nonpersistent": 380 - }, - { - "target": 2433, - "persistent": 2052, - "nonpersistent": 381 - }, - { - "target": 2434, - "persistent": 2052, - "nonpersistent": 382 - }, - { - "target": 2435, - "persistent": 2052, - "nonpersistent": 383 - }, - { - "target": 2436, - "persistent": 2052, - "nonpersistent": 384 - }, - { - "target": 2437, - "persistent": 2052, - "nonpersistent": 385 - }, - { - "target": 2438, - "persistent": 2052, - "nonpersistent": 386 - }, - { - "target": 2439, - "persistent": 2052, - "nonpersistent": 387 - }, - { - "target": 2440, - "persistent": 2052, - "nonpersistent": 388 - }, - { - "target": 2441, - "persistent": 2052, - "nonpersistent": 389 - }, - { - "target": 2442, - "persistent": 2052, - "nonpersistent": 390 - }, - { - "target": 2443, - "persistent": 2052, - "nonpersistent": 391 - }, - { - "target": 2444, - "persistent": 2052, - "nonpersistent": 392 - }, - { - "target": 2445, - "persistent": 2052, - "nonpersistent": 393 - }, - { - "target": 2446, - "persistent": 2052, - "nonpersistent": 394 - }, - { - "target": 2447, - "persistent": 2052, - "nonpersistent": 395 - }, - { - "target": 2448, - "persistent": 2052, - "nonpersistent": 396 - }, - { - "target": 2449, - "persistent": 2052, - "nonpersistent": 397 - }, - { - "target": 2450, - "persistent": 2052, - "nonpersistent": 398 - }, - { - "target": 2451, - "persistent": 2052, - "nonpersistent": 399 - }, - { - "target": 2452, - "persistent": 2052, - "nonpersistent": 400 - }, - { - "target": 2453, - "persistent": 2052, - "nonpersistent": 401 - }, - { - "target": 2454, - "persistent": 2052, - "nonpersistent": 402 - }, - { - "target": 2455, - "persistent": 2052, - "nonpersistent": 403 - }, - { - "target": 2456, - "persistent": 2052, - "nonpersistent": 404 - }, - { - "target": 2457, - "persistent": 2052, - "nonpersistent": 405 - }, - { - "target": 2458, - "persistent": 2052, - "nonpersistent": 406 - }, - { - "target": 2459, - "persistent": 2052, - "nonpersistent": 407 - }, - { - "target": 2460, - "persistent": 2052, - "nonpersistent": 408 - }, - { - "target": 2461, - "persistent": 2052, - "nonpersistent": 409 - }, - { - "target": 2462, - "persistent": 2052, - "nonpersistent": 410 - }, - { - "target": 2463, - "persistent": 2052, - "nonpersistent": 411 - }, - { - "target": 2464, - "persistent": 2052, - "nonpersistent": 412 - }, - { - "target": 2465, - "persistent": 2052, - "nonpersistent": 413 - }, - { - "target": 2466, - "persistent": 2052, - "nonpersistent": 414 - }, - { - "target": 2467, - "persistent": 2052, - "nonpersistent": 415 - }, - { - "target": 2468, - "persistent": 2052, - "nonpersistent": 416 - }, - { - "target": 2469, - "persistent": 2052, - "nonpersistent": 417 - }, - { - "target": 2470, - "persistent": 2052, - "nonpersistent": 418 - }, - { - "target": 2471, - "persistent": 2052, - "nonpersistent": 419 - }, - { - "target": 2472, - "persistent": 2052, - "nonpersistent": 420 - }, - { - "target": 2473, - "persistent": 2052, - "nonpersistent": 421 - }, - { - "target": 2474, - "persistent": 2052, - "nonpersistent": 422 - }, - { - "target": 2475, - "persistent": 2052, - "nonpersistent": 423 - }, - { - "target": 2476, - "persistent": 2052, - "nonpersistent": 424 - }, - { - "target": 2477, - "persistent": 2052, - "nonpersistent": 425 - }, - { - "target": 2478, - "persistent": 2052, - "nonpersistent": 426 - }, - { - "target": 2479, - "persistent": 2052, - "nonpersistent": 427 - }, - { - "target": 2480, - "persistent": 2052, - "nonpersistent": 428 - }, - { - "target": 2481, - "persistent": 2052, - "nonpersistent": 429 - }, - { - "target": 2482, - "persistent": 2052, - "nonpersistent": 430 - }, - { - "target": 2483, - "persistent": 2052, - "nonpersistent": 431 - }, - { - "target": 2484, - "persistent": 2052, - "nonpersistent": 432 - }, - { - "target": 2485, - "persistent": 2052, - "nonpersistent": 433 - }, - { - "target": 2486, - "persistent": 2052, - "nonpersistent": 434 - }, - { - "target": 2487, - "persistent": 2052, - "nonpersistent": 435 - }, - { - "target": 2488, - "persistent": 2052, - "nonpersistent": 436 - }, - { - "target": 2489, - "persistent": 2052, - "nonpersistent": 437 - }, - { - "target": 2490, - "persistent": 2052, - "nonpersistent": 438 - }, - { - "target": 2491, - "persistent": 2052, - "nonpersistent": 439 - }, - { - "target": 2492, - "persistent": 2052, - "nonpersistent": 440 - }, - { - "target": 2493, - "persistent": 2052, - "nonpersistent": 441 - }, - { - "target": 2494, - "persistent": 2052, - "nonpersistent": 442 - }, - { - "target": 2495, - "persistent": 2052, - "nonpersistent": 443 - }, - { - "target": 2496, - "persistent": 2052, - "nonpersistent": 444 - }, - { - "target": 2497, - "persistent": 2052, - "nonpersistent": 445 - }, - { - "target": 2498, - "persistent": 2052, - "nonpersistent": 446 - }, - { - "target": 2499, - "persistent": 2052, - "nonpersistent": 447 - }, - { - "target": 2500, - "persistent": 2052, - "nonpersistent": 448 - }, - { - "target": 2501, - "persistent": 2052, - "nonpersistent": 449 - }, - { - "target": 2502, - "persistent": 2052, - "nonpersistent": 450 - }, - { - "target": 2503, - "persistent": 2052, - "nonpersistent": 451 - }, - { - "target": 2504, - "persistent": 2052, - "nonpersistent": 452 - }, - { - "target": 2505, - "persistent": 2052, - "nonpersistent": 453 - }, - { - "target": 2506, - "persistent": 2052, - "nonpersistent": 454 - }, - { - "target": 2507, - "persistent": 2052, - "nonpersistent": 455 - }, - { - "target": 2508, - "persistent": 2052, - "nonpersistent": 456 - }, - { - "target": 2509, - "persistent": 2052, - "nonpersistent": 457 - }, - { - "target": 2510, - "persistent": 2052, - "nonpersistent": 458 - }, - { - "target": 2511, - "persistent": 2052, - "nonpersistent": 459 - }, - { - "target": 2512, - "persistent": 2052, - "nonpersistent": 460 - }, - { - "target": 2513, - "persistent": 2052, - "nonpersistent": 461 - }, - { - "target": 2514, - "persistent": 2052, - "nonpersistent": 462 - }, - { - "target": 2515, - "persistent": 2052, - "nonpersistent": 463 - }, - { - "target": 2516, - "persistent": 2052, - "nonpersistent": 464 - }, - { - "target": 2517, - "persistent": 2052, - "nonpersistent": 465 - }, - { - "target": 2518, - "persistent": 2052, - "nonpersistent": 466 - }, - { - "target": 2519, - "persistent": 2052, - "nonpersistent": 467 - }, - { - "target": 2520, - "persistent": 2052, - "nonpersistent": 468 - }, - { - "target": 2521, - "persistent": 2052, - "nonpersistent": 469 - }, - { - "target": 2522, - "persistent": 2052, - "nonpersistent": 470 - }, - { - "target": 2523, - "persistent": 2052, - "nonpersistent": 471 - }, - { - "target": 2524, - "persistent": 2052, - "nonpersistent": 472 - }, - { - "target": 2525, - "persistent": 2052, - "nonpersistent": 473 - }, - { - "target": 2526, - "persistent": 2052, - "nonpersistent": 474 - }, - { - "target": 2527, - "persistent": 2052, - "nonpersistent": 475 - }, - { - "target": 2528, - "persistent": 2052, - "nonpersistent": 476 - }, - { - "target": 2529, - "persistent": 2052, - "nonpersistent": 477 - }, - { - "target": 2530, - "persistent": 2052, - "nonpersistent": 478 - }, - { - "target": 2531, - "persistent": 2052, - "nonpersistent": 479 - }, - { - "target": 2532, - "persistent": 2052, - "nonpersistent": 480 - }, - { - "target": 2533, - "persistent": 2052, - "nonpersistent": 481 - }, - { - "target": 2534, - "persistent": 2052, - "nonpersistent": 482 - }, - { - "target": 2535, - "persistent": 2052, - "nonpersistent": 483 - }, - { - "target": 2536, - "persistent": 2052, - "nonpersistent": 484 - }, - { - "target": 2537, - "persistent": 2052, - "nonpersistent": 485 - }, - { - "target": 2538, - "persistent": 2052, - "nonpersistent": 486 - }, - { - "target": 2539, - "persistent": 2052, - "nonpersistent": 487 - }, - { - "target": 2540, - "persistent": 2052, - "nonpersistent": 488 - }, - { - "target": 2541, - "persistent": 2052, - "nonpersistent": 489 - }, - { - "target": 2542, - "persistent": 2052, - "nonpersistent": 490 - }, - { - "target": 2543, - "persistent": 2052, - "nonpersistent": 491 - }, - { - "target": 2544, - "persistent": 2052, - "nonpersistent": 492 - }, - { - "target": 2545, - "persistent": 2052, - "nonpersistent": 493 - }, - { - "target": 2546, - "persistent": 2052, - "nonpersistent": 494 - }, - { - "target": 2547, - "persistent": 2052, - "nonpersistent": 495 - }, - { - "target": 2548, - "persistent": 2052, - "nonpersistent": 496 - }, - { - "target": 2549, - "persistent": 2052, - "nonpersistent": 497 - }, - { - "target": 2550, - "persistent": 2052, - "nonpersistent": 498 - }, - { - "target": 2551, - "persistent": 2052, - "nonpersistent": 499 - }, - { - "target": 2552, - "persistent": 2052, - "nonpersistent": 500 - }, - { - "target": 2553, - "persistent": 2052, - "nonpersistent": 501 - }, - { - "target": 2554, - "persistent": 2052, - "nonpersistent": 502 - }, - { - "target": 2555, - "persistent": 2052, - "nonpersistent": 503 - }, - { - "target": 2556, - "persistent": 2052, - "nonpersistent": 504 - }, - { - "target": 2557, - "persistent": 2052, - "nonpersistent": 505 - }, - { - "target": 2558, - "persistent": 2052, - "nonpersistent": 506 - }, - { - "target": 2559, - "persistent": 2052, - "nonpersistent": 507 - }, - { - "target": 2560, - "persistent": 2052, - "nonpersistent": 508 - }, - { - "target": 2561, - "persistent": 2052, - "nonpersistent": 509 - }, - { - "target": 2562, - "persistent": 2052, - "nonpersistent": 510 - }, - { - "target": 2563, - "persistent": 2052, - "nonpersistent": 511 - }, - { - "target": 2564, - "persistent": 2052, - "nonpersistent": 512 - }, - { - "target": 2565, - "persistent": 2052, - "nonpersistent": 513 - }, - { - "target": 2566, - "persistent": 2052, - "nonpersistent": 514 - }, - { - "target": 2567, - "persistent": 2052, - "nonpersistent": 515 - }, - { - "target": 2568, - "persistent": 2052, - "nonpersistent": 516 - }, - { - "target": 2569, - "persistent": 2052, - "nonpersistent": 517 - }, - { - "target": 2570, - "persistent": 2052, - "nonpersistent": 518 - }, - { - "target": 2571, - "persistent": 2052, - "nonpersistent": 519 - }, - { - "target": 2572, - "persistent": 2052, - "nonpersistent": 520 - }, - { - "target": 2573, - "persistent": 2052, - "nonpersistent": 521 - }, - { - "target": 2574, - "persistent": 2052, - "nonpersistent": 522 - }, - { - "target": 2575, - "persistent": 2052, - "nonpersistent": 523 - }, - { - "target": 2576, - "persistent": 2052, - "nonpersistent": 524 - }, - { - "target": 2577, - "persistent": 2052, - "nonpersistent": 525 - }, - { - "target": 2578, - "persistent": 2052, - "nonpersistent": 526 - }, - { - "target": 2579, - "persistent": 2052, - "nonpersistent": 527 - }, - { - "target": 2580, - "persistent": 2052, - "nonpersistent": 528 - }, - { - "target": 2581, - "persistent": 2052, - "nonpersistent": 529 - }, - { - "target": 2582, - "persistent": 2052, - "nonpersistent": 530 - }, - { - "target": 2583, - "persistent": 2052, - "nonpersistent": 531 - }, - { - "target": 2584, - "persistent": 2052, - "nonpersistent": 532 - }, - { - "target": 2585, - "persistent": 2052, - "nonpersistent": 533 - }, - { - "target": 2586, - "persistent": 2052, - "nonpersistent": 534 - }, - { - "target": 2587, - "persistent": 2052, - "nonpersistent": 535 - }, - { - "target": 2588, - "persistent": 2052, - "nonpersistent": 536 - }, - { - "target": 2589, - "persistent": 2052, - "nonpersistent": 537 - }, - { - "target": 2590, - "persistent": 2052, - "nonpersistent": 538 - }, - { - "target": 2591, - "persistent": 2052, - "nonpersistent": 539 - }, - { - "target": 2592, - "persistent": 2052, - "nonpersistent": 540 - }, - { - "target": 2593, - "persistent": 2052, - "nonpersistent": 541 - }, - { - "target": 2594, - "persistent": 2052, - "nonpersistent": 542 - }, - { - "target": 2595, - "persistent": 2052, - "nonpersistent": 543 - }, - { - "target": 2596, - "persistent": 2052, - "nonpersistent": 544 - }, - { - "target": 2597, - "persistent": 2052, - "nonpersistent": 545 - }, - { - "target": 2598, - "persistent": 2052, - "nonpersistent": 546 - }, - { - "target": 2599, - "persistent": 2052, - "nonpersistent": 547 - }, - { - "target": 2600, - "persistent": 2052, - "nonpersistent": 548 - }, - { - "target": 2601, - "persistent": 2052, - "nonpersistent": 549 - }, - { - "target": 2602, - "persistent": 2052, - "nonpersistent": 550 - }, - { - "target": 2603, - "persistent": 2052, - "nonpersistent": 551 - }, - { - "target": 2604, - "persistent": 2052, - "nonpersistent": 552 - }, - { - "target": 2605, - "persistent": 2052, - "nonpersistent": 553 - }, - { - "target": 2606, - "persistent": 2052, - "nonpersistent": 554 - }, - { - "target": 2607, - "persistent": 2052, - "nonpersistent": 555 - }, - { - "target": 2608, - "persistent": 2052, - "nonpersistent": 556 - }, - { - "target": 2609, - "persistent": 2052, - "nonpersistent": 557 - }, - { - "target": 2610, - "persistent": 2052, - "nonpersistent": 558 - }, - { - "target": 2611, - "persistent": 2052, - "nonpersistent": 559 - }, - { - "target": 2612, - "persistent": 2052, - "nonpersistent": 560 - }, - { - "target": 2613, - "persistent": 2052, - "nonpersistent": 561 - }, - { - "target": 2614, - "persistent": 2052, - "nonpersistent": 562 - }, - { - "target": 2615, - "persistent": 2052, - "nonpersistent": 563 - }, - { - "target": 2616, - "persistent": 2052, - "nonpersistent": 564 - }, - { - "target": 2617, - "persistent": 2052, - "nonpersistent": 565 - }, - { - "target": 2618, - "persistent": 2052, - "nonpersistent": 566 - }, - { - "target": 2619, - "persistent": 2052, - "nonpersistent": 567 - }, - { - "target": 2620, - "persistent": 2052, - "nonpersistent": 568 - }, - { - "target": 2621, - "persistent": 2052, - "nonpersistent": 569 - }, - { - "target": 2622, - "persistent": 2052, - "nonpersistent": 570 - }, - { - "target": 2623, - "persistent": 2052, - "nonpersistent": 571 - }, - { - "target": 2624, - "persistent": 2052, - "nonpersistent": 572 - }, - { - "target": 2625, - "persistent": 2052, - "nonpersistent": 573 - }, - { - "target": 2626, - "persistent": 2052, - "nonpersistent": 574 - }, - { - "target": 2627, - "persistent": 2052, - "nonpersistent": 575 - }, - { - "target": 2628, - "persistent": 2052, - "nonpersistent": 576 - }, - { - "target": 2629, - "persistent": 2052, - "nonpersistent": 577 - }, - { - "target": 2630, - "persistent": 2052, - "nonpersistent": 578 - }, - { - "target": 2631, - "persistent": 2052, - "nonpersistent": 579 - }, - { - "target": 2632, - "persistent": 2052, - "nonpersistent": 580 - }, - { - "target": 2633, - "persistent": 2052, - "nonpersistent": 581 - }, - { - "target": 2634, - "persistent": 2052, - "nonpersistent": 582 - }, - { - "target": 2635, - "persistent": 2052, - "nonpersistent": 583 - }, - { - "target": 2636, - "persistent": 2052, - "nonpersistent": 584 - }, - { - "target": 2637, - "persistent": 2052, - "nonpersistent": 585 - }, - { - "target": 2638, - "persistent": 2052, - "nonpersistent": 586 - }, - { - "target": 2639, - "persistent": 2052, - "nonpersistent": 587 - }, - { - "target": 2640, - "persistent": 2052, - "nonpersistent": 588 - }, - { - "target": 2641, - "persistent": 2052, - "nonpersistent": 589 - }, - { - "target": 2642, - "persistent": 2052, - "nonpersistent": 590 - }, - { - "target": 2643, - "persistent": 2052, - "nonpersistent": 591 - }, - { - "target": 2644, - "persistent": 2052, - "nonpersistent": 592 - }, - { - "target": 2645, - "persistent": 2052, - "nonpersistent": 593 - }, - { - "target": 2646, - "persistent": 2052, - "nonpersistent": 594 - }, - { - "target": 2647, - "persistent": 2052, - "nonpersistent": 595 - }, - { - "target": 2648, - "persistent": 2052, - "nonpersistent": 596 - }, - { - "target": 2649, - "persistent": 2052, - "nonpersistent": 597 - }, - { - "target": 2650, - "persistent": 2052, - "nonpersistent": 598 - }, - { - "target": 2651, - "persistent": 2052, - "nonpersistent": 599 - }, - { - "target": 2652, - "persistent": 2052, - "nonpersistent": 600 - }, - { - "target": 2653, - "persistent": 2052, - "nonpersistent": 601 - }, - { - "target": 2654, - "persistent": 2052, - "nonpersistent": 602 - }, - { - "target": 2655, - "persistent": 2052, - "nonpersistent": 603 - }, - { - "target": 2656, - "persistent": 2052, - "nonpersistent": 604 - }, - { - "target": 2657, - "persistent": 2052, - "nonpersistent": 605 - }, - { - "target": 2658, - "persistent": 2052, - "nonpersistent": 606 - }, - { - "target": 2659, - "persistent": 2052, - "nonpersistent": 607 - }, - { - "target": 2660, - "persistent": 2052, - "nonpersistent": 608 - }, - { - "target": 2661, - "persistent": 2052, - "nonpersistent": 609 - }, - { - "target": 2662, - "persistent": 2052, - "nonpersistent": 610 - }, - { - "target": 2663, - "persistent": 2052, - "nonpersistent": 611 - }, - { - "target": 2664, - "persistent": 2052, - "nonpersistent": 612 - }, - { - "target": 2665, - "persistent": 2052, - "nonpersistent": 613 - }, - { - "target": 2666, - "persistent": 2052, - "nonpersistent": 614 - }, - { - "target": 2667, - "persistent": 2052, - "nonpersistent": 615 - }, - { - "target": 2668, - "persistent": 2052, - "nonpersistent": 616 - }, - { - "target": 2669, - "persistent": 2052, - "nonpersistent": 617 - }, - { - "target": 2670, - "persistent": 2052, - "nonpersistent": 618 - }, - { - "target": 2671, - "persistent": 2052, - "nonpersistent": 619 - }, - { - "target": 2672, - "persistent": 2052, - "nonpersistent": 620 - }, - { - "target": 2673, - "persistent": 2052, - "nonpersistent": 621 - }, - { - "target": 2674, - "persistent": 2052, - "nonpersistent": 622 - }, - { - "target": 2675, - "persistent": 2052, - "nonpersistent": 623 - }, - { - "target": 2676, - "persistent": 2052, - "nonpersistent": 624 - }, - { - "target": 2677, - "persistent": 2052, - "nonpersistent": 625 - }, - { - "target": 2678, - "persistent": 2052, - "nonpersistent": 626 - }, - { - "target": 2679, - "persistent": 2052, - "nonpersistent": 627 - }, - { - "target": 2680, - "persistent": 2052, - "nonpersistent": 628 - }, - { - "target": 2681, - "persistent": 2052, - "nonpersistent": 629 - }, - { - "target": 2682, - "persistent": 2052, - "nonpersistent": 630 - }, - { - "target": 2683, - "persistent": 2052, - "nonpersistent": 631 - }, - { - "target": 2684, - "persistent": 2052, - "nonpersistent": 632 - }, - { - "target": 2685, - "persistent": 2052, - "nonpersistent": 633 - }, - { - "target": 2686, - "persistent": 2052, - "nonpersistent": 634 - }, - { - "target": 2687, - "persistent": 2052, - "nonpersistent": 635 - }, - { - "target": 2688, - "persistent": 2052, - "nonpersistent": 636 - }, - { - "target": 2689, - "persistent": 2052, - "nonpersistent": 637 - }, - { - "target": 2690, - "persistent": 2052, - "nonpersistent": 638 - }, - { - "target": 2691, - "persistent": 2052, - "nonpersistent": 639 - }, - { - "target": 2692, - "persistent": 2052, - "nonpersistent": 640 - }, - { - "target": 2693, - "persistent": 2052, - "nonpersistent": 641 - }, - { - "target": 2694, - "persistent": 2052, - "nonpersistent": 642 - }, - { - "target": 2695, - "persistent": 2052, - "nonpersistent": 643 - }, - { - "target": 2696, - "persistent": 2052, - "nonpersistent": 644 - }, - { - "target": 2697, - "persistent": 2052, - "nonpersistent": 645 - }, - { - "target": 2698, - "persistent": 2052, - "nonpersistent": 646 - }, - { - "target": 2699, - "persistent": 2052, - "nonpersistent": 647 - }, - { - "target": 2700, - "persistent": 2052, - "nonpersistent": 648 - }, - { - "target": 2701, - "persistent": 2052, - "nonpersistent": 649 - }, - { - "target": 2702, - "persistent": 2052, - "nonpersistent": 650 - }, - { - "target": 2703, - "persistent": 2052, - "nonpersistent": 651 - }, - { - "target": 2704, - "persistent": 2052, - "nonpersistent": 652 - }, - { - "target": 2705, - "persistent": 2052, - "nonpersistent": 653 - }, - { - "target": 2706, - "persistent": 2052, - "nonpersistent": 654 - }, - { - "target": 2707, - "persistent": 2052, - "nonpersistent": 655 - }, - { - "target": 2708, - "persistent": 2052, - "nonpersistent": 656 - }, - { - "target": 2709, - "persistent": 2052, - "nonpersistent": 657 - }, - { - "target": 2710, - "persistent": 2052, - "nonpersistent": 658 - }, - { - "target": 2711, - "persistent": 2052, - "nonpersistent": 659 - }, - { - "target": 2712, - "persistent": 2052, - "nonpersistent": 660 - }, - { - "target": 2713, - "persistent": 2052, - "nonpersistent": 661 - }, - { - "target": 2714, - "persistent": 2052, - "nonpersistent": 662 - }, - { - "target": 2715, - "persistent": 2052, - "nonpersistent": 663 - }, - { - "target": 2716, - "persistent": 2052, - "nonpersistent": 664 - }, - { - "target": 2717, - "persistent": 2052, - "nonpersistent": 665 - }, - { - "target": 2718, - "persistent": 2052, - "nonpersistent": 666 - }, - { - "target": 2719, - "persistent": 2052, - "nonpersistent": 667 - }, - { - "target": 2720, - "persistent": 2052, - "nonpersistent": 668 - }, - { - "target": 2721, - "persistent": 2052, - "nonpersistent": 669 - }, - { - "target": 2722, - "persistent": 2052, - "nonpersistent": 670 - }, - { - "target": 2723, - "persistent": 2052, - "nonpersistent": 671 - }, - { - "target": 2724, - "persistent": 2052, - "nonpersistent": 672 - }, - { - "target": 2725, - "persistent": 2052, - "nonpersistent": 673 - }, - { - "target": 2726, - "persistent": 2052, - "nonpersistent": 674 - }, - { - "target": 2727, - "persistent": 2052, - "nonpersistent": 675 - }, - { - "target": 2728, - "persistent": 2052, - "nonpersistent": 676 - }, - { - "target": 2729, - "persistent": 2052, - "nonpersistent": 677 - }, - { - "target": 2730, - "persistent": 2052, - "nonpersistent": 678 - }, - { - "target": 2731, - "persistent": 2052, - "nonpersistent": 679 - }, - { - "target": 2732, - "persistent": 2052, - "nonpersistent": 680 - }, - { - "target": 2733, - "persistent": 2052, - "nonpersistent": 681 - }, - { - "target": 2734, - "persistent": 2052, - "nonpersistent": 682 - }, - { - "target": 2735, - "persistent": 2052, - "nonpersistent": 683 - }, - { - "target": 2736, - "persistent": 2052, - "nonpersistent": 684 - }, - { - "target": 2737, - "persistent": 2052, - "nonpersistent": 685 - }, - { - "target": 2738, - "persistent": 2052, - "nonpersistent": 686 - }, - { - "target": 2739, - "persistent": 2052, - "nonpersistent": 687 - }, - { - "target": 2740, - "persistent": 2052, - "nonpersistent": 688 - }, - { - "target": 2741, - "persistent": 2052, - "nonpersistent": 689 - }, - { - "target": 2742, - "persistent": 2052, - "nonpersistent": 690 - }, - { - "target": 2743, - "persistent": 2052, - "nonpersistent": 691 - }, - { - "target": 2744, - "persistent": 2052, - "nonpersistent": 692 - }, - { - "target": 2745, - "persistent": 2052, - "nonpersistent": 693 - }, - { - "target": 2746, - "persistent": 2052, - "nonpersistent": 694 - }, - { - "target": 2747, - "persistent": 2052, - "nonpersistent": 695 - }, - { - "target": 2748, - "persistent": 2052, - "nonpersistent": 696 - }, - { - "target": 2749, - "persistent": 2052, - "nonpersistent": 697 - }, - { - "target": 2750, - "persistent": 2052, - "nonpersistent": 698 - }, - { - "target": 2751, - "persistent": 2052, - "nonpersistent": 699 - }, - { - "target": 2752, - "persistent": 2052, - "nonpersistent": 700 - }, - { - "target": 2753, - "persistent": 2052, - "nonpersistent": 701 - }, - { - "target": 2754, - "persistent": 2052, - "nonpersistent": 702 - }, - { - "target": 2755, - "persistent": 2052, - "nonpersistent": 703 - }, - { - "target": 2756, - "persistent": 2052, - "nonpersistent": 704 - }, - { - "target": 2757, - "persistent": 2052, - "nonpersistent": 705 - }, - { - "target": 2758, - "persistent": 2052, - "nonpersistent": 706 - }, - { - "target": 2759, - "persistent": 2052, - "nonpersistent": 707 - }, - { - "target": 2760, - "persistent": 2052, - "nonpersistent": 708 - }, - { - "target": 2761, - "persistent": 2052, - "nonpersistent": 709 - }, - { - "target": 2762, - "persistent": 2052, - "nonpersistent": 710 - }, - { - "target": 2763, - "persistent": 2052, - "nonpersistent": 711 - }, - { - "target": 2764, - "persistent": 2052, - "nonpersistent": 712 - }, - { - "target": 2765, - "persistent": 2052, - "nonpersistent": 713 - }, - { - "target": 2766, - "persistent": 2052, - "nonpersistent": 714 - }, - { - "target": 2767, - "persistent": 2052, - "nonpersistent": 715 - }, - { - "target": 2768, - "persistent": 2052, - "nonpersistent": 716 - }, - { - "target": 2769, - "persistent": 2052, - "nonpersistent": 717 - }, - { - "target": 2770, - "persistent": 2052, - "nonpersistent": 718 - }, - { - "target": 2771, - "persistent": 2052, - "nonpersistent": 719 - }, - { - "target": 2772, - "persistent": 2052, - "nonpersistent": 720 - }, - { - "target": 2773, - "persistent": 2052, - "nonpersistent": 721 - }, - { - "target": 2774, - "persistent": 2052, - "nonpersistent": 722 - }, - { - "target": 2775, - "persistent": 2052, - "nonpersistent": 723 - }, - { - "target": 2776, - "persistent": 2052, - "nonpersistent": 724 - }, - { - "target": 2777, - "persistent": 2052, - "nonpersistent": 725 - }, - { - "target": 2778, - "persistent": 2052, - "nonpersistent": 726 - }, - { - "target": 2779, - "persistent": 2052, - "nonpersistent": 727 - }, - { - "target": 2780, - "persistent": 2052, - "nonpersistent": 728 - }, - { - "target": 2781, - "persistent": 2052, - "nonpersistent": 729 - }, - { - "target": 2782, - "persistent": 2052, - "nonpersistent": 730 - }, - { - "target": 2783, - "persistent": 2052, - "nonpersistent": 731 - }, - { - "target": 2784, - "persistent": 2052, - "nonpersistent": 732 - }, - { - "target": 2785, - "persistent": 2052, - "nonpersistent": 733 - }, - { - "target": 2786, - "persistent": 2052, - "nonpersistent": 734 - }, - { - "target": 2787, - "persistent": 2052, - "nonpersistent": 735 - }, - { - "target": 2788, - "persistent": 2052, - "nonpersistent": 736 - }, - { - "target": 2789, - "persistent": 2052, - "nonpersistent": 737 - }, - { - "target": 2790, - "persistent": 2052, - "nonpersistent": 738 - }, - { - "target": 2791, - "persistent": 2052, - "nonpersistent": 739 - }, - { - "target": 2792, - "persistent": 2052, - "nonpersistent": 740 - }, - { - "target": 2793, - "persistent": 2052, - "nonpersistent": 741 - }, - { - "target": 2794, - "persistent": 2052, - "nonpersistent": 742 - }, - { - "target": 2795, - "persistent": 2052, - "nonpersistent": 743 - }, - { - "target": 2796, - "persistent": 2052, - "nonpersistent": 744 - }, - { - "target": 2797, - "persistent": 2052, - "nonpersistent": 745 - }, - { - "target": 2798, - "persistent": 2052, - "nonpersistent": 746 - }, - { - "target": 2799, - "persistent": 2052, - "nonpersistent": 747 - }, - { - "target": 2800, - "persistent": 2052, - "nonpersistent": 748 - }, - { - "target": 2801, - "persistent": 2052, - "nonpersistent": 749 - }, - { - "target": 2802, - "persistent": 2052, - "nonpersistent": 750 - }, - { - "target": 2803, - "persistent": 2052, - "nonpersistent": 751 - }, - { - "target": 2804, - "persistent": 2052, - "nonpersistent": 752 - }, - { - "target": 2805, - "persistent": 2052, - "nonpersistent": 753 - }, - { - "target": 2806, - "persistent": 2052, - "nonpersistent": 754 - }, - { - "target": 2807, - "persistent": 2052, - "nonpersistent": 755 - }, - { - "target": 2808, - "persistent": 2052, - "nonpersistent": 756 - }, - { - "target": 2809, - "persistent": 2052, - "nonpersistent": 757 - }, - { - "target": 2810, - "persistent": 2052, - "nonpersistent": 758 - }, - { - "target": 2811, - "persistent": 2052, - "nonpersistent": 759 - }, - { - "target": 2812, - "persistent": 2052, - "nonpersistent": 760 - }, - { - "target": 2813, - "persistent": 2052, - "nonpersistent": 761 - }, - { - "target": 2814, - "persistent": 2052, - "nonpersistent": 762 - }, - { - "target": 2815, - "persistent": 2052, - "nonpersistent": 763 - }, - { - "target": 2816, - "persistent": 2052, - "nonpersistent": 764 - }, - { - "target": 2817, - "persistent": 2052, - "nonpersistent": 765 - }, - { - "target": 2818, - "persistent": 2052, - "nonpersistent": 766 - }, - { - "target": 2819, - "persistent": 2052, - "nonpersistent": 767 - }, - { - "target": 2820, - "persistent": 2052, - "nonpersistent": 768 - }, - { - "target": 2821, - "persistent": 2052, - "nonpersistent": 769 - }, - { - "target": 2822, - "persistent": 2052, - "nonpersistent": 770 - }, - { - "target": 2823, - "persistent": 2052, - "nonpersistent": 771 - }, - { - "target": 2824, - "persistent": 2052, - "nonpersistent": 772 - }, - { - "target": 2825, - "persistent": 2052, - "nonpersistent": 773 - }, - { - "target": 2826, - "persistent": 2052, - "nonpersistent": 774 - }, - { - "target": 2827, - "persistent": 2052, - "nonpersistent": 775 - }, - { - "target": 2828, - "persistent": 2052, - "nonpersistent": 776 - }, - { - "target": 2829, - "persistent": 2052, - "nonpersistent": 777 - }, - { - "target": 2830, - "persistent": 2052, - "nonpersistent": 778 - }, - { - "target": 2831, - "persistent": 2052, - "nonpersistent": 779 - }, - { - "target": 2832, - "persistent": 2052, - "nonpersistent": 780 - }, - { - "target": 2833, - "persistent": 2052, - "nonpersistent": 781 - }, - { - "target": 2834, - "persistent": 2052, - "nonpersistent": 782 - }, - { - "target": 2835, - "persistent": 2052, - "nonpersistent": 783 - }, - { - "target": 2836, - "persistent": 2052, - "nonpersistent": 784 - }, - { - "target": 2837, - "persistent": 2052, - "nonpersistent": 785 - }, - { - "target": 2838, - "persistent": 2052, - "nonpersistent": 786 - }, - { - "target": 2839, - "persistent": 2052, - "nonpersistent": 787 - }, - { - "target": 2840, - "persistent": 2052, - "nonpersistent": 788 - }, - { - "target": 2841, - "persistent": 2052, - "nonpersistent": 789 - }, - { - "target": 2842, - "persistent": 2052, - "nonpersistent": 790 - }, - { - "target": 2843, - "persistent": 2052, - "nonpersistent": 791 - }, - { - "target": 2844, - "persistent": 2052, - "nonpersistent": 792 - }, - { - "target": 2845, - "persistent": 2052, - "nonpersistent": 793 - }, - { - "target": 2846, - "persistent": 2052, - "nonpersistent": 794 - }, - { - "target": 2847, - "persistent": 2052, - "nonpersistent": 795 - }, - { - "target": 2848, - "persistent": 2052, - "nonpersistent": 796 - }, - { - "target": 2849, - "persistent": 2052, - "nonpersistent": 797 - }, - { - "target": 2850, - "persistent": 2052, - "nonpersistent": 798 - }, - { - "target": 2851, - "persistent": 2052, - "nonpersistent": 799 - }, - { - "target": 2852, - "persistent": 2052, - "nonpersistent": 800 - }, - { - "target": 2853, - "persistent": 2052, - "nonpersistent": 801 - }, - { - "target": 2854, - "persistent": 2052, - "nonpersistent": 802 - }, - { - "target": 2855, - "persistent": 2052, - "nonpersistent": 803 - }, - { - "target": 2856, - "persistent": 2052, - "nonpersistent": 804 - }, - { - "target": 2857, - "persistent": 2052, - "nonpersistent": 805 - }, - { - "target": 2858, - "persistent": 2052, - "nonpersistent": 806 - }, - { - "target": 2859, - "persistent": 2052, - "nonpersistent": 807 - }, - { - "target": 2860, - "persistent": 2052, - "nonpersistent": 808 - }, - { - "target": 2861, - "persistent": 2052, - "nonpersistent": 809 - }, - { - "target": 2862, - "persistent": 2052, - "nonpersistent": 810 - }, - { - "target": 2863, - "persistent": 2052, - "nonpersistent": 811 - }, - { - "target": 2864, - "persistent": 2052, - "nonpersistent": 812 - }, - { - "target": 2865, - "persistent": 2052, - "nonpersistent": 813 - }, - { - "target": 2866, - "persistent": 2052, - "nonpersistent": 814 - }, - { - "target": 2867, - "persistent": 2052, - "nonpersistent": 815 - }, - { - "target": 2868, - "persistent": 2052, - "nonpersistent": 816 - }, - { - "target": 2869, - "persistent": 2052, - "nonpersistent": 817 - }, - { - "target": 2870, - "persistent": 2052, - "nonpersistent": 818 - }, - { - "target": 2871, - "persistent": 2052, - "nonpersistent": 819 - }, - { - "target": 2872, - "persistent": 2052, - "nonpersistent": 820 - }, - { - "target": 2873, - "persistent": 2052, - "nonpersistent": 821 - }, - { - "target": 2874, - "persistent": 2052, - "nonpersistent": 822 - }, - { - "target": 2875, - "persistent": 2052, - "nonpersistent": 823 - }, - { - "target": 2876, - "persistent": 2052, - "nonpersistent": 824 - }, - { - "target": 2877, - "persistent": 2052, - "nonpersistent": 825 - }, - { - "target": 2878, - "persistent": 2052, - "nonpersistent": 826 - }, - { - "target": 2879, - "persistent": 2052, - "nonpersistent": 827 - }, - { - "target": 2880, - "persistent": 2052, - "nonpersistent": 828 - }, - { - "target": 2881, - "persistent": 2052, - "nonpersistent": 829 - }, - { - "target": 2882, - "persistent": 2052, - "nonpersistent": 830 - }, - { - "target": 2883, - "persistent": 2052, - "nonpersistent": 831 - }, - { - "target": 2884, - "persistent": 2052, - "nonpersistent": 832 - }, - { - "target": 2885, - "persistent": 2052, - "nonpersistent": 833 - }, - { - "target": 2886, - "persistent": 2052, - "nonpersistent": 834 - }, - { - "target": 2887, - "persistent": 2052, - "nonpersistent": 835 - }, - { - "target": 2888, - "persistent": 2052, - "nonpersistent": 836 - }, - { - "target": 2889, - "persistent": 2052, - "nonpersistent": 837 - }, - { - "target": 2890, - "persistent": 2052, - "nonpersistent": 838 - }, - { - "target": 2891, - "persistent": 2052, - "nonpersistent": 839 - }, - { - "target": 2892, - "persistent": 2052, - "nonpersistent": 840 - }, - { - "target": 2893, - "persistent": 2052, - "nonpersistent": 841 - }, - { - "target": 2894, - "persistent": 2052, - "nonpersistent": 842 - }, - { - "target": 2895, - "persistent": 2052, - "nonpersistent": 843 - }, - { - "target": 2896, - "persistent": 2052, - "nonpersistent": 844 - }, - { - "target": 2897, - "persistent": 2052, - "nonpersistent": 845 - }, - { - "target": 2898, - "persistent": 2052, - "nonpersistent": 846 - }, - { - "target": 2899, - "persistent": 2052, - "nonpersistent": 847 - }, - { - "target": 2900, - "persistent": 2052, - "nonpersistent": 848 - }, - { - "target": 2901, - "persistent": 2052, - "nonpersistent": 849 - }, - { - "target": 2902, - "persistent": 2052, - "nonpersistent": 850 - }, - { - "target": 2903, - "persistent": 2052, - "nonpersistent": 851 - }, - { - "target": 2904, - "persistent": 2052, - "nonpersistent": 852 - }, - { - "target": 2905, - "persistent": 2052, - "nonpersistent": 853 - }, - { - "target": 2906, - "persistent": 2052, - "nonpersistent": 854 - }, - { - "target": 2907, - "persistent": 2052, - "nonpersistent": 855 - }, - { - "target": 2908, - "persistent": 2052, - "nonpersistent": 856 - }, - { - "target": 2909, - "persistent": 2052, - "nonpersistent": 857 - }, - { - "target": 2910, - "persistent": 2052, - "nonpersistent": 858 - }, - { - "target": 2911, - "persistent": 2052, - "nonpersistent": 859 - }, - { - "target": 2912, - "persistent": 2052, - "nonpersistent": 860 - }, - { - "target": 2913, - "persistent": 2052, - "nonpersistent": 861 - }, - { - "target": 2914, - "persistent": 2052, - "nonpersistent": 862 - }, - { - "target": 2915, - "persistent": 2052, - "nonpersistent": 863 - }, - { - "target": 2916, - "persistent": 2052, - "nonpersistent": 864 - }, - { - "target": 2917, - "persistent": 2052, - "nonpersistent": 865 - }, - { - "target": 2918, - "persistent": 2052, - "nonpersistent": 866 - }, - { - "target": 2919, - "persistent": 2052, - "nonpersistent": 867 - }, - { - "target": 2920, - "persistent": 2052, - "nonpersistent": 868 - }, - { - "target": 2921, - "persistent": 2052, - "nonpersistent": 869 - }, - { - "target": 2922, - "persistent": 2052, - "nonpersistent": 870 - }, - { - "target": 2923, - "persistent": 2052, - "nonpersistent": 871 - }, - { - "target": 2924, - "persistent": 2052, - "nonpersistent": 872 - }, - { - "target": 2925, - "persistent": 2052, - "nonpersistent": 873 - }, - { - "target": 2926, - "persistent": 2052, - "nonpersistent": 874 - }, - { - "target": 2927, - "persistent": 2052, - "nonpersistent": 875 - }, - { - "target": 2928, - "persistent": 2052, - "nonpersistent": 876 - }, - { - "target": 2929, - "persistent": 2052, - "nonpersistent": 877 - }, - { - "target": 2930, - "persistent": 2052, - "nonpersistent": 878 - }, - { - "target": 2931, - "persistent": 2052, - "nonpersistent": 879 - }, - { - "target": 2932, - "persistent": 2052, - "nonpersistent": 880 - }, - { - "target": 2933, - "persistent": 2052, - "nonpersistent": 881 - }, - { - "target": 2934, - "persistent": 2052, - "nonpersistent": 882 - }, - { - "target": 2935, - "persistent": 2052, - "nonpersistent": 883 - }, - { - "target": 2936, - "persistent": 2052, - "nonpersistent": 884 - }, - { - "target": 2937, - "persistent": 2052, - "nonpersistent": 885 - }, - { - "target": 2938, - "persistent": 2052, - "nonpersistent": 886 - }, - { - "target": 2939, - "persistent": 2052, - "nonpersistent": 887 - }, - { - "target": 2940, - "persistent": 2052, - "nonpersistent": 888 - }, - { - "target": 2941, - "persistent": 2052, - "nonpersistent": 889 - }, - { - "target": 2942, - "persistent": 2052, - "nonpersistent": 890 - }, - { - "target": 2943, - "persistent": 2052, - "nonpersistent": 891 - }, - { - "target": 2944, - "persistent": 2052, - "nonpersistent": 892 - }, - { - "target": 2945, - "persistent": 2052, - "nonpersistent": 893 - }, - { - "target": 2946, - "persistent": 2052, - "nonpersistent": 894 - }, - { - "target": 2947, - "persistent": 2052, - "nonpersistent": 895 - }, - { - "target": 2948, - "persistent": 2052, - "nonpersistent": 896 - }, - { - "target": 2949, - "persistent": 2052, - "nonpersistent": 897 - }, - { - "target": 2950, - "persistent": 2052, - "nonpersistent": 898 - }, - { - "target": 2951, - "persistent": 2052, - "nonpersistent": 899 - }, - { - "target": 2952, - "persistent": 2052, - "nonpersistent": 900 - }, - { - "target": 2953, - "persistent": 2052, - "nonpersistent": 901 - }, - { - "target": 2954, - "persistent": 2052, - "nonpersistent": 902 - }, - { - "target": 2955, - "persistent": 2052, - "nonpersistent": 903 - }, - { - "target": 2956, - "persistent": 2052, - "nonpersistent": 904 - }, - { - "target": 2957, - "persistent": 2052, - "nonpersistent": 905 - }, - { - "target": 2958, - "persistent": 2052, - "nonpersistent": 906 - }, - { - "target": 2959, - "persistent": 2052, - "nonpersistent": 907 - }, - { - "target": 2960, - "persistent": 2052, - "nonpersistent": 908 - }, - { - "target": 2961, - "persistent": 2052, - "nonpersistent": 909 - }, - { - "target": 2962, - "persistent": 2052, - "nonpersistent": 910 - }, - { - "target": 2963, - "persistent": 2052, - "nonpersistent": 911 - }, - { - "target": 2964, - "persistent": 2052, - "nonpersistent": 912 - }, - { - "target": 2965, - "persistent": 2052, - "nonpersistent": 913 - }, - { - "target": 2966, - "persistent": 2052, - "nonpersistent": 914 - }, - { - "target": 2967, - "persistent": 2052, - "nonpersistent": 915 - }, - { - "target": 2968, - "persistent": 2052, - "nonpersistent": 916 - }, - { - "target": 2969, - "persistent": 2052, - "nonpersistent": 917 - }, - { - "target": 2970, - "persistent": 2052, - "nonpersistent": 918 - }, - { - "target": 2971, - "persistent": 2052, - "nonpersistent": 919 - }, - { - "target": 2972, - "persistent": 2052, - "nonpersistent": 920 - }, - { - "target": 2973, - "persistent": 2052, - "nonpersistent": 921 - }, - { - "target": 2974, - "persistent": 2052, - "nonpersistent": 922 - }, - { - "target": 2975, - "persistent": 2052, - "nonpersistent": 923 - }, - { - "target": 2976, - "persistent": 2052, - "nonpersistent": 924 - }, - { - "target": 2977, - "persistent": 2052, - "nonpersistent": 925 - }, - { - "target": 2978, - "persistent": 2052, - "nonpersistent": 926 - }, - { - "target": 2979, - "persistent": 2052, - "nonpersistent": 927 - }, - { - "target": 2980, - "persistent": 2052, - "nonpersistent": 928 - }, - { - "target": 2981, - "persistent": 2052, - "nonpersistent": 929 - }, - { - "target": 2982, - "persistent": 2052, - "nonpersistent": 930 - }, - { - "target": 2983, - "persistent": 2052, - "nonpersistent": 931 - }, - { - "target": 2984, - "persistent": 2052, - "nonpersistent": 932 - }, - { - "target": 2985, - "persistent": 2052, - "nonpersistent": 933 - }, - { - "target": 2986, - "persistent": 2052, - "nonpersistent": 934 - }, - { - "target": 2987, - "persistent": 2052, - "nonpersistent": 935 - }, - { - "target": 2988, - "persistent": 2052, - "nonpersistent": 936 - }, - { - "target": 2989, - "persistent": 2052, - "nonpersistent": 937 - }, - { - "target": 2990, - "persistent": 2052, - "nonpersistent": 938 - }, - { - "target": 2991, - "persistent": 2052, - "nonpersistent": 939 - }, - { - "target": 2992, - "persistent": 2052, - "nonpersistent": 940 - }, - { - "target": 2993, - "persistent": 2052, - "nonpersistent": 941 - }, - { - "target": 2994, - "persistent": 2052, - "nonpersistent": 942 - }, - { - "target": 2995, - "persistent": 2052, - "nonpersistent": 943 - }, - { - "target": 2996, - "persistent": 2052, - "nonpersistent": 944 - }, - { - "target": 2997, - "persistent": 2052, - "nonpersistent": 945 - }, - { - "target": 2998, - "persistent": 2052, - "nonpersistent": 946 - }, - { - "target": 2999, - "persistent": 2052, - "nonpersistent": 947 - }, - { - "target": 3000, - "persistent": 2052, - "nonpersistent": 948 } ]