diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,10 @@
 # Changelog
 
+- 0.5.1 (2025-12-27)
+  * Improves the constant-time semantics in wNAF scalar multiplication,
+    replacing "indexing via a constant-time selected value" by performing
+    a full window scan and selecting via mask.
+
 - 0.5.0 (2025-12-21)
   * We get a significant upgrade to all functionality by pulling in the
     ppad-fixed library for large unsigned and Montgomery-form integers.
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
@@ -283,9 +283,12 @@
     Storable.pokeByteOff ptr 31 (word8 w0)
 {-# INLINABLE unroll32 #-}
 
--- cheeky montgomery-assisted modQ
+-- modQ via conditional subtraction
 modQ :: Wider -> Wider
-modQ = S.from . S.to
+modQ x =
+  let !(Wider xw) = x
+      !(Wider qw) = _CURVE_Q
+  in  W.select x (x - _CURVE_Q) (CT.not (W.lt# xw qw))
 {-# INLINABLE modQ #-}
 
 -- bytewise xor
@@ -333,7 +336,11 @@
 
 -- Is field element?
 fe :: Wider -> Bool
-fe n = n > 0 && n < _CURVE_P
+fe n = case W.cmp_vartime n 0 of
+  GT -> case W.cmp_vartime n _CURVE_P of
+    LT -> True
+    _  -> False
+  _ -> False
 {-# INLINE fe #-}
 
 -- Is group element?
@@ -361,7 +368,7 @@
         !x2z1 = bx * az
         !y1z2 = ay * bz
         !y2z1 = by * az
-    in  CT.decide (CT.and# (C.eq x1z2 x2z1) (C.eq y1z2 y2z1))
+    in  CT.decide (CT.and (C.eq x1z2 x2z1) (C.eq y1z2 y2z1))
 
 -- | An ECC-flavoured alias for a secp256k1 point.
 type Pub = Projective
@@ -375,9 +382,9 @@
 
 -- Convert to projective coordinates.
 projective :: Affine -> Projective
-projective = \case
-  Affine 0 0 -> _CURVE_ZERO
-  Affine x y -> Projective x y 1
+projective (Affine x y)
+  | C.eq_vartime x 0 || C.eq_vartime y 0 = _CURVE_ZERO
+  | otherwise = Projective x y 1
 
 -- | secp256k1 generator point.
 _CURVE_G :: Projective
@@ -407,10 +414,7 @@
 
 -- Point is valid
 valid :: Projective -> Bool
-valid p = case affine p of
-  Affine x y
-    | C.sqr y /= weierstrass x -> False
-    | otherwise -> True
+valid (affine -> Affine x y) = C.eq_vartime (C.sqr y) (weierstrass x)
 
 -- (bip0340) return point with x coordinate == x and with even y coordinate
 --
@@ -425,10 +429,9 @@
 lift_vartime :: C.Montgomery -> Maybe Affine
 lift_vartime x = do
   let !c = weierstrass x
-  !y <- C.sqrt c
-  let !y_e | C.odd y   = negate y
+  !y <- C.sqrt_vartime c
+  let !y_e | C.odd_vartime y = negate y
            | otherwise = y
-  guard (C.sqr y_e == c)
   pure $! Affine x y_e
 
 even_y_vartime :: Projective -> Projective
@@ -554,7 +557,7 @@
 
 mul# :: Proj -> Limb4 -> (# () | Proj #)
 mul# (# px, py, pz #) s
-    | CT.decide (CT.not# (ge# s)) = (# () | #)
+    | CT.decide (CT.not (ge# s)) = (# () | #)
     | otherwise =
         let !(P gx gy gz) = _CURVE_G
             !(C.Montgomery o) = C.one
@@ -573,12 +576,12 @@
 ge# :: Limb4 -> CT.Choice
 ge# n =
   let !(Wider q) = _CURVE_Q
-  in  CT.and# (W.gt# n Z) (W.lt# n q)
+  in  CT.and (W.gt# n Z) (W.lt# n q)
 {-# INLINE ge# #-}
 
 mul_wnaf# :: ByteArray -> Int -> Limb4 -> (# () | Proj #)
 mul_wnaf# ctxArray ctxW ls
-    | CT.decide (CT.not# (ge# ls)) = (# () | #)
+    | CT.decide (CT.not (ge# ls)) = (# () | #)
     | otherwise =
         let !(P zx zy zz) = _CURVE_ZERO
             !(P gx gy gz) = _CURVE_G
@@ -606,9 +609,9 @@
               !is_zero     = CT.from_word_eq# b0 0##
               !c0          = CT.from_word# (Exts.and# w 1##)
               !off_nz      = Exts.minusWord# (Exts.plusWord# off0 abs_b) 1##
-              !off         = CT.select_word# off0 off_nz (CT.not# is_zero)
+              !off         = CT.select_word# off0 off_nz (CT.not is_zero)
 
-              !pr          = index_proj# ctxArray (Exts.word2Int# off)
+              !pr          = ct_index_proj# ctxArray off0 s off
               !neg_pr      = neg# pr
               !pt_zero     = select_proj# pr neg_pr c0
               !pt_nonzero  = select_proj# pr neg_pr bor
@@ -639,6 +642,27 @@
   in  (# x, y, z #)
 {-# INLINE index_proj# #-}
 
+-- Constant-time table lookup within a window.
+--
+-- Unconditionally scans all entries from 'base' to 'base + size - 1',
+-- selecting the one where 'index' equals 'target'.
+ct_index_proj#
+  :: ByteArray
+  -> Exts.Word#  -- ^ base index
+  -> Exts.Word#  -- ^ size of window
+  -> Exts.Word#  -- ^ target index
+  -> Proj
+ct_index_proj# arr base size target = loop 0## (# Z, Z, Z #) where
+  loop i acc
+    | Exts.isTrue# (i `Exts.geWord#` size) = acc
+    | otherwise =
+        let !idx  = Exts.plusWord# base i
+            !pt   = index_proj# arr (Exts.word2Int# idx)
+            !eq   = CT.from_word_eq# idx target
+            !nacc = select_proj# acc pt eq
+        in  loop (Exts.plusWord# i 1##) nacc
+{-# INLINE ct_index_proj# #-}
+
 -- ec arithmetic --------------------------------------------------------------
 
 -- Negate secp256k1 point.
@@ -720,7 +744,7 @@
 --   >>> sign_ecdsa' tex sec msg
 --   >>> sign_schnorr' tex sec msg aux
 precompute :: Context
-precompute = _precompute 8
+precompute = _precompute 4
 
 -- This is a highly-optimized version of a function originally
 -- translated from noble-secp256k1's "precompute". Points are stored in
@@ -865,7 +889,7 @@
   | not (fe x) = Nothing
   | otherwise = do
       let !mx = C.to x
-      !my <- C.sqrt (weierstrass mx)
+      !my <- C.sqrt_vartime (weierstrass mx)
       let !yodd = CT.decide (W.odd (C.retr my))
           !hodd = B.testBit h 0
       pure $!
@@ -995,7 +1019,7 @@
       t       = xor bytes_d (hash_aux a)
       rand    = hash_nonce (t <> bytes_p <> m)
       k'      = S.to (unsafe_roll32 rand)
-  guard (k' /= 0) -- negligible probability
+  guard (not (S.eq_vartime k' 0)) -- negligible probability
   pt <- _mul (S.retr k')
   let Affine (C.retr -> x_r) (C.retr -> y_r) = affine pt
       k         = S.select k' (negate k') (W.odd y_r)
@@ -1064,7 +1088,7 @@
       let dif = add pt0 (neg pt1)
       guard (dif /= _CURVE_ZERO)
       let Affine (C.from -> x_R) (C.from -> y_R) = affine dif
-      guard $ not (CT.decide (W.odd y_R) || x_R /= r) -- XX
+      guard $ not (CT.decide (W.odd y_R) || not (W.eq_vartime x_R r))
 {-# INLINE _verify_schnorr #-}
 
 -- hardcoded tag of BIP0340/aux
@@ -1113,7 +1137,7 @@
     ecdsa_r :: !Wider
   , ecdsa_s :: !Wider
   }
-  deriving (Eq, Generic)
+  deriving (Generic)
 
 instance Show ECDSA where
   show _ = "<ecdsa signature>"
@@ -1250,7 +1274,7 @@
       case mpair of
         Nothing -> pure Nothing
         Just (r, s)
-          | r == 0 -> sign_loop g -- negligible probability
+          | W.eq_vartime r 0 -> sign_loop g -- negligible probability
           | otherwise ->
               let !sig = Just $! ECDSA r s
               in  case ty of
@@ -1264,9 +1288,9 @@
   loop drbg = do
     bytes <- DRBG.gen mempty (fi _CURVE_Q_BYTES) drbg
     let can = bits2int bytes
-    if   can >= _CURVE_Q
-    then loop drbg
-    else pure can
+    case W.cmp_vartime can _CURVE_Q of
+      LT -> pure can
+      _  -> loop drbg -- 2 ^ -128 probability
 {-# INLINE gen_k #-}
 
 -- | Verify a "low-s" ECDSA signature for the provided message and
@@ -1285,7 +1309,7 @@
   -> ECDSA         -- ^ signature
   -> Bool
 verify_ecdsa m p sig@(ECDSA _ s)
-  | s > _CURVE_QH = False
+  | CT.decide (W.gt s _CURVE_QH) = False
   | otherwise = verify_ecdsa_unrestricted m p sig
 
 -- | The same as 'verify_ecdsa', except uses a 'Context' to optimise
@@ -1306,7 +1330,7 @@
   -> ECDSA         -- ^ signature
   -> Bool
 verify_ecdsa' tex m p sig@(ECDSA _ s)
-  | s > _CURVE_QH = False
+  | CT.decide (W.gt s _CURVE_QH) = False
   | otherwise = verify_ecdsa_unrestricted' tex m p sig
 
 -- | Verify an unrestricted ECDSA signature for the provided message and
@@ -1363,6 +1387,6 @@
   let capR = add pt0 pt1
   guard (capR /= _CURVE_ZERO)
   let Affine (S.to . C.retr -> v) _ = affine capR
-  guard (v == r)
+  guard (S.eq_vartime v r)
 {-# INLINE _verify_ecdsa_unrestricted #-}
 
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.5.0
+version:            0.5.1
 synopsis:           Schnorr signatures, ECDSA, and ECDH on the elliptic curve
                     secp256k1
 license:            MIT
@@ -38,7 +38,7 @@
     , bytestring >= 0.9 && < 0.13
     , ppad-hmac-drbg >= 0.1 && < 0.2
     , ppad-sha256 >= 0.2 && < 0.3
-    , ppad-fixed >= 0.1 && < 0.2
+    , ppad-fixed >= 0.1.2 && < 0.2
     , primitive >= 0.8 && < 0.10
 
 test-suite secp256k1-tests
diff --git a/test/Noble.hs b/test/Noble.hs
--- a/test/Noble.hs
+++ b/test/Noble.hs
@@ -1,4 +1,4 @@
-{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
+{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns -fno-warn-orphans #-}
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
@@ -18,6 +18,7 @@
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as TE
 import Data.Word.Wider (Wider(..))
+import qualified Data.Word.Wider as Wider
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (assertEqual, assertBool, assertFailure, testCase)
 
@@ -25,6 +26,9 @@
 decodeLenient bs = case B16.decode bs of
   Nothing -> error "bang"
   Just b -> b
+
+instance Eq ECDSA where
+  ECDSA r0 s0 == ECDSA r1 s1 = Wider.eq_vartime r0 r1 && Wider.eq_vartime s0 s1
 
 data Ecdsa = Ecdsa {
     ec_valid   :: ![(Int, ValidTest)]
