diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
 # Changelog
 
+- 0.2.2 (2025-02-16)
+  * Exports the secp256k1 "point at infinity" as _CURVE_ZERO.
+
 - 0.2.1 (2024-12-18)
   * Adds 'serialize_point' for compressed-format serialization of
     secp256k1 points.
diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs
--- a/lib/Crypto/Curve/Secp256k1.hs
+++ b/lib/Crypto/Curve/Secp256k1.hs
@@ -21,10 +21,18 @@
 -- "low-S" signatures) on the elliptic curve secp256k1.
 
 module Crypto.Curve.Secp256k1 (
+  -- * Field and group parameters
+    _CURVE_Q
+  , _CURVE_P
+  , remQ
+  , modQ
+
   -- * secp256k1 points
-    Pub
+  , Pub
   , derive_pub
   , derive_pub'
+  , _CURVE_G
+  , _CURVE_ZERO
 
   -- * Parsing
   , parse_int256
@@ -74,11 +82,6 @@
   -- for testing/benchmarking
   , _sign_ecdsa_no_hash
   , _sign_ecdsa_no_hash'
-  , _CURVE_P
-  , _CURVE_Q
-  , _CURVE_G
-  , remQ
-  , modQ
   ) where
 
 import Control.Monad (when)
@@ -182,15 +185,6 @@
   where
     l = BS.length u
 
--- replacing the following w/a series of functions with the hashed tags
--- hard-coded is possible, but there is virtually no performance benefit
-
--- (bip0340) tagged hash function
-hash_tagged :: BS.ByteString -> BS.ByteString -> BS.ByteString
-hash_tagged tag x =
-  let !h = SHA256.hash tag
-  in  SHA256.hash (h <> h <> x)
-
 -- (bip0340) return point with x coordinate == x and with even y coordinate
 lift :: Integer -> Maybe Affine
 lift x
@@ -237,7 +231,7 @@
 -- Convert to affine coordinates.
 affine :: Projective -> Affine
 affine p@(Projective x y z)
-  | p == _ZERO = Affine 0 0
+  | p == _CURVE_ZERO = Affine 0 0
   | z == 1     = Affine x y
   | otherwise  = case modinv z (fi _CURVE_P) of
       Nothing -> error "ppad-secp256k1 (affine): impossible point"
@@ -246,7 +240,7 @@
 -- Convert to projective coordinates.
 projective :: Affine -> Projective
 projective (Affine x y)
-  | x == 0 && y == 0 = _ZERO
+  | x == 0 && y == 0 = _CURVE_ZERO
   | otherwise = Projective x y 1
 
 -- Point is valid
@@ -260,13 +254,13 @@
 -- curve parameters -----------------------------------------------------------
 -- see https://www.secg.org/sec2-v2.pdf for parameter specs
 
--- secp256k1 field prime
+-- | secp256k1 field prime.
 --
--- = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
+--   = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
 _CURVE_P :: Integer
 _CURVE_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
 
--- secp256k1 group order
+-- | secp256k1 group order.
 _CURVE_Q :: Integer
 _CURVE_Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
 
@@ -290,7 +284,7 @@
 _CURVE_B :: Integer
 _CURVE_B = 7
 
--- secp256k1 generator
+-- | secp256k1 generator point.
 --
 -- = parse_point
 --     "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"
@@ -299,9 +293,14 @@
   x = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
   y = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
 
+-- | secp256k1 zero point / point at infinity / monoidal identity.
+_CURVE_ZERO :: Projective
+_CURVE_ZERO = Projective 0 1 0
+
 -- secp256k1 zero point / point at infinity / monoidal identity
 _ZERO :: Projective
 _ZERO = Projective 0 1 0
+{-# DEPRECATED _ZERO "use _CURVE_ZERO instead" #-}
 
 -- secp256k1 in prime order j-invariant 0 form (i.e. a == 0).
 weierstrass :: Integer -> Integer
@@ -321,13 +320,12 @@
 remP a = I.integerRem a _CURVE_P
 {-# INLINE remP #-}
 
--- Division modulo secp256k1 group order.
+-- | Division modulo secp256k1 group order.
 modQ :: Integer -> Integer
 modQ a = I.integerMod a _CURVE_Q
 {-# INLINE modQ #-}
 
--- Division modulo secp256k1 group order, when argument is nonnegative.
--- (more efficient than modQ)
+-- | Division modulo secp256k1 group order, when argument is nonnegative.
 remQ :: Integer -> Integer
 remQ a = I.integerRem a _CURVE_Q
 {-# INLINE remQ #-}
@@ -356,8 +354,8 @@
         when (ev > 0) $ do
           when (I.integerTestBit ev 0) $ do
             numv <- readSTRef num
-            modifySTRef' r (\rv -> (rv * numv) `I.integerRem` _CURVE_P)
-          modifySTRef' num (\numv -> (numv * numv) `I.integerRem` _CURVE_P)
+            modifySTRef' r (\rv -> remP (rv * numv))
+          modifySTRef' num (\numv -> remP (numv * numv))
           modifySTRef' e (`I.integerShiftR` 1)
           loop
 
@@ -555,7 +553,7 @@
 mul :: Projective -> Integer -> Projective
 mul p _SECRET
     | not (ge _SECRET) = error "ppad-secp256k1 (mul): scalar not in group"
-    | otherwise  = loop (0 :: Int) _ZERO _CURVE_G p _SECRET
+    | otherwise  = loop (0 :: Int) _CURVE_ZERO _CURVE_G p _SECRET
   where
     loop !j !acc !f !d !m
       | j == _CURVE_Q_BITS = acc
@@ -572,10 +570,10 @@
 -- Don't use this function if the scalar could potentially be a secret.
 mul_unsafe :: Projective -> Integer -> Projective
 mul_unsafe p n
-    | n == 0 = _ZERO
+    | n == 0 = _CURVE_ZERO
     | not (ge n) =
         error "ppad-secp256k1 (mul_unsafe): scalar not in group"
-    | otherwise  = loop _ZERO p n
+    | otherwise  = loop _CURVE_ZERO p n
   where
     loop !r !d m
       | m <= 0 = r
@@ -635,7 +633,7 @@
 -- secp256k1 points.
 mul_wnaf :: Context -> Integer -> Projective
 mul_wnaf Context {..} _SECRET =
-    loop 0 _ZERO _CURVE_G _SECRET
+    loop 0 _CURVE_ZERO _CURVE_G _SECRET
   where
     wins = 256 `quot` ctxW + 1
     wsize = 2 ^ (ctxW - 1)
@@ -779,8 +777,8 @@
 
 -- | Serialize a secp256k1 point in 33-byte compressed form.
 --
---   >>> serialize_point <secp256k1 point>
---   "<33-byte bytestring>"
+--   >>> serialize_point pub
+--   "<33-byte compressed point>"
 serialize_point :: Projective -> BS.ByteString
 serialize_point (affine -> Affine x y) = BS.cons b (unroll32 x) where
   b | I.integerTestBit y 0 = 0x03
@@ -845,11 +843,11 @@
             | otherwise = _SECRET
 
           bytes_d = unroll32 d
-          h_a = hash_tagged "BIP0340/aux" a
+          h_a = hash_aux a
           t = xor bytes_d h_a
 
           bytes_p = unroll32 x_p
-          rand = hash_tagged "BIP0340/nonce" (t <> bytes_p <> m)
+          rand = hash_nonce (t <> bytes_p <> m)
 
           k' = modQ (roll32 rand)
 
@@ -861,7 +859,7 @@
                   | otherwise = k'
 
                 bytes_r = unroll32 x_r
-                e = modQ . roll32 . hash_tagged "BIP0340/challenge"
+                e = modQ . roll32 . hash_challenge
                   $ bytes_r <> bytes_p <> m
 
                 bytes_ked = unroll32 (modQ (k + e * d))
@@ -920,16 +918,37 @@
         let (roll32 -> r, roll32 -> s) = BS.splitAt 32 sig
         in  if   r >= _CURVE_P || s >= _CURVE_Q
             then False
-            else let e = modQ . roll32 $ hash_tagged "BIP0340/challenge"
+            else let e = modQ . roll32 $ hash_challenge
                            (unroll32 r <> unroll32 x_P <> m)
                      dif = add (_mul s)
                                (neg (mul_unsafe (projective capP) e))
-                 in  if   dif == _ZERO
+                 in  if   dif == _CURVE_ZERO
                      then False
                      else let Affine x_R y_R = affine dif
                           in  not (I.integerTestBit y_R 0 || x_R /= r)
 {-# INLINE _verify_schnorr #-}
 
+-- hardcoded tag of BIP0340/aux
+--
+-- \x -> let h = SHA256.hash "BIP0340/aux"
+--       in  SHA256.hash (h <> h <> x)
+hash_aux :: BS.ByteString -> BS.ByteString
+hash_aux x = SHA256.hash $
+  "\241\239N^\192c\202\218m\148\202\250\157\152~\160i&X9\236\193\US\151-w\165.\216\193\204\144\241\239N^\192c\202\218m\148\202\250\157\152~\160i&X9\236\193\US\151-w\165.\216\193\204\144" <> x
+{-# INLINE hash_aux #-}
+
+-- hardcoded tag of BIP0340/nonce
+hash_nonce :: BS.ByteString -> BS.ByteString
+hash_nonce x = SHA256.hash $
+  "\aIw4\167\155\203\&5[\155\140}\ETXO\DC2\FS\244\&4\215>\247-\218\EM\135\NULa\251R\191\235/\aIw4\167\155\203\&5[\155\140}\ETXO\DC2\FS\244\&4\215>\247-\218\EM\135\NULa\251R\191\235/" <> x
+{-# INLINE hash_nonce #-}
+
+-- hardcoded tag of BIP0340/challenge
+hash_challenge :: BS.ByteString -> BS.ByteString
+hash_challenge x = SHA256.hash $
+  "{\181-z\159\239X2>\177\191z@}\179\130\210\243\242\216\ESC\177\"OI\254Q\143mH\211|{\181-z\159\239X2>\177\191z@}\179\130\210\243\242\216\ESC\177\"OI\254Q\143mH\211|" <> x
+{-# INLINE hash_challenge #-}
+
 -- ecdsa ----------------------------------------------------------------------
 -- see https://www.rfc-editor.org/rfc/rfc6979, https://secg.org/sec1-v2.pdf
 
@@ -1211,7 +1230,7 @@
           u1   = remQ (e * s_inv)
           u2   = remQ (r * s_inv)
           capR = add (_mul u1) (mul_unsafe p u2)
-      in  if   capR == _ZERO
+      in  if   capR == _CURVE_ZERO
           then False
           else let Affine (modQ -> v) _ = affine capR
                in  v == r
diff --git a/ppad-secp256k1.cabal b/ppad-secp256k1.cabal
--- a/ppad-secp256k1.cabal
+++ b/ppad-secp256k1.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-secp256k1
-version:            0.2.1
+version:            0.2.2
 synopsis:           Schnorr signatures & ECDSA on the elliptic curve secp256k1
 license:            MIT
 license-file:       LICENSE
