Scaffold an initial cardano-crypto-leios package#670
Conversation
d85df0c to
aa227fa
Compare
bf92c7d to
780e347
Compare
1102098 to
25e16ac
Compare
Roundtrip and golden tests for LeiosCert
These are the only means to create and verify leios certificates about a certain message (a leios vote). Committee selection was deliberately kept out of scope
The golden test compares 'cardano-crypto-leios/test/golden/LeiosCert' byte-for-byte against the hex-dump output of 'encodeWithIndex'. Without this attribute, the default Windows 'core.autocrlf=true' translates LF to CRLF on checkout and the comparison fails, even though the file is committed with LF endings.
These were needed/useful in the cardano-ledger-dijkstra integration
73d303e to
745de18
Compare
This avoids redundant import warnings on newer GHC versions
745de18 to
38a3b98
Compare
lehins
left a comment
There was a problem hiding this comment.
Consistency is one of the most important parts in software development. It is important to use consistent dependencies as the rest of the project, in this case cardano-base repo being that project.
| { signers :: !BitField | ||
| , aggregatedSignature :: !LeiosSignature |
There was a problem hiding this comment.
This is a pretty terrible naming, since signers can easily be a local binding anywhere in the cardano-node codebase. I suggest something more descriptive like:
| { signers :: !BitField | |
| , aggregatedSignature :: !LeiosSignature | |
| { leisCertSigners :: !BitField | |
| , leisCertSignature :: !LeiosSignature | |
| -- ^ Aggregated BLS signature |
There was a problem hiding this comment.
I deliberately wanted to match the CDDL as close as possible. The call sites should be all in this module, outside construction and inspection are not really intended (to be convenient) and we could even choose to not export the field selectors. However, I didn't want to be too prescriptive on this type.
There was a problem hiding this comment.
I understand the desire, but I am speaking from experience people doing this precise mistake in the past of trying to name bindings in Haskell the same way as they were in the spec and it is not worth it!
It causes problems in such a huge codebase as ours.
So, there are only two options:
- If these fields are not used outside of this module, then they should not be exported.
- If they are used outside of this module, then they need decent names. Do not worry about matching to the CDDL spec exactly, people can figure out what maps to what
- Replace indexed-hex golden file with raw binary; drop the .gitattributes LF pin and the base16-bytestring dep. - Extract InsufficientWeight's named fields into a WeightMismatch record so no constructor has partial accessors; drop -Wno-partial-fields and DuplicateRecordFields. - Introduce strict LeiosVoter to replace the lazy (Weight, VerKey) tuple in Committee; switch BLSAggregationFailed to Text; tighten verifier accumulator strictness. - Don't export field selectors that can easily overlap.
Replaces the list-of-bytes construction in 'mkBitField' (and the 'BS.unpack' list comprehension in 'bitFieldMembers') with direct mutable 'ByteArray' operations from 'Data.Primitive.ByteArray', so the ByteArray-backed representation isn't undone by intermediate list allocations. Wire encode/decode stay zero-copy via the existing SBS aliasing; on-wire bytes are unchanged (golden test confirms).
'enforceSize' from cardano-binary only accepts definite-length lists, which would reject any producer that emits the 2-element outer array of a Leios certificate as an indefinite-length CBOR array. Switch to 'decodeListLenOrIndef' + 'matchSize' for the definite branch and a trailing 'decodeBreakOr' for the indefinite branch. Adds a QuickCheck property that round-trips through a hand-rolled indefinite-length encoding to lock the new behaviour in.
Section headers move into the export list; the body's '-- *' / '-- **' markers are removed to avoid double sections in Haddock. Doc strings stay at the definitions. 'mkBitField' and 'bitFieldMembers' are no longer exported — they're only callable through 'aggregateLeiosCert' / 'verifyLeiosCert', which the tests exercise transitively. Adversarial tests still have the 'bitFieldFromBytes' / 'bitFieldToBytes' wire helpers.
'aggregateLeiosCert' was binding 'entries = Map.toAscList contributions' just to feed two separate consumers: a range-check over keys and a signature-aggregation over values. Each consumer can take its Map.keys / Map.elems input directly, which lets list fusion eliminate the intermediates per pass. Adds source/destination type applications to every fromIntegral in the package (src + test + testlib) so the conversion's intent is explicit at the call site and silent type-changes during refactors are caught.
This should be a typical size (> 99% of current stake distribution)
lehins
left a comment
There was a problem hiding this comment.
More feedback, none of which is terribly critical and we can fix it up later if needed.
| getVoterId :: LeiosVerificationKey -> Committee -> Maybe VoterId | ||
| getVoterId vk committee = | ||
| VoterId . fromIntegral @Int @Word16 | ||
| <$> V.findIndex ((== vk) . voterVKey) committee.committeeVoters |
There was a problem hiding this comment.
Where is getVoterId going to be used. There are two issues with this function:
- It has O(n) complexity, which can lead to significant performance degradation if used in an inner loop.
- It is susceptible to integer overflow for a committee size larger than 65535
In theory neither one of these is dangerous, as long as we are sure these edge cases are not triggered. For example today we know that we can't have 65K stake pools, but in 20 years time this might change and this function could become a silent killer.
In any case, if it is only used in testing, I'd suggest moving it into the testlib. However if it will be used in production then we need to properly document those points and instead of simply calling fromIntegral, we should make this function partial upon overflow (eg. error "Impossible: committee is too large").
| when (got < required) $ | ||
| Left (InsufficientWeight WeightMismatch {got, required}) | ||
| aggVk <- | ||
| uncheckedAggregateVerKeysDSIGN (reverse vks) -- REVIEW: reverse needed? |
There was a problem hiding this comment.
It is needed when used with foldlM. Alternatively you could use foldrM
Normally this would not be desired during validation, since:
foldrMwill not short-circuit on the very first failure. But that is ok for us, since we need to optimize the most common case which is the valid case- with
foldrMvalidation happens from the end of the list in the opposite direction, but since we only reportMalformedSignersfailure without any other information this point is irrelevant
That being said, it would be a whole lot more efficient to implement a manual nested loop over bits in the byte array, instead of converting BitArray to a list of indices and monadically folding over that list. That being said, such optimization can be postpone until a later time.
| uncheckedAggregateVerKeysDSIGN (reverse vks) -- REVIEW: reverse needed? | |
| uncheckedAggregateVerKeysDSIGN (reverse vks) |
| { got :: !Weight | ||
| , required :: !Weight |
There was a problem hiding this comment.
If the definition is exported into a public api, could I please ask you give those bindings decent names. Whatever you come up, but not something that is so short and not descriptive as got. Maybe:
| { got :: !Weight | |
| , required :: !Weight | |
| { weightReceived :: !Weight | |
| , weightRequired :: !Weight |
| { signers :: !BitField | ||
| , aggregatedSignature :: !LeiosSignature |
There was a problem hiding this comment.
I understand the desire, but I am speaking from experience people doing this precise mistake in the past of trying to name bindings in Haskell the same way as they were in the spec and it is not worth it!
It causes problems in such a huge codebase as ours.
So, there are only two options:
- If these fields are not used outside of this module, then they should not be exported.
- If they are used outside of this module, then they need decent names. Do not worry about matching to the CDDL spec exactly, people can figure out what maps to what
| -- skip per-key PoP checks (they use 'uncheckedAggregateVerKeysDSIGN' / | ||
| -- 'aggregateSigsDSIGN' under the hood). Passing in unchecked keys defeats | ||
| -- the security of the aggregate signature. | ||
| newtype Committee = Committee {committeeVoters :: Vector LeiosVoter} |
There was a problem hiding this comment.
Can we please call it LeiosCommittee, since we already have Committee in Ledger and it will be too confusing if we do not disambiguate
| newtype Committee = Committee {committeeVoters :: Vector LeiosVoter} | |
| newtype LeiosCommittee = LeiosCommittee {leiosCommitteeVoters :: Vector LeiosVoter} |
| = -- | A voter index in the sigs is past the committee bound. | ||
| VoterIdOutOfBounds !VoterId | ||
| | -- | BLS signature aggregation failed (e.g. malformed input signature). | ||
| BLSAggregationFailed !Text |
There was a problem hiding this comment.
Ditto
| BLSAggregationFailed !Text | |
| BLSAggregationFailed Text |
| -- Builds directly into a mutable 'ByteArray' via a single allocation and | ||
| -- writes one bit per member of the input set. | ||
| signers = BitField $ runByteArray $ do | ||
| let len = (n + 7) `div` 8 |
There was a problem hiding this comment.
n isn't used outside this binding, this will be cleaner:
| let len = (n + 7) `div` 8 | |
| let n = committeeSize committee | |
| len = (n + 7) `div` 8 |
|
|
||
| -- | Encode a 'BitField' to CBOR bytes. | ||
| encodeBitField :: BitField -> Encoding | ||
| encodeBitField = encodeBytes . byteArrayToByteString . bitFieldBytes |
There was a problem hiding this comment.
Conversion to ByteString is unnecessary.
| encodeBitField = encodeBytes . byteArrayToByteString . bitFieldBytes | |
| encodeBitField = toCBOR . bitFieldBytes |
| -- Accepts both definite-length and indefinite-length encodings of the | ||
| -- outer 2-element array. | ||
| decodeLeiosCert :: Decoder s LeiosCert | ||
| decodeLeiosCert = do |
There was a problem hiding this comment.
To be honest with you, looking at this implementation makes me believe that encoders/decoders should be implemented in ledger in cardano-ledger-binary instead, because:
- we already have proper helpers for this stuff (for this case eg.
decodeRecordNamed) - we'll probably have to ignore some of those decoders in this module anyways and rewrite them in ledger using versioned decoder.
We stopped using plain decoder/encoder for almost anything in ledger, except for byron era of course.
|
|
||
| -- | Decode a 'BitField' from CBOR bytes. | ||
| decodeBitField :: Decoder s BitField | ||
| decodeBitField = BitField . byteArrayFromByteString <$> decodeBytes |
There was a problem hiding this comment.
To drive my point about defining these decoders in ledger further: we have introduced indefinite length decoding for bytes for Dijkstra era, which plain decodeBytes does not support.
Adds a new package for leios cryptographic types and operations. This was done in course of IntersectMBO/ouroboros-consensus#2068, I'm currently integrating this with the
cardano-ledgermasterand expect a follow-up PR there.The digital signature scheme is BLS12-381 and fixed in the module. Contrary to the CIP-164, the certificate does not contain a slot or
EbHashanymore. This makes definition incardano-basea lot easier and in the current block structure design, the "message" against which the certificate is signed would be available from the (block) context in which the certificate is used.Most importantly, this module contains encoders/decoders for the
LeiosCerttype including roundtrip and golden tests. This should be enough for thecardano-ledgerto use this type confidently inDijkstraera blocks.There are also property tests about aggregating and verifying certificates. The
Committeeis part of this package, but how it is selected is deliberately kept out of scope.