diff --git a/cabal.project b/cabal.project index c282783de5..4d03c67b7f 100644 --- a/cabal.project +++ b/cabal.project @@ -13,10 +13,8 @@ repository cardano-haskell-packages -- See CONTRIBUTING for some Nix commands you will need to run if you -- update either of these. index-state: - -- Bump this if you need newer packages from Hackage - , hackage.haskell.org 2026-05-26T19:31:06Z - -- Bump this if you need newer packages from CHaP - , cardano-haskell-packages 2026-06-29T12:05:19Z + , hackage.haskell.org 2026-07-15T21:58:35Z + , cardano-haskell-packages 2026-07-27T20:44:57Z -- TODO: needed to drop because newer cuddle on hackage would have not been seen -- active-repositories: @@ -45,6 +43,12 @@ if os (windows) constraints: bitvec -simd +-- TODO: bump packages instead of this +allow-newer: + , cardano-crypto-class +constraints: + , any.crypton < 1.1 + -- Points to ouroboros-network/leios-prototype source-repository-package type: git @@ -69,8 +73,8 @@ source-repository-package source-repository-package type: git location: https://github.com/IntersectMBO/cardano-ledger - tag: fb8d6f8a83b0efb86281e0b80e7ffba160dad8b1 - --sha256: sha256-U/vpyZNerL1YXiQg1TIAOL5L438NNE3XkuIYD0HPTJw= + tag: f3104f00f9819ba94de119c38bc3e0109982821f + --sha256: sha256-jkdxc8jmx1Paw+SxbQjPORBeeO2GVBeC6OGkP0vZwgc= subdir: libs/cardano-data libs/cardano-ledger-api @@ -97,3 +101,15 @@ source-repository-package subdir: kes-agent kes-agent-crypto + +-- cardano-base ch1bo/mk-committee (Leios committee/BLS + crypton memory revert). +source-repository-package + type: git + location: https://github.com/IntersectMBO/cardano-base + tag: 9c6078d9c39c542abcb5912d0d7c222b7ba8a03d + --sha256: sha256-FfzoMXitbX/pzEJDQExQikGef6FcantzGWvqu2g19ho= + subdir: + cardano-binary + cardano-crypto-class + cardano-crypto-leios + cardano-crypto-praos diff --git a/cardano-blueprint b/cardano-blueprint index 766cf4a739..4e18b1d0dc 160000 --- a/cardano-blueprint +++ b/cardano-blueprint @@ -1 +1 @@ -Subproject commit 766cf4a73925a1b5ff8e2473209d8deac18256bf +Subproject commit 4e18b1d0dcb57faf6091cd5847d2da95526b739b diff --git a/changelog.d/20260731_094653_sebastian.nagel_bls_voting_key.md b/changelog.d/20260731_094653_sebastian.nagel_bls_voting_key.md new file mode 100644 index 0000000000..a969cabb4e --- /dev/null +++ b/changelog.d/20260731_094653_sebastian.nagel_bls_voting_key.md @@ -0,0 +1,23 @@ + + +### Breaking + +- `PraosCanBeLeader` gained a `praosCanBeLeaderSignKeyBLS :: + Maybe (SignKeyDSIGN BLS12381MinSigDSIGN)` field, carrying the optional BLS + (Leios) signing key of a block producer. Code constructing `PraosCanBeLeader` + must set this field (`Nothing` when no BLS key is used). + +### Non-Breaking + +- Added `shelleyLeaderVotingKey`, projecting the BLS voting key out of a + `ShelleyLeaderCredentials`. + +- `topLevelConfigVotingKey` for the Cardano block is now taken from the + block-producer credentials (the BLS key supplied via `--shelley-bls-key`) + instead of being unsafely derived from cold-key material. Producers without a + BLS key now have `topLevelConfigVotingKey = Nothing` and do not vote. diff --git a/flake.lock b/flake.lock index ef6fd197f3..2af91cd63f 100644 --- a/flake.lock +++ b/flake.lock @@ -3,16 +3,16 @@ "CHaP": { "flake": false, "locked": { - "lastModified": 1782769557, - "narHash": "sha256-tCYMTwH15mNajHybvC3KGVPBPMlLlqIOYE0ZgcqRkJU=", + "lastModified": 1786144110, + "narHash": "sha256-7hIsWllt2gyPIZ8Wvb1BImf9xfQwDkz0B3sXIZpctxg=", "owner": "intersectmbo", "repo": "cardano-haskell-packages", - "rev": "3ee6b1ecce230d62f0083590f38c4ae3457da226", + "rev": "89f08ac683754f1746d5cd1ff4ea60ba13faec29", "type": "github" }, "original": { "owner": "intersectmbo", - "ref": "repo", + "ref": "index-only", "repo": "cardano-haskell-packages", "type": "github" } diff --git a/flake.nix b/flake.nix index 2f2c8d64a0..206f525773 100644 --- a/flake.nix +++ b/flake.nix @@ -21,7 +21,7 @@ flake = false; }; CHaP = { - url = "github:intersectmbo/cardano-haskell-packages?ref=repo"; + url = "github:intersectmbo/cardano-haskell-packages?ref=index-only"; flake = false; }; iohkNix = { @@ -47,7 +47,8 @@ flake = false; }; }; - outputs = inputs: + outputs = + inputs: let supportedSystems = [ "x86_64-linux" @@ -88,7 +89,10 @@ agda-spec = pkgs.agda-spec.shell; website = pkgs.mkShell { - packages = [ pkgs.nodejs pkgs.yarn ]; + packages = [ + pkgs.nodejs + pkgs.yarn + ]; }; }; inherit hydraJobs; diff --git a/ouroboros-consensus-cardano/src/byron/Ouroboros/Consensus/Byron/Crypto/DSIGN.hs b/ouroboros-consensus-cardano/src/byron/Ouroboros/Consensus/Byron/Crypto/DSIGN.hs index 2da173a883..cbdeb06793 100644 --- a/ouroboros-consensus-cardano/src/byron/Ouroboros/Consensus/Byron/Crypto/DSIGN.hs +++ b/ouroboros-consensus-cardano/src/byron/Ouroboros/Consensus/Byron/Crypto/DSIGN.hs @@ -19,6 +19,7 @@ module Ouroboros.Consensus.Byron.Crypto.DSIGN , VerKeyDSIGN (..) ) where +import Cardano.Binary.FixedSizeCodec (FixedSizeCodec (..)) import qualified Cardano.Chain.Block as CC.Block import qualified Cardano.Chain.UTxO as CC.UTxO import Cardano.Crypto @@ -43,7 +44,6 @@ import Data.Coerce (coerce) import Data.Proxy (Proxy (..)) import GHC.Generics (Generic) import NoThunks.Class (InspectHeapNamed (..), NoThunks) -import Ouroboros.Consensus.Util (eitherToMaybe) import Ouroboros.Consensus.Util.Condense class (HasSignTag a, Decoded a) => ByronSignable a @@ -68,9 +68,6 @@ data ByronDSIGN instance DSIGNAlgorithm ByronDSIGN where type SeedSizeDSIGN ByronDSIGN = 32 - type VerKeySizeDSIGN ByronDSIGN = 64 - type SignKeySizeDSIGN ByronDSIGN = 128 - type SigSizeDSIGN ByronDSIGN = 64 algorithmNameDSIGN _ = "ByronDSIGN" @@ -114,16 +111,23 @@ instance DSIGNAlgorithm ByronDSIGN where then Right () else Left "Verification failed" - rawSerialiseVerKeyDSIGN (VerKeyByronDSIGN (VerificationKey vk)) = CC.unXPub vk - rawSerialiseSignKeyDSIGN (SignKeyByronDSIGN (SigningKey sk)) = CC.unXPrv sk - rawSerialiseSigDSIGN (SigByronDSIGN (Signature sig)) = CC.unXSignature sig - - rawDeserialiseVerKeyDSIGN bs = - VerKeyByronDSIGN . VerificationKey <$> (eitherToMaybe $ CC.xpub bs) - rawDeserialiseSignKeyDSIGN bs = - SignKeyByronDSIGN . SigningKey <$> (eitherToMaybe $ CC.xprv bs) - rawDeserialiseSigDSIGN bs = - SigByronDSIGN . Signature <$> (eitherToMaybe $ CC.xsignature bs) +instance FixedSizeCodec (VerKeyDSIGN ByronDSIGN) where + type FixedSize (VerKeyDSIGN ByronDSIGN) = 64 + rawEncodeFixedSized (VerKeyByronDSIGN (VerificationKey vk)) = CC.unXPub vk + rawDecodeFixedSized bs = + VerKeyByronDSIGN . VerificationKey <$> either fail pure (CC.xpub bs) + +instance FixedSizeCodec (SignKeyDSIGN ByronDSIGN) where + type FixedSize (SignKeyDSIGN ByronDSIGN) = 128 + rawEncodeFixedSized (SignKeyByronDSIGN (SigningKey sk)) = CC.unXPrv sk + rawDecodeFixedSized bs = + SignKeyByronDSIGN . SigningKey <$> either fail pure (CC.xprv bs) + +instance FixedSizeCodec (SigDSIGN ByronDSIGN) where + type FixedSize (SigDSIGN ByronDSIGN) = 64 + rawEncodeFixedSized (SigByronDSIGN (Signature sig)) = CC.unXSignature sig + rawDecodeFixedSized bs = + SigByronDSIGN . Signature <$> either fail pure (CC.xsignature bs) instance Condense (SigDSIGN ByronDSIGN) where condense (SigByronDSIGN s) = show s diff --git a/ouroboros-consensus-cardano/src/ouroboros-consensus-cardano/Ouroboros/Consensus/Cardano/Node.hs b/ouroboros-consensus-cardano/src/ouroboros-consensus-cardano/Ouroboros/Consensus/Cardano/Node.hs index 3549de5377..f04589f9ef 100644 --- a/ouroboros-consensus-cardano/src/ouroboros-consensus-cardano/Ouroboros/Consensus/Cardano/Node.hs +++ b/ouroboros-consensus-cardano/src/ouroboros-consensus-cardano/Ouroboros/Consensus/Cardano/Node.hs @@ -46,22 +46,21 @@ module Ouroboros.Consensus.Cardano.Node import Cardano.Binary (DecoderError (..), enforceSize) import Cardano.Chain.Slotting (EpochSlots) -import Cardano.Crypto.DSIGN (DSIGNAlgorithm (rawDeserialiseSignKeyDSIGN)) -import Cardano.Crypto.Hash.Class (hashToBytes) import qualified Cardano.Ledger.Api.Era as L import qualified Cardano.Ledger.Api.Transition as L import qualified Cardano.Ledger.BaseTypes as SL -import Cardano.Ledger.Hashes (KeyHash (..), hashKey) import qualified Cardano.Ledger.Shelley.API as SL +import Cardano.Ledger.Shelley.LedgerState (NewEpochState, esSnapshotsL, nesEsL) +import Cardano.Ledger.State (ssStakeGoL, ssStakeMarkL, ssStakeSetL) import Cardano.Prelude (cborError) import qualified Cardano.Protocol.TPraos.OCert as Absolute (KESPeriod (..)) +import Cardano.Slotting.Slot (EpochNo (..)) import qualified Codec.CBOR.Decoding as CBOR import Codec.CBOR.Encoding (Encoding) import qualified Codec.CBOR.Encoding as CBOR import Codec.CBOR.Read (deserialiseFromBytes) import Control.Exception (assert) import qualified Control.Tracer as Tracer -import qualified Data.ByteString as BS import qualified Data.ByteString.Short as Short import Data.Functor.These (These1 (..)) import qualified Data.Map.Strict as Map @@ -73,7 +72,7 @@ import Data.SOP.OptNP (NonEmptyOptNP, OptNP (OptSkip)) import qualified Data.SOP.OptNP as OptNP import Data.SOP.Strict import Data.Word (Word16, Word64) -import Lens.Micro ((^.)) +import Lens.Micro ((&), (.~), (^.)) import Ouroboros.Consensus.Block import Ouroboros.Consensus.Byron.ByronHFC import Ouroboros.Consensus.Byron.Ledger (ByronBlock) @@ -115,7 +114,10 @@ import Ouroboros.Consensus.Shelley.Ledger.Block ) import Ouroboros.Consensus.Shelley.Ledger.NetworkProtocolVersion import Ouroboros.Consensus.Shelley.Node -import Ouroboros.Consensus.Shelley.Node.Common (shelleyBlockIssuerVKey) +import Ouroboros.Consensus.Shelley.Node.Common + ( shelleyBlockIssuerVKey + , shelleyLeaderVotingKey + ) import qualified Ouroboros.Consensus.Shelley.Node.Praos as Praos import qualified Ouroboros.Consensus.Shelley.Node.TPraos as TPraos import Ouroboros.Consensus.Storage.Serialisation @@ -502,6 +504,30 @@ toTriggerHardFork = \case CardanoTriggerHardForkAtEpoch epochNo -> TriggerHardForkAtEpoch epochNo +-- | Warm the initial stake snapshots for early-bootstrap (test) networks, so the +-- stake distribution -- and hence anything reading it at genesis, e.g. the Leios +-- committee via @nesPd@ -- is active from the first epochs instead of only after +-- the ~2-epoch snapshot pipeline has run. @ssStakeMark@ is already seeded from +-- genesis staking by the ledger's 'L.injectIntoTestState'; here we backfill +-- set\/go by how early the era boots. Only fires for +-- 'CardanoTriggerHardForkAtEpoch' (test-only; real networks use +-- 'CardanoTriggerHardForkAtDefaultVersion' and are left untouched): +-- +-- * hard fork at epoch 0 -> go = set = mark +-- * hard fork at epoch 1 -> set = mark +-- * hard fork at epoch >=2 -> unchanged +seedInitialStakeSnapshots :: + TriggerHardFork -> + NewEpochState era -> + NewEpochState era +seedInitialStakeSnapshots trigger nes = case trigger of + TriggerHardForkAtEpoch (EpochNo 0) -> nes & setSnap ssStakeSetL & setSnap ssStakeGoL + TriggerHardForkAtEpoch (EpochNo 1) -> nes & setSnap ssStakeSetL + _ -> nes + where + mark = nes ^. nesEsL . esSnapshotsL . ssStakeMarkL + setSnap l = (nesEsL . esSnapshotsL . l) .~ mark + newtype CardanoHardForkTriggers = CardanoHardForkTriggers { getCardanoHardForkTriggers :: NP CardanoHardForkTrigger (CardanoShelleyEras StandardCrypto) @@ -949,18 +975,13 @@ protocolInfoCardano (SomeHasFS hasFS) paramsCardano (Shelley.ShelleyStorageConfig praosSlotsPerKESPeriod k) (Shelley.ShelleyStorageConfig praosSlotsPerKESPeriod k) , topLevelConfigCheckpoints = cardanoCheckpoints - , -- FIXME: REMOVE THIS. Accesses and re-uses KES signing key material. - topLevelConfigVotingKey = do + , -- The Leios/Peras voting key comes from the block-producer credentials + -- (loaded from @--shelley-bls-key@). We vote with the first set of + -- credentials that carries a BLS key; 'Nothing' disables voting. + topLevelConfigVotingKey = case credssShelleyBased of [] -> Nothing - (c : _) -> - rawDeserialiseSignKeyDSIGN - -- Pad the 28 bytes of blake2b_224 to get 32 bytes for BLS - . (<> BS.pack (replicate 4 0)) - . hashToBytes - . unKeyHash - . hashKey - $ shelleyBlockIssuerVKey c + (c : _) -> shelleyLeaderVotingKey c } -- When the initial ledger state is not in the Byron era, register various @@ -989,15 +1010,34 @@ protocolInfoCardano (SomeHasFS hasFS) paramsCardano (CardanoEras c) perEraInjections = fn (Comp . pure) - :* hcmap (Proxy @IsShelleyBlock) shelleyInjection shelleyTcfgs + :* hczipWith + (Proxy @IsShelleyBlock) + (\(K trigger) -> shelleyInjection trigger) + perEraTriggers + shelleyTcfgs + + -- Crypto-erased triggers per Shelley-based era, so 'shelleyInjection' can + -- warm the initial stake snapshots on an early ('AtEpoch') bootstrap. A K-NP + -- avoids the @c ~ StandardCrypto@ mismatch of the raw 'CardanoHardForkTriggers'. + perEraTriggers :: NP (K TriggerHardFork) (CardanoShelleyEras c) + perEraTriggers = + K (toTriggerHardFork triggerHardForkShelley) + :* K (toTriggerHardFork triggerHardForkAllegra) + :* K (toTriggerHardFork triggerHardForkMary) + :* K (toTriggerHardFork triggerHardForkAlonzo) + :* K (toTriggerHardFork triggerHardForkBabbage) + :* K (toTriggerHardFork triggerHardForkConway) + :* K (toTriggerHardFork triggerHardForkDijkstra) + :* Nil shelleyInjection :: forall proto era. Shelley.ShelleyCompatible proto era => + TriggerHardFork -> WrapTransitionConfig (ShelleyBlock proto era) -> (Flip LedgerState ValuesMK -.-> (m :.: Flip LedgerState ValuesMK)) (ShelleyBlock proto era) - shelleyInjection (WrapTransitionConfig tcfg) = fn $ \(Flip stIn) -> Comp $ do + shelleyInjection trigger (WrapTransitionConfig tcfg) = fn $ \(Flip stIn) -> Comp $ do let stowed = stowLedgerTables stIn newNES <- L.injectIntoTestState @@ -1005,7 +1045,10 @@ protocolInfoCardano (SomeHasFS hasFS) paramsCardano tcfg (Shelley.shelleyLedgerState stowed) pure . Flip . unstowLedgerTables $ - stowed{Shelley.shelleyLedgerState = newNES} + stowed + { Shelley.shelleyLedgerState = + seedInitialStakeSnapshots trigger newNES + } shelleyTcfgs :: NP WrapTransitionConfig (CardanoShelleyEras c) shelleyTcfgs = diff --git a/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Ledger.hs b/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Ledger.hs index fb2e79cc8d..7b1349422b 100644 --- a/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Ledger.hs +++ b/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Ledger.hs @@ -61,8 +61,7 @@ module Ouroboros.Consensus.Shelley.Ledger.Ledger , BigEndianTxIn (..) ) where -import Cardano.Crypto.DSIGN (DSIGNAlgorithm (deriveVerKeyDSIGN), rawDeserialiseSignKeyDSIGN) -import Cardano.Crypto.Hash.Class (hashToBytes) +import Cardano.Crypto.Leios (mkLeiosCommittee) import Cardano.Ledger.BaseTypes (strictMaybeToMaybe) import qualified Cardano.Ledger.BaseTypes as SL (TxIx (..), epochInfoPure) import Cardano.Ledger.BaseTypes.NonZero (unNonZero) @@ -86,7 +85,6 @@ import qualified Cardano.Ledger.Block as Core import qualified Cardano.Ledger.Block as SL import Cardano.Ledger.Core ( Era - , KeyHash (..) , eraDecoder , ppMaxBHSizeL , ppMaxTxSizeL @@ -95,7 +93,14 @@ import qualified Cardano.Ledger.Core as Core import qualified Cardano.Ledger.Shelley.API as SL import qualified Cardano.Ledger.Shelley.Governance as SL import qualified Cardano.Ledger.Shelley.LedgerState as SL -import Cardano.Ledger.State (individualPoolStake, poolDistrDistrL) +import Cardano.Ledger.State + ( LeiosKey (..) + , LeiosPossessionProof (..) + , LeiosPubKey (..) + , individualPoolStake + , individualPoolStakeBls + , poolDistrDistrL + ) import qualified Cardano.Ledger.State as SL import Cardano.Slotting.EpochInfo import Codec.CBOR.Decoding (Decoder) @@ -107,19 +112,18 @@ import Control.Arrow (left, second) import qualified Control.Exception as Exception import Control.Monad.Except import qualified Control.State.Transition.Extended as STS -import qualified Data.ByteString as BS import Data.Coerce import Data.Foldable (toList) import Data.Functor.Identity import qualified Data.Map as Map -import Data.Maybe (fromJust) import Data.Maybe.Strict (StrictMaybe (..), maybeToStrictMaybe) import Data.MemPack import qualified Data.Text as T import qualified Data.Text as Text +import qualified Data.Vector.Strict as V import Data.Word import GHC.Generics (Generic) -import LeiosDemoTypes (mkCommitteeEveryoneVotes) +import LeiosDemoTypes (committeeStakeCoverage, selectCommitteeByStake) import LeiosVoting (HasLeiosVoting (..)) import Lens.Micro import Lens.Micro.Extras (view) @@ -1004,23 +1008,27 @@ instance HasLeiosVoting (ShelleyBlock (Praos c) DijkstraEra) where getLeiosCommittee ls = Just everyoneVotes where - -- TODO: stake-based scheme and move to era boundary (to cache computation) + -- Every pool in the (snapshotted) stake distribution gets a committee seat + -- weighted by its stake fraction; a pool that has not registered a Leios + -- key gets a keyless seat (an invalid proof of possession is dropped to + -- keyless by 'mkLeiosCommittee'). + -- + -- Weights are the raw stake fractions and are deliberately NOT normalised + -- as we have fractions of active stake already in the + -- 'individualPoolStake'. + -- + -- TODO: Move this to the era boundary (to cache the computation). everyoneVotes = - mkCommitteeEveryoneVotes - [ (vk, stake) - | (poolId, ips) <- Map.toList stakeDistribution - , let vk = deriveVerKeyDSIGN $ unsafeDeriveSigningKey poolId - stake = individualPoolStake ips - ] + mkLeiosCommittee . V.fromList $ + selectCommitteeByStake + committeeStakeCoverage -- TODO: take from pparams + [ (seatKey ips, ips.individualPoolStake) + | ips <- Map.elems stakeDistribution + ] + + seatKey ips = case ips.individualPoolStakeBls of + SJust lk -> SJust (unLeiosPubKey lk.leiosPubKey, unLeiosPossessionProof lk.leiosPossessionProof) + SNothing -> SNothing stakeDistribution = ls.shelleyLedgerState.nesPd ^. poolDistrDistrL - - -- FIXME: REMOVE THIS. Interprets cold key hashes as signing keys - unsafeDeriveSigningKey = - fromJust - . rawDeserialiseSignKeyDSIGN - -- Pad the 28 bytes of blake2b_224 to get 32 bytes for BLS - . (<> BS.pack (replicate 4 0)) - . hashToBytes - . unKeyHash diff --git a/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Mempool.hs b/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Mempool.hs index 239a484cfb..06c58cd200 100644 --- a/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Mempool.hs +++ b/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Mempool.hs @@ -753,7 +753,7 @@ leiosEndorserBlockMeasure st = DijkstraMeasure { conwayMeasure = conway - { alonzoMeasure = alonzo{byteSize = IgnoringOverflow Leios.leiosEBMaxClosureSize} + { alonzoMeasure = alonzo{byteSize = IgnoringOverflow Leios.maxEBClosureSize} , refScriptsSize = IgnoringOverflow $ ByteSize32 (pparams ^. SL.ppMaxRefScriptSizePerBlockG) diff --git a/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Node/Common.hs b/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Node/Common.hs index 6cd5b3f7a5..5ab8b24d5b 100644 --- a/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Node/Common.hs +++ b/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Node/Common.hs @@ -17,8 +17,10 @@ module Ouroboros.Consensus.Shelley.Node.Common , ShelleyEraWithCrypto , ShelleyLeaderCredentials (..) , shelleyBlockIssuerVKey + , shelleyLeaderVotingKey ) where +import Cardano.Crypto.Leios (LeiosSigningKey) import Cardano.Ledger.BaseTypes (unNonZero) import qualified Cardano.Ledger.Keys as SL import qualified Cardano.Ledger.Shelley.API as SL @@ -35,7 +37,7 @@ import Ouroboros.Consensus.Ledger.SupportsMempool (TxLimits) import Ouroboros.Consensus.Node.InitStorage import qualified Ouroboros.Consensus.Protocol.Ledger.HotKey as HotKey import Ouroboros.Consensus.Protocol.Praos.Common - ( PraosCanBeLeader (praosCanBeLeaderColdVerKey) + ( PraosCanBeLeader (praosCanBeLeaderColdVerKey, praosCanBeLeaderSignKeyBLS) ) import Ouroboros.Consensus.Shelley.Ledger ( ShelleyBlock @@ -69,6 +71,14 @@ shelleyBlockIssuerVKey :: shelleyBlockIssuerVKey = praosCanBeLeaderColdVerKey . shelleyLeaderCredentialsCanBeLeader +-- | The BLS voting signing key for this set of credentials, if one was provided +-- (via @--shelley-bls-key@). Used as the Leios/Peras voting key; 'Nothing' +-- disables voting for this node. +shelleyLeaderVotingKey :: + ShelleyLeaderCredentials c -> Maybe LeiosSigningKey +shelleyLeaderVotingKey = + praosCanBeLeaderSignKeyBLS . shelleyLeaderCredentialsCanBeLeader + {------------------------------------------------------------------------------- BlockForging -------------------------------------------------------------------------------} diff --git a/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Node/Protocol/Shelley.hs b/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Node/Protocol/Shelley.hs index 5a7b56d658..dd4df1edc1 100644 --- a/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Node/Protocol/Shelley.hs +++ b/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Node/Protocol/Shelley.hs @@ -234,6 +234,7 @@ mkPraosLeaderCredentials PraosCanBeLeader { praosCanBeLeaderColdVerKey = coerceKeyRole vkey , praosCanBeLeaderSignKeyVRF = vrfKey + , praosCanBeLeaderSignKeyBLS = Nothing , praosCanBeLeaderCredentialsSource = PraosCredentialsUnsound opcert kesKey } , shelleyLeaderCredentialsLabel = "Shelley" diff --git a/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs b/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs index bcac4ab40e..78f0927b5b 100644 --- a/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs +++ b/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs @@ -31,6 +31,7 @@ import qualified Data.Aeson as Json import qualified Data.ByteString.Lazy as LBS import qualified Data.Map as Map import Data.Maybe (fromJust) +import Data.Maybe.Strict (StrictMaybe (SNothing)) import Ouroboros.Consensus.Block (validateView) import Ouroboros.Consensus.Protocol.Praos ( Praos @@ -95,7 +96,7 @@ validate context MutatedHeader{header, mutation} = } = context -- TODO: get these from the context coin = fromJust . toCompact . Coin - ownsAllStake vrfKey = IndividualPoolStake 1 (coin 1) vrfKey + ownsAllStake vrfKey = IndividualPoolStake 1 (coin 1) vrfKey SNothing poolDistr = Map.fromList [(poolId, ownsAllStake hashVRFKey)] poolId = hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey hashVRFKey = hashVerKeyVRF @StandardCrypto $ deriveVerKeyVRF vrfSignKey diff --git a/ouroboros-consensus-cardano/src/unstable-shelley-testlib/Test/ThreadNet/Infra/Shelley.hs b/ouroboros-consensus-cardano/src/unstable-shelley-testlib/Test/ThreadNet/Infra/Shelley.hs index 699cf39193..d450c47731 100644 --- a/ouroboros-consensus-cardano/src/unstable-shelley-testlib/Test/ThreadNet/Infra/Shelley.hs +++ b/ouroboros-consensus-cardano/src/unstable-shelley-testlib/Test/ThreadNet/Infra/Shelley.hs @@ -37,10 +37,14 @@ module Test.ThreadNet.Infra.Shelley ) where import Cardano.Crypto.DSIGN - ( DSIGNAlgorithm (..) + ( BLS12381MinSigDSIGN + , DSIGNAlgorithm (..) , SignKeyDSIGN + , createPossessionProofDSIGN , seedSizeDSIGN ) +import Cardano.Crypto.Leios (leiosSignContext) +import Cardano.Ledger.State (LeiosKey (..), LeiosPossessionProof (..), LeiosPubKey (..)) import Cardano.Crypto.KES ( KESAlgorithm (..) , UnsoundPureKESAlgorithm (..) @@ -103,7 +107,7 @@ import Ouroboros.Consensus.Protocol.Praos.AgentClient , KESAgentContext ) import Ouroboros.Consensus.Protocol.Praos.Common - ( PraosCanBeLeader (PraosCanBeLeader) + ( PraosCanBeLeader (..) , PraosCredentialsSource (..) , praosCanBeLeaderColdVerKey , praosCanBeLeaderCredentialsSource @@ -175,6 +179,7 @@ data CoreNode c = CoreNode -- ^ The hash of the corresponding verification (public) key will be -- used as the staking credential. , cnVRF :: !(SignKeyVRF (VRF c)) + , cnBLS :: !(Maybe (SignKeyDSIGN BLS12381MinSigDSIGN)) , cnKES :: !(UnsoundPureSignKeyKES (KES c)) , cnOCert :: !(SL.OCert c) } @@ -218,6 +223,7 @@ genCoreNode startKESPeriod = do delKey <- genKeyDSIGN <$> genSeed (seedSizeDSIGN (Proxy @LK.DSIGN)) stkKey <- genKeyDSIGN <$> genSeed (seedSizeDSIGN (Proxy @LK.DSIGN)) vrfKey <- genKeyVRF <$> genSeed (seedSizeVRF (Proxy @(VRF c))) + blsKey <- genKeyDSIGN <$> genSeed (seedSizeDSIGN (Proxy @BLS12381MinSigDSIGN)) kesKey <- unsoundPureGenKeyKES <$> genSeed (seedSizeKES (Proxy @(KES c))) let kesPub = unsoundPureDeriveVerKeyKES kesKey sigma = @@ -237,6 +243,7 @@ genCoreNode startKESPeriod = do , cnDelegateKey = delKey , cnStakingKey = stkKey , cnVRF = vrfKey + , cnBLS = Just blsKey , cnKES = kesKey , cnOCert = ocert } @@ -250,13 +257,16 @@ genCoreNode startKESPeriod = do genSeed = fmap mkSeedFromBytes . genBytes mkLeaderCredentials :: CoreNode c -> ShelleyLeaderCredentials c -mkLeaderCredentials CoreNode{cnDelegateKey, cnVRF, cnKES, cnOCert} = +mkLeaderCredentials CoreNode{cnDelegateKey, cnVRF, cnKES, cnOCert, cnBLS} = ShelleyLeaderCredentials { shelleyLeaderCredentialsCanBeLeader = PraosCanBeLeader { praosCanBeLeaderCredentialsSource = PraosCredentialsUnsound cnOCert cnKES , praosCanBeLeaderColdVerKey = SL.VKey $ deriveVerKeyDSIGN cnDelegateKey , praosCanBeLeaderSignKeyVRF = cnVRF + , -- Vote with the node's Leios (BLS) key; must be the key whose verification + -- key is registered as the pool's 'sppLeiosKey' (see mkGenesisConfig). + praosCanBeLeaderSignKeyBLS = cnBLS } , shelleyLeaderCredentialsLabel = "ThreadNet" } @@ -461,15 +471,26 @@ mkGenesisConfig pVer k f d maxLovelaceSupply slotLength kesCfg coreNodes = , SL.sppOwners = Set.singleton poolOwnerHash , SL.sppRelays = Seq.empty , SL.sppMetadata = SL.SNothing - , SL.sppLeiosKey = SL.SNothing + , SL.sppLeiosKey = leiosKey } ) - | CoreNode{cnDelegateKey, cnStakingKey, cnVRF} <- coreNodes + | CoreNode{cnDelegateKey, cnStakingKey, cnVRF, cnBLS} <- coreNodes , -- The pool and owner hashes are derived from the same key, but -- use different hashing schemes let poolHash = SL.hashKey . SL.VKey $ deriveVerKeyDSIGN cnDelegateKey , let poolOwnerHash = SL.hashKey . SL.VKey $ deriveVerKeyDSIGN cnDelegateKey , let vrfHash = hashVerKeyVRF @c $ deriveVerKeyVRF cnVRF + , -- Register the node's Leios verification key + a valid proof of possession + -- so its committee seat is keyed. The vk must equal the voter's + -- 'deriveVerKeyDSIGN' of its 'praosCanBeLeaderSignKeyBLS' (same 'cnBLS'), + -- else 'getLeiosSeatId' won't match; an invalid PoP is dropped to keyless. + let leiosKey = case cnBLS of + Nothing -> SL.SNothing + Just blsSk -> + SL.SJust $ + LeiosKey + (LeiosPubKey (deriveVerKeyDSIGN blsSk)) + (LeiosPossessionProof (createPossessionProofDSIGN leiosSignContext blsSk)) ] mkProtocolShelley :: diff --git a/ouroboros-consensus-cardano/test/cardano-test/Test/Consensus/Cardano/Golden.hs b/ouroboros-consensus-cardano/test/cardano-test/Test/Consensus/Cardano/Golden.hs index 25f50b376e..33b1fa17b1 100644 --- a/ouroboros-consensus-cardano/test/cardano-test/Test/Consensus/Cardano/Golden.hs +++ b/ouroboros-consensus-cardano/test/cardano-test/Test/Consensus/Cardano/Golden.hs @@ -11,7 +11,7 @@ module Test.Consensus.Cardano.Golden (tests) where import qualified Data.ByteString as BS import LeiosDemoTypes ( LeiosVote - , LeiosVoterId (..) + , LeiosSeatId (..) , RbHash (..) , encodeLeiosVote , signLeiosVote @@ -57,7 +57,7 @@ tests = -- key. Pinned by the corresponding golden file. typicalVote :: LeiosVote typicalVote = - signLeiosVote (generateWith genLeiosSigningKey 42) (LeiosVoterId 1000) rbHash + signLeiosVote (generateWith genLeiosSigningKey 42) (LeiosSeatId 1000) rbHash where rbHash = MkRbHash (BS.pack [0 .. 31]) diff --git a/ouroboros-consensus-diffusion/src/ouroboros-consensus-diffusion/Ouroboros/Consensus/NodeKernel.hs b/ouroboros-consensus-diffusion/src/ouroboros-consensus-diffusion/Ouroboros/Consensus/NodeKernel.hs index e5db463e6e..2d23ecb398 100644 --- a/ouroboros-consensus-diffusion/src/ouroboros-consensus-diffusion/Ouroboros/Consensus/NodeKernel.hs +++ b/ouroboros-consensus-diffusion/src/ouroboros-consensus-diffusion/Ouroboros/Consensus/NodeKernel.hs @@ -535,6 +535,8 @@ initNodeKernel -- to local "EB closure acquired" notifications and emit a vote for -- each acquired EB (which the LeiosNotify server then publishes to -- peers). 'Nothing' disables voting on this node. + -- TODO: Also re-spawn voting thread upon SIGHUP similar to how the + -- blockForgingController does it for block forging void $ forkLinkedThread registry "NodeKernel.leiosVoting" $ runLeiosVoting diff --git a/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs b/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs index 75606faeac..ec9b81f79f 100644 --- a/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs +++ b/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs @@ -637,7 +637,7 @@ doValidateVRFSignature :: doValidateVRFSignature eta0 pd f b = do case Map.lookup hk pd of Nothing -> throwError $ VRFKeyUnknown hk - Just (SL.IndividualPoolStake sigma _totalPoolStake vrfHK) -> do + Just (SL.IndividualPoolStake sigma _totalPoolStake vrfHK _leiosKey) -> do let vrfHKStake = SL.fromVRFVerKeyHash vrfHK vrfHKBlock = VRF.hashVerKeyVRF vrfK vrfHKStake diff --git a/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos/Common.hs b/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos/Common.hs index c8be04fc01..795099e4f5 100644 --- a/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos/Common.hs +++ b/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos/Common.hs @@ -27,6 +27,8 @@ module Ouroboros.Consensus.Protocol.Praos.Common , instantiatePraosCredentials ) where +import Cardano.Crypto.DSIGN.BLS12381 (BLS12381MinSigDSIGN) +import Cardano.Crypto.DSIGN.Class (SignKeyDSIGN) import qualified Cardano.Crypto.KES.Class as KES import Cardano.Crypto.VRF import qualified Cardano.Crypto.VRF as VRF @@ -274,6 +276,9 @@ data PraosCanBeLeader c = PraosCanBeLeader { praosCanBeLeaderColdVerKey :: !(SL.VKey BlockIssuer) -- ^ Stake pool cold key or genesis stakeholder delegate cold key. , praosCanBeLeaderSignKeyVRF :: !(SignKeyVRF (VRF c)) + , praosCanBeLeaderSignKeyBLS :: !(Maybe (SignKeyDSIGN BLS12381MinSigDSIGN)) + -- ^ Optional BLS12-381 signing key for the Leios voting scheme. Set only for + -- block producers participating in Leios (Dijkstra era); 'Nothing' otherwise. , praosCanBeLeaderCredentialsSource :: !(PraosCredentialsSource c) -- ^ How to obtain KES credentials (ocert + sign key) } diff --git a/ouroboros-consensus.cabal b/ouroboros-consensus.cabal index cd0e788665..530e7ce6bd 100644 --- a/ouroboros-consensus.cabal +++ b/ouroboros-consensus.cabal @@ -369,7 +369,7 @@ library binary >=0.8 && <0.11, bytestring >=0.10 && <0.13, cardano-binary, - cardano-crypto-class >=2.3 && <2.6, + cardano-crypto-class >=2.6 && <2.7, cardano-crypto-leios >=0.1, cardano-diffusion:api, cardano-ledger-binary ^>=1.9, @@ -741,6 +741,7 @@ test-suite consensus-test bytestring, cardano-binary, cardano-crypto-class:{cardano-crypto-class, testlib}, + cardano-crypto-leios:testlib, cardano-diffusion:api, cardano-ledger-core:{cardano-ledger-core, testlib}, cardano-slotting:{cardano-slotting, testlib}, @@ -1489,6 +1490,7 @@ library cardano text, these, validation ^>=1.2, + vector, library unstable-byronspec import: common-lib @@ -1618,6 +1620,7 @@ library unstable-shelley-testlib base, bytestring, cardano-crypto-class, + cardano-crypto-leios, cardano-data, cardano-ledger-allegra:cardano-ledger-allegra, cardano-ledger-alonzo-test, diff --git a/ouroboros-consensus/src/ouroboros-consensus/LeiosDemoTypes.hs b/ouroboros-consensus/src/ouroboros-consensus/LeiosDemoTypes.hs index b1d6ec49e4..a5224a41c4 100644 --- a/ouroboros-consensus/src/ouroboros-consensus/LeiosDemoTypes.hs +++ b/ouroboros-consensus/src/ouroboros-consensus/LeiosDemoTypes.hs @@ -28,10 +28,9 @@ import Cardano.Binary , toStrictByteString ) import qualified Cardano.Binary as CBOR +import Cardano.Binary.FixedSizeCodec (decodeFixedSized, encodeFixedSized) import Cardano.Crypto.DSIGN - ( decodeSigDSIGN - , encodeSigDSIGN - , signDSIGN + ( signDSIGN , verifyDSIGN ) import qualified Cardano.Crypto.Hash as Hash @@ -40,20 +39,18 @@ import Cardano.Crypto.Leios , LeiosCert (..) , LeiosCommittee (..) , LeiosDSIGN + , LeiosSeat (..) + , LeiosSeatId (..) , LeiosSignature , LeiosSigningKey , LeiosVerificationKey - , LeiosVoter (..) - , LeiosVoterId (..) , VerificationError , Weight , aggregateLeiosCert - , decodeLeiosVoterId - , encodeLeiosVoterId - , getLeiosVoterId + , getLeiosSeatId , leiosCommitteeSize , leiosSignContext - , resolveLeiosVoter + , resolveLeiosSeat , verifyLeiosCert ) import Cardano.Crypto.Util (SignableRepresentation (..)) @@ -75,12 +72,13 @@ import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Short as SBS import Data.Fixed (Pico) import qualified Data.Foldable as F -import Data.Function (on) import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap -import Data.List (nubBy, sortOn) +import Data.List (sortOn) import Data.Map (Map) import qualified Data.Map.Strict as Map +import Data.Maybe.Strict (StrictMaybe (..)) +import Data.Ord (Down (..)) import Data.Ratio ((%)) import Data.Sequence (Seq) import qualified Data.Sequence as Seq @@ -676,20 +674,22 @@ decodeLeiosEb = do -- * Voting --- | Create a 'LeiosCommittee' from a mapping of verification keys and some --- associated weight. Duplicate entries by verification key are ignored. The --- final 'Weight' in the committee is normalized by the total of the input map. --- TODO: The total can only be calculated here in "everyone votes" scheme. -mkCommitteeEveryoneVotes :: Real w => [(LeiosVerificationKey, w)] -> LeiosCommittee -mkCommitteeEveryoneVotes inputs = - LeiosCommittee - . V.fromList - . sortOn voterWeight - $ [ LeiosVoter{voterWeight = toRational weight / totalWeight, voterVKey = vk} - | (vk, weight) <- nubBy ((==) `on` fst) inputs - ] +-- | Select the voting committee from a stake (weight) distribution per CIP-164: +-- order by stake descending and take the shortest prefix whose cumulative stake +-- reaches @target@ (σ_c). +selectCommitteeByStake :: + -- | The target coverage of weights / stake. + Weight -> + -- | All available voters weights. + [(a, Weight)] -> + -- | The selected committee weights. + [(a, Weight)] +selectCommitteeByStake target = go 0 . sortOn (Down . snd) where - totalWeight = toRational . sum $ snd <$> inputs + go _ [] = [] + go acc (p : ps) + | acc >= target = [] + | otherwise = p : go (acc + snd p) ps -- ** Vote @@ -698,7 +698,7 @@ data LeiosVote = MkLeiosVote { announcingRbHash :: RbHash -- ^ The message that gets signed, the hash of the ranking block -- that announced an endorser block. - , voterId :: LeiosVoterId + , voterId :: LeiosSeatId -- ^ Identity within a 'LeiosCommittee' who signed this vote. , voteSignature :: LeiosSignature -- ^ The cryptographic signature of the vote. @@ -718,16 +718,16 @@ encodeLeiosVote :: LeiosVote -> Encoding encodeLeiosVote MkLeiosVote{announcingRbHash, voterId, voteSignature} = CBOR.encodeListLen 3 <> encodeRbHash announcingRbHash - <> encodeLeiosVoterId voterId - <> encodeSigDSIGN voteSignature + <> CBOR.encodeWord16 voterId.leiosSeatIndex + <> encodeFixedSized voteSignature -- | Dedoe a 'LeiosVote' from CBOR. decodeLeiosVote :: Decoder s LeiosVote decodeLeiosVote = do enforceSize (fromString "LeiosVote") 3 pointRbHash <- decodeRbHash - voterId <- decodeLeiosVoterId - voteSignature <- decodeSigDSIGN + voterId <- LeiosSeatId <$> CBOR.decodeWord16 + voteSignature <- decodeFixedSized pure MkLeiosVote { announcingRbHash = pointRbHash @@ -739,11 +739,11 @@ voteToObject :: LeiosVote -> Aeson.Object voteToObject MkLeiosVote{announcingRbHash, voterId} = mconcat [ "rbHash" .= prettyRbHash announcingRbHash - , "voterId" .= voterId.leiosVoterIndex + , "voterId" .= voterId.leiosSeatIndex ] -- | Create a vote for given 'LeiosPoint' and signing key. -signLeiosVote :: LeiosSigningKey -> LeiosVoterId -> RbHash -> LeiosVote +signLeiosVote :: LeiosSigningKey -> LeiosSeatId -> RbHash -> LeiosVote signLeiosVote sk voterId announcingRbHash = MkLeiosVote { announcingRbHash @@ -754,16 +754,20 @@ signLeiosVote sk voterId announcingRbHash = -- | Validate a 'LeiosVote' against a selected 'Commitee'. validateLeiosVote :: LeiosCommittee -> LeiosVote -> Either VoteInvalid Weight validateLeiosVote committee MkLeiosVote{announcingRbHash, voterId, voteSignature} = - case resolveLeiosVoter committee voterId of + case resolveLeiosSeat committee voterId of Nothing -> Left SignerNotInCommittee - Just voter -> - case verifyDSIGN leiosSignContext voter.voterVKey announcingRbHash voteSignature of - Left _ -> Left InvalidSignature - Right () -> Right voter.voterWeight + Just seat -> + case seat.seatVKey of + SNothing -> Left SignerHasNoKey + SJust vk -> + case verifyDSIGN leiosSignContext vk announcingRbHash voteSignature of + Left _ -> Left InvalidSignature + Right () -> Right seat.seatWeight data VoteInvalid = InvalidSignature | SignerNotInCommittee + | SignerHasNoKey deriving (Eq, Show) -- | Why a CertRB was rejected during ledger validation of its Leios @@ -1144,6 +1148,9 @@ maxTxsPerEb = msgOverhead = 1 + 1 -- short list len + small word sequenceOverhead = 1 + 2 -- sequence major byte + a length > 255 +maxEBClosureSize :: ByteSize32 +maxEBClosureSize = ByteSize32 12_000_000 + minCertificationGap :: Word64 minCertificationGap = 10 @@ -1151,15 +1158,9 @@ minCertificationGap = 10 minCertificationThreshold :: Rational minCertificationThreshold = 3 % 4 -leiosMempoolSize :: ByteSize32 -leiosMempoolSize = ByteSize32 24_090_112 -- 2 * (leiosEBMaxClosureSize + RB block size (mainnet = 90112)) - --- TODO: dry with maxMsgLeiosBlockBytesSize -leiosEBMaxSize :: ByteSize32 -leiosEBMaxSize = ByteSize32 512_000 - -leiosEBMaxClosureSize :: ByteSize32 -leiosEBMaxClosureSize = ByteSize32 12_000_000 +-- | Stake to be covered when selecting the committee. +committeeStakeCoverage :: Weight +committeeStakeCoverage = 99 % 100 -- * Utilities for prototyping diff --git a/ouroboros-consensus/src/ouroboros-consensus/LeiosVoteState.hs b/ouroboros-consensus/src/ouroboros-consensus/LeiosVoteState.hs index 4d72d83060..01cff94215 100644 --- a/ouroboros-consensus/src/ouroboros-consensus/LeiosVoteState.hs +++ b/ouroboros-consensus/src/ouroboros-consensus/LeiosVoteState.hs @@ -23,7 +23,7 @@ import LeiosDemoTypes , LeiosCommittee , LeiosSignature , LeiosVote (..) - , LeiosVoterId + , LeiosSeatId , RbHash , VoteInvalid (..) , Weight @@ -61,7 +61,7 @@ data LeiosVoteSubscription m = LeiosVoteSubscription {getNextVote :: STM m Leios -- Holds the contributing voters plus a memoised certificate once the -- threshold is crossed. data PointState = PointState - { psVoters :: !(Map LeiosVoterId (Weight, LeiosSignature)) + { psVoters :: !(Map LeiosSeatId (Weight, LeiosSignature)) , psCert :: !(Maybe LeiosCert) -- ^ Assembled once when this point's total weight first reaches -- 'minCertificationThreshold'; reused for subsequent post-threshold diff --git a/ouroboros-consensus/src/ouroboros-consensus/LeiosVoting.hs b/ouroboros-consensus/src/ouroboros-consensus/LeiosVoting.hs index 8cda026793..2f1b28623c 100644 --- a/ouroboros-consensus/src/ouroboros-consensus/LeiosVoting.hs +++ b/ouroboros-consensus/src/ouroboros-consensus/LeiosVoting.hs @@ -36,7 +36,7 @@ import LeiosDemoTypes , LeiosSigningKey , RbHash (..) , TraceLeiosKernel (..) - , getLeiosVoterId + , getLeiosSeatId , signLeiosVote ) import LeiosVoteState (AddVoteResult (..), LeiosVoteState (..)) @@ -169,7 +169,7 @@ runLeiosVoting tracer chainDB btime leiosDB voteState = \case let SlotNo aw = pointSlotNo point deadlineSlot = SlotNo (aw + lHdrWaitSlots + lVoteWindowSlots) notVoted r = traceWith tracer TraceLeiosNotVoted{ebPoint = point, reason = r} - mVoterId = getLeiosCommittee (ledgerState extLedger) >>= getLeiosVoterId vk + mVoterId = getLeiosCommittee (ledgerState extLedger) >>= getLeiosSeatId vk mAnnouncer = tipAnnouncerFor @blk (headerState extLedger) point case (currentSlot > deadlineSlot, mAnnouncer, mVoterId) of (True, _, _) -> notVoted TooLate diff --git a/ouroboros-consensus/test/consensus-test/Test/LeiosDemoTypes.hs b/ouroboros-consensus/test/consensus-test/Test/LeiosDemoTypes.hs index ae43209a4a..f035354fb8 100644 --- a/ouroboros-consensus/test/consensus-test/Test/LeiosDemoTypes.hs +++ b/ouroboros-consensus/test/consensus-test/Test/LeiosDemoTypes.hs @@ -1,41 +1,40 @@ -{-# LANGUAGE OverloadedRecordDot #-} -{-# LANGUAGE TypeApplications #-} - module Test.LeiosDemoTypes (tests) where import Cardano.Binary (serialize') -import Cardano.Crypto.DSIGN - ( DSIGNAlgorithm (deriveVerKeyDSIGN) - , genKeyDSIGN - , seedSizeDSIGN - ) import qualified Data.ByteString as BS -import Data.Data (Proxy (..)) -import Data.List (sort) +import Data.Function ((&)) +import Data.Functor ((<&>)) +import Data.List ((\\)) +import Data.Ratio ((%)) import qualified Data.Vector.Strict as V import LeiosDemoTypes ( BytesSize - , LeiosCommittee (..) - , LeiosDSIGN , LeiosEb (..) - , LeiosSigningKey - , LeiosVoter (..) , TxHash (..) , encodeLeiosEb , leiosEbBytesSize , maxTxsPerEb - , mkCommitteeEveryoneVotes + , selectCommitteeByStake ) -import Test.Crypto.Util (arbitrarySeedOfSize) import Test.QuickCheck ( Gen , Property + , checkCoverage , chooseInt + , chooseInteger + , conjoin , counterexample + , cover , forAll + , forAllShrink , frequency + , genericShrink + , listOf + , property + , shrinkIntegral + , shrinkRealFrac , vectorOf - , (.&&.) + , (.||.) , (===) ) import Test.Tasty (TestTree, testGroup) @@ -46,7 +45,9 @@ tests = testGroup "LeiosDemoTypes" [ testProperty "leiosEbBytesSize consistent with encodeLeiosEb" prop_ebBytesSizeConsistent - , testProperty "mkCommitteeEveryoneVotes normalizes and sorts" prop_committeeNormalizedAndSorted + , testProperty + "selectCommitteeByStake orders by stake and applies the cutoff" + prop_selectCommitteeByStake ] -- | Minimum tx size as per the ASSUMPTION in 'leiosEbBytesSize'. @@ -109,24 +110,57 @@ prop_ebBytesSizeConsistent = ("items: " <> show (V.length (leiosEbTxs eb))) (estimatedSize === actualSize) -genLeiosSigningKey :: Gen LeiosSigningKey -genLeiosSigningKey = do - seed <- arbitrarySeedOfSize (seedSizeDSIGN (Proxy @LeiosDSIGN)) - pure $ genKeyDSIGN seed - --- | 'mkCommitteeEveryoneVotes' must produce weights that sum to 1 and are --- sorted ascending (so 'LeiosVoterId' assignment by index is stable). Inputs are --- generated with distinct verification keys, since dedup-by-key is a separate --- concern not exercised here. -prop_committeeNormalizedAndSorted :: Property -prop_committeeNormalizedAndSorted = - forAll (chooseInt (1, 20)) $ \n -> - forAll (vectorOf n genLeiosSigningKey) $ \sks -> - forAll (vectorOf n (chooseInt (1, 1000))) $ \ws -> - let inputs = zip (deriveVerKeyDSIGN <$> sks) ws - committee = mkCommitteeEveryoneVotes inputs - weights = voterWeight <$> V.toList committee.leiosCommitteeVoters - in counterexample ("committee: " <> show committee) $ - counterexample "weights sum to 1" (sum weights === 1) - .&&. counterexample "weights sorted ascending" (weights === sort weights) - .&&. counterexample "preserves cardinality" (length weights === n) +-- | 'selectCommitteeByStake' selects the highest-stake pools truncated at a +-- cumulative-stake target. +prop_selectCommitteeByStake :: Property +prop_selectCommitteeByStake = + forAllShrink (listOf genWeight) genericShrink $ \rawStakes -> + forAllShrink genWeight (filter (> 0) . shrinkRealFrac) $ \target -> + let weights = snd <$> selectCommitteeByStake target (zip [0 :: Int ..] rawStakes) + allSelected = length weights == length rawStakes + in conjoin + [ cutoffReached target weights .||. allSelected + , committeeNonEmpty rawStakes weights + , isDescending weights + , isMinimal target weights + , selectsTopStake rawStakes weights + ] + & counterexample ("target: " <> show target <> ", weights: " <> show weights) + & cover 0.1 (not allSelected) "not all selected" + & checkCoverage + where + genWeight = chooseInteger (1, 100) <&> (% 100) + + forAllIndices xs f + | null xs = property True + | otherwise = forAllShrink (chooseInt (0, length xs - 1)) shrinkIntegral f + + -- The selected stake reaches the target. + cutoffReached target weights = + sum weights >= target + & counterexample "cutoff not reached" + + -- A non-empty pool set yields a non-empty committee (target > 0). + committeeNonEmpty rawStakes weights = + null rawStakes || not (null weights) + & counterexample "empty committee for a non-empty pool set" + + -- Descending order: any prefix outweighs the rest. + isDescending weights = + forAllIndices weights $ \i -> + let (prefix, rest) = splitAt i weights + in null prefix || null rest || minimum prefix >= maximum rest + & counterexample ("weights not monotonically decreasing at " <> show i) + + -- Minimal: dropping any single member falls below the target. + isMinimal target weights = + forAllIndices weights $ \i -> + sum weights - (weights !! i) < target + & counterexample + "committee not minimal: dropping an entry still reaches the target" + + -- Top-stake: no excluded pool outweighs a selected one. + selectsTopStake rawStakes weights = + let excluded = rawStakes \\ weights + in null weights || null excluded || minimum weights >= maximum excluded + & counterexample ("an excluded pool outweighs a selected one: " <> show excluded) diff --git a/ouroboros-consensus/test/consensus-test/Test/LeiosVoteState.hs b/ouroboros-consensus/test/consensus-test/Test/LeiosVoteState.hs index bf740c9d86..ba8c0ec8ac 100644 --- a/ouroboros-consensus/test/consensus-test/Test/LeiosVoteState.hs +++ b/ouroboros-consensus/test/consensus-test/Test/LeiosVoteState.hs @@ -6,8 +6,6 @@ module Test.LeiosVoteState (tests) where import Cardano.Crypto.DSIGN ( DSIGNAlgorithm (deriveVerKeyDSIGN) - , genKeyDSIGN - , seedSizeDSIGN ) import Control.Concurrent.Class.MonadSTM.Strict ( atomically @@ -18,18 +16,14 @@ import Control.Concurrent.Class.MonadSTM.Strict import Control.Monad (forM_) import Control.Monad.Class.MonadTimer.SI (timeout) import Control.Monad.IOSim (runSimOrThrow) -import Data.Data (Proxy (..)) import Data.Maybe (fromJust, isNothing) import LeiosDemoTypes - ( LeiosCommittee (..) - , LeiosDSIGN + ( LeiosSeatId (..) , LeiosSigningKey , LeiosVote (..) - , LeiosVoterId (..) , VoteInvalid (..) - , getLeiosVoterId + , getLeiosSeatId , leiosCommitteeSize - , mkCommitteeEveryoneVotes , signLeiosVote ) import LeiosVoteState @@ -39,19 +33,17 @@ import LeiosVoteState , newLeiosVoteState , subscribeVotes ) -import Test.Crypto.Util (arbitrarySeedOfSize) +import Test.Cardano.Crypto.Leios.Gen (TestCommittee (..), genCommittee, genLeiosSigningKey) import Test.LeiosDemoDb (genRbHash) import Test.QuickCheck ( Gen , Property - , chooseInt , counterexample , elements , forAll , listOf1 , property , suchThat - , vectorOf , (.&&.) , (===) ) @@ -72,29 +64,6 @@ tests = , testProperty "vote signed with key not on committee is rejected" prop_signerNotInCommittee ] -genLeiosSigningKey :: Gen LeiosSigningKey -genLeiosSigningKey = do - seed <- arbitrarySeedOfSize (seedSizeDSIGN (Proxy @LeiosDSIGN)) - pure $ genKeyDSIGN seed - -data TestCommittee = TestCommittee - { committee :: LeiosCommittee - , allKeys :: [LeiosSigningKey] - } - deriving Show - --- | A non-empty committee. -genCommittee :: Gen TestCommittee -genCommittee = do - n <- chooseInt (1, 10) - allKeys <- vectorOf n genLeiosSigningKey - weights <- vectorOf n (chooseInt (1, 100)) - pure - TestCommittee - { committee = mkCommitteeEveryoneVotes $ zip (deriveVerKeyDSIGN <$> allKeys) weights - , allKeys - } - -- | A 'VotingKey' that is *not* a member of the given committee. genKeyNotIn :: TestCommittee -> Gen LeiosSigningKey genKeyNotIn c = do @@ -106,7 +75,7 @@ genKeyNotIn c = do genVoteFor :: TestCommittee -> Gen LeiosVote genVoteFor c = do key <- elements c.allKeys - let vid = fromJust $ getLeiosVoterId (deriveVerKeyDSIGN key) c.committee + let vid = fromJust $ getLeiosSeatId (deriveVerKeyDSIGN key) c.committee signLeiosVote key vid <$> genRbHash -- | A subscriber should receive a vote that was added after subscribing. @@ -219,7 +188,7 @@ prop_signerNotInCommittee = forAll genRbHash $ \announcingRbHash -> property $ runSimOrThrow $ do -- VoterId must be outside of committe, otherwise this is just a bad signature let n = leiosCommitteeSize testCommittee.committee - let vote = signLeiosVote key (LeiosVoterId $ fromIntegral n) announcingRbHash + let vote = signLeiosVote key (LeiosSeatId $ fromIntegral n) announcingRbHash st <- newLeiosVoteState (pure (Just testCommittee.committee)) sub <- subscribeVotes st r <- addVote st vote