morley-1.13.0: src/Tezos/Crypto/BLS12381.hs
-- SPDX-FileCopyrightText: 2021 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
-- | Support for BLS12-381 elliptic curve.
--
-- Some general hints on the implementation can be found in this python
-- re-implementation used by Tezos for testing:
-- <https://gitlab.com/metastatedev/tezos/-/commit/f10c39e0030e6b4fdd416a62de7b80b6ffdfeacf#80b4b1585c1e6fa82f2cfaf75001c490613f22c3>.
-- And it uses this library inside: <https://github.com/ethereum/py_ecc/tree/master/py_ecc/optimized_bls12_381>.
module Tezos.Crypto.BLS12381
( Bls12381Fr
, Bls12381G1
, Bls12381G2
, CurveObject (..)
, fromMichelsonBytesUnsafe
, MultiplyPoint (..)
, DeserializationError (..)
, checkPairing
-- * Playground
, readFromHexUnsafe
, generateFrom
, g1One
, g2One
) where
import Prelude hiding (negate, one)
import qualified Prelude
import Control.Exception (assert)
import Control.Lens (each, toListOf)
import Control.Monad.Random (MonadRandom, evalRand, getRandom, mkStdGen)
import Data.Bits (bit, complement, setBit, testBit, (.&.))
import qualified Data.ByteString as BS
import qualified Data.Curve as C
import qualified Data.Curve.Weierstrass as CW
import qualified Data.Curve.Weierstrass.BLS12381 as CW.BLS
import qualified Data.Field.Galois as GF
import qualified Data.Pairing.BLS12381 as BLS
import Fmt (Buildable(..), pretty, (+|), (|+))
import Named (arg, type (:!), (!))
import Text.Hex (decodeHex, encodeHex)
import Util.Instances ()
import Util.Named ()
import Util.Num
-- | Methods common for all BLS12-381 primitives.
class CurveObject a where
-- | Representation of @0@, aka additive identity.
zero :: a
-- | Negate a value.
negate :: a -> a
-- | Add up two values.
add :: a -> a -> a
-- | Generate a random value.
generate :: MonadRandom m => m a
-- | Read a value from Michelson's bytes form.
--
-- Michelson tends to represent all BLS12-381 types in bytes form,
-- some special types also allow other forms.
fromMichelsonBytes :: ByteString -> Either DeserializationError a
-- | Produce Michelson's bytes representation.
toMichelsonBytes :: a -> ByteString
-- | Generate a random value from given seed.
generateFrom :: (CurveObject a) => Int -> a
generateFrom = evalRand generate . mkStdGen
-- | Read a value from Michelson's bytes form assuming that it is correct.
fromMichelsonBytesUnsafe :: (CurveObject a, HasCallStack) => ByteString -> a
fromMichelsonBytesUnsafe = either (error . pretty) id . fromMichelsonBytes
-- | Reads an object from hex string.
--
-- To be used only in playground and tests.
readFromHexUnsafe :: (CurveObject a, HasCallStack) => String -> a
readFromHexUnsafe hex =
let bs = decodeHex (toText hex) ?: error "bad hex"
in fromMichelsonBytesUnsafe bs
-- | Multiplication operations on BLS12-381 objects.
class MultiplyPoint scalar point where
-- | Multiply point value by scalar value.
multiply :: scalar -> point -> point
-- | G1 point on the curve.
newtype Bls12381G1 = Bls12381G1 { unBls12381G1 :: BLS.G1' }
deriving stock (Show, Eq)
deriving newtype (NFData)
instance CurveObject Bls12381G1 where
zero = Bls12381G1 C.id
negate (Bls12381G1 v) = Bls12381G1 (C.inv v)
add (Bls12381G1 a) (Bls12381G1 b) = Bls12381G1 (C.add a b)
generate = Bls12381G1 <$> C.rnd
fromMichelsonBytes =
let bsToCoord = toPrime . fromBigEndian
in fmap Bls12381G1 . parseJA2WAPoint g1CoordLen bsToCoord
toMichelsonBytes =
let coordToBs = toBigEndian g1CoordLen . fromPrime
in convertWA2JAPoint g1CoordLen coordToBs . unBls12381G1
instance MultiplyPoint Integer Bls12381G1 where
multiply s (Bls12381G1 p) = Bls12381G1 (C.mul' p s)
-- | G2 point on the curve.
newtype Bls12381G2 = Bls12381G2 { unBls12381G2 :: BLS.G2' }
deriving stock (Show, Eq)
deriving newtype (NFData)
instance CurveObject Bls12381G2 where
zero = Bls12381G2 C.id
negate (Bls12381G2 v) = Bls12381G2 (C.inv v)
add (Bls12381G2 a) (Bls12381G2 b) = Bls12381G2 (C.add a b)
generate = Bls12381G2 <$> C.rnd
fromMichelsonBytes =
let fromBsPair = map fromBigEndian . toListOf each . BS.splitAt (g2CoordLen `div` 2)
bsToCoord = GF.toE . reverse . map toPrime . fromBsPair
in fmap Bls12381G2 . parseJA2WAPoint g2CoordLen bsToCoord
toMichelsonBytes =
let toBsPair = foldMap (toBigEndian $ g2CoordLen `div` 2)
coordToBs = toBsPair . map fromPrime . reverse . GF.fromE
in convertWA2JAPoint g1CoordLen coordToBs . unBls12381G2
instance MultiplyPoint Integer Bls12381G2 where
multiply s (Bls12381G2 p) = Bls12381G2 (C.mul' p s)
-- | An element of an algebraic number field (scalar), used for multiplying
-- 'Bls12381G1' and 'Bls12381G2'.
newtype Bls12381Fr = Bls12381Fr { unBls12381Fr :: BLS.Fr }
deriving stock (Show, Eq, Ord)
deriving newtype (Num, Enum, Bounded, Real, Integral, Fractional, NFData)
instance CurveObject Bls12381Fr where
zero = Bls12381Fr 0
negate = Prelude.negate
add = (+)
generate = Bls12381Fr <$> getRandom
fromMichelsonBytes bs =
if length bs > frLen
then Left $ TooLargeLength ! #limit frLen ! #given (length bs)
else
let num = fromLittleEndian bs
in fromIntegralChecked num
& first (\_ -> ValueOutsideOfField $ toInteger num)
toMichelsonBytes = toLittleEndian frLen . fromPrime . unBls12381Fr
instance MultiplyPoint Bls12381Fr Bls12381G1 where
multiply (Bls12381Fr s) (Bls12381G1 p) = Bls12381G1 (C.mul p s)
instance MultiplyPoint Bls12381Fr Bls12381G2 where
multiply (Bls12381Fr s) (Bls12381G2 p) = Bls12381G2 (C.mul p s)
-- | Checks that product of pairings of points in the list is equal to 1 in
-- Fq12 field.
checkPairing :: [(Bls12381G1, Bls12381G2)] -> Bool
checkPairing pairs =
-- Some hints for implementation of this function:
-- https://gitlab.com/metastatedev/tezos/-/commit/bb2cda17d48a52ce854e027f0222a0463e0e66f0#af97cb649204420968454a94e7bfaa6a6e27195a_1285_1330
-- https://gitlab.com/metastatedev/tezos/-/commit/f10c39e0030e6b4fdd416a62de7b80b6ffdfeacf#80b4b1585c1e6fa82f2cfaf75001c490613f22c3_0_172
-- Monoid instance on GT' has the desired multiplicative semantics
foldMap pairing pairs == mempty
where
pairing :: (Bls12381G1, Bls12381G2) -> BLS.GT'
pairing (Bls12381G1 g1, Bls12381G2 g2) =
BLS.finalExponentiationBLS12 BLS.parameterHex
(BLS.millerAlgorithmBLS12 BLS.parameterBin g1 g2)
----------------------------------------------------------------------------
-- Serialization helpers
----------------------------------------------------------------------------
-- | All kinds of errors that can occur when reading a Michelson value.
data DeserializationError
= CompressedFormIsNotSupported
| UnexpectedLength ("expected" :! Int) ("given" :! Int)
| TooLargeLength ("limit" :! Int) ("given" :! Int)
| ValueOutsideOfField Integer
| PointNotOnCurve ByteString
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData)
instance Buildable DeserializationError where
build = \case
CompressedFormIsNotSupported ->
"Compressed form of BLS12-381 point is not supported by Tezos"
UnexpectedLength (arg #expected -> expected) (arg #given -> given) ->
"Unexpected length of BLS12-381 primitive: \
\expected " +| expected |+ ", but given " +| given |+ ""
TooLargeLength (arg #limit -> limit) (arg #given -> given) ->
"Too large length of BLS12-381 primitive: \
\limit is " +| limit |+ ", but given " +| given |+ ""
ValueOutsideOfField v ->
"Value is too large for the given field of values: " +| v |+ ""
PointNotOnCurve bs ->
"Point is not on curve: 0x" +| encodeHex bs |+ ""
{- Note on serialization:
In BLS12-381, "381" stands for the number of bits necessary to represent a
coordinate of a point on a curve, i.e. we have to use 48 bytes, getting 3 extra
bits. In the BLS12-381 library used by Tezos, those bits are exploited to carry
some meta information.
More info can be found here:
<https://github.com/zkcrypto/pairing/blob/307aa1f29dccaed09abe774d2027cad57fc5d0b4/src/bls12_381/README.md#serialization>.
So Fr is just a scalar (but a pretty big one), represented in little-endian
as said in the Michelson docs.
G1 and G2 represent a point on curve and have the following form:
X coordinate Y coordinate
|!___________________|____________________|
\ (big-endian) (big-endian)
\
`- bits with meta info
Generally, various coordinate systems may be used to represent a point on curve,
and the library picked by Tezos uses Jacobian coordinates, probably assuming
that the third @Z@ coordinate is always @1@.
Note that we use 'Data.Pairing.BLS12381' which by default picks Weierstrass
coordinates (it has two, not three coordinates, and an "infinity point" which
is kept as a special case), but it also provides methods for converting between
different coordinate systems.
Coordinates take a different amount of space in G1 and G2:
* In G1 both X and Y are from a so-called "Fr" field, where numbers take
48 bytes (without the leading 3 bits).
* In G2 both coordinates are from "Fr2" field which is a two-dimensional field
over "Fr", i.e. X and Y themselves contain two 48-byte coordinates each.
Note that this is correct for the "uncompressed" form, and there is a different
"compressed" form that, fortunately, seems to be not supported by Michelson.
They initially planned to have @COMPRESS@ and @UNCOMPRESS@ instructions, perhaps
for manual conversions, but those instructions didn't appear in Edonet
eventually.
-}
-- | A helper datatype for representing points in raw bytes form.
data RawPoint
= Infinity
-- ^ Point at infinity.
| RawPoint ByteString
-- ^ Bytes representing the payload.
-- | Given the Michelson representation of a point, interpret flags
-- and return the bare payload.
--
-- This assumes that a proper number of bytes is provided.
parsePointFlags
:: HasCallStack
=> ByteString -> Either DeserializationError RawPoint
parsePointFlags bsFull =
case BS.uncons bsFull of
Nothing -> error "Empty byte sequence"
Just (b, bs)
| b `testBit` compressionBit ->
Left CompressedFormIsNotSupported
| b `testBit` infinityBit ->
return Infinity
| otherwise -> do
let
b' = b .&. complement
(sum $ map bit [compressionBit, infinityBit, flag3Bit])
return $ RawPoint (BS.cons b' bs)
-- | Fill a point in raw bytes form with the necessary flags.
fillPointFlags :: HasCallStack => Int -> RawPoint -> ByteString
fillPointFlags 0 = error "Coordinates are unexpectedly empty"
fillPointFlags len = \case
Infinity -> BS.cons (0 `setBit` infinityBit) (BS.replicate (len - 1) 0)
RawPoint bs -> bs
-- | Get a bytestring containing coordinates of a point and split it,
-- checking that each coordinate occupies the given number of bytes.
splitUncompressedPoint
:: Int -> ByteString -> Either DeserializationError (ByteString, ByteString)
splitUncompressedPoint coordLen bs
| length bs /= coordLen * 2 =
Left $ UnexpectedLength ! #expected (coordLen * 2) ! #given (length bs)
| otherwise =
Right $ BS.splitAt coordLen bs
-- | Parse a point in Weierstrass form and Affine coordinates,
-- assuming that in the provided bytestring the point is given in Jacobian
-- coordinates (the library used by Tezos operates in Jacobian coordinates).
parseJA2WAPoint
:: ( CW.BLS.WJCurve CW.BLS.BLS12381 fq BLS.Fr
, CW.BLS.WACurve CW.BLS.BLS12381 fq BLS.Fr
)
=> Int
-> (ByteString -> fq)
-> ByteString
-> Either DeserializationError (CW.BLS.WAPoint CW.BLS.BLS12381 fq BLS.Fr)
parseJA2WAPoint coordLen toCoord full = do
(xRawWithFlags, yRaw) <- splitUncompressedPoint coordLen full
xRawPoint <- parsePointFlags xRawWithFlags
case xRawPoint of
Infinity -> return CW.O
RawPoint xRaw ->
let point = C.toA $ CW.J (toCoord xRaw) (toCoord yRaw) 1
in if C.def point
then return point
else Left $ PointNotOnCurve full
-- | Turn a Weierstrass Affine point into Jacobian coordinates and represent
-- those as bytes.
convertWA2JAPoint
:: ( CW.BLS.WJCurve CW.BLS.BLS12381 fq BLS.Fr
, CW.BLS.WACurve CW.BLS.BLS12381 fq BLS.Fr
)
=> Int
-> (fq -> ByteString)
-> CW.BLS.WAPoint CW.BLS.BLS12381 fq BLS.Fr
-> ByteString
convertWA2JAPoint coordLen toRawCoord point =
let
rawPoint = case point of
CW.O -> Infinity
p@CW.A{} -> RawPoint $
let CW.J x y z = C.fromA p
-- Conversion from Affine coordinates used to produce an already
-- normalized value.
-- In case this turns out to be incorrect, probably @x / z@ and @y / z@
-- are just what we want.
in assert (z == 1) $
toRawCoord x <> toRawCoord y
in fillPointFlags (coordLen * 2) rawPoint
-- | Interpret a byte sequence as a number in big-endian.
fromBigEndian :: ByteString -> Natural
fromBigEndian bs =
foldl' (\acc byte -> acc * 0x100 + fromIntegral @Word8 @Natural byte) 0 $
BS.unpack bs
-- | Interpret a byte sequence as a number in little-endian.
fromLittleEndian :: ByteString -> Natural
fromLittleEndian bs =
sum . zipWith (*) (iterate (* 0x100) 1) . map (fromIntegral @Word8 @Natural) $
BS.unpack bs
-- | Represent a number in a big-endian byte sequence, padding the output
-- to the expected length.
--
-- We assert that the length is sufficient for representing the given number.
toBigEndian :: Int -> Natural -> ByteString
toBigEndian len num =
BS.pack $
let
(remainder, bytes) = mapAccumR
(\x _ -> second (fromIntegral @Natural @Word8) $ x `divMod` 0x100)
num [1 .. len]
in assert (remainder == 0) bytes
-- | Represent a number in a little-endian byte sequence, padding the output
-- to the expected length.
--
-- We assert that the length is sufficient for representing the given number.
toLittleEndian :: Int -> Natural -> ByteString
toLittleEndian len num =
BS.pack $
let
(remainder, bytes) = mapAccumL
(\x _ -> second (fromIntegral @Natural @Word8) $ x `divMod` 0x100)
num [1 .. len]
in assert (remainder == 0) bytes
-- | Turn a prime field element into a natural.
fromPrime :: KnownNat p => GF.Prime p -> Natural
fromPrime p =
-- This should be safe, since 'Prime's exist in modular arithmetics,
-- so its conversion to an integer should produce non-negative elements.
-- In fact, 'GF.Prime' is a newtype wrapper over 'Natural', but
-- its constructor is not exported :/
assert (p >= 0) $
fromIntegral p
-- | The inverse to 'fromPrime'.
toPrime :: KnownNat p => Natural -> GF.Prime p
toPrime = fromIntegral
-- Primitives' lengths
----------------------------------------------------------------------------
-- | Length of a single coordinate of a point in raw bytes form.
g1CoordLen, g2CoordLen :: Int
g1CoordLen = 48
g2CoordLen = 96 -- each coordinate is an element of two-dimensional field Fr2
-- | Length of 'Fr' in raw bytes form.
frLen :: Int
frLen = 32
-- Meta bits
----------------------------------------------------------------------------
-- | This bit designates whether the point is represented in compressed form
-- (only X coordinate), or uncompressed form (both X and Y coordinates).
compressionBit :: Int
compressionBit = 7
-- | This bit designates whether the given point is at infinity.
--
-- If so, all other bytes should be zeros.
infinityBit :: Int
infinityBit = 6
-- | This bit is set iff "this point is in compressed form /and/ it is not the
-- point at infinity /and/ its y-coordinate is the lexicographically largest of
-- the two associated with the encoded x-coordinate".
--
-- Fortunatelly, this flag seems to be not relevant for us at the moment.
flag3Bit :: Int
flag3Bit = 5
----------------------------------------------------------------------------
-- Other constants
----------------------------------------------------------------------------
-- | @1@ represented in G1 - as the libraries used by Tezos see it.
--
-- Taken from here: <https://github.com/ethereum/py_ecc/blob/3f644b4c07c8270b8fbe989eb799766aca66face/py_ecc/optimized_bls12_381/optimized_curve.py#L34>.
g1One :: Bls12381G1
g1One = Bls12381G1 $ CW.toA $ CW.J
3685416753713387016781088315183077757961620795782546409894578378688607592378376318836054947676345821548104185464507
1339506544944476473020471379941921221584933875938349620426543736416511423956333506472724655353366534992391756441569
1
-- | @1@ represented in G2.
g2One :: Bls12381G2
g2One = Bls12381G2 $ CW.toA $ CW.J
(GF.toE
[ 352701069587466618187139116011060144890029952792775240219908644239793785735715026873347600343865175952761926303160
, 3059144344244213709971259814753781636986470325476647558659373206291635324768958432433509563104347017837885763365758
])
(GF.toE
[ 1985150602287291935568054521177171638300868978215655730859378665066344726373823718423869104263333984641494340347905
, 927553665492332455747201965776037880757740193453592970025027978793976877002675564980949289727957565575433344219582
])
(GF.toE [1, 0])