diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## [1.2.0] - 2023-03-14
+
+### Changed
+
+- Make compatible with latest upstream LTS Haskell.
+
 ## [1.1.0] - 2023-11-01
 
 ### Changed
diff --git a/secp256k1-haskell.cabal b/secp256k1-haskell.cabal
--- a/secp256k1-haskell.cabal
+++ b/secp256k1-haskell.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           secp256k1-haskell
-version:        1.2.0
+version:        1.3.0
 synopsis:       Bindings for secp256k1
 description:    Sign and verify signatures using the secp256k1 library.
 category:       Crypto
diff --git a/src/Crypto/Secp256k1.hs b/src/Crypto/Secp256k1.hs
--- a/src/Crypto/Secp256k1.hs
+++ b/src/Crypto/Secp256k1.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE DuplicateRecordFields #-}
+
 -- |
 -- Module      : Crypto.Secp256k1
 -- License     : UNLICENSE
@@ -57,6 +58,15 @@
     tweakMulPubKey,
     combinePubKeys,
     tweakNegate,
+
+    -- * BIP340 Support
+    XOnlyPubKey,
+    deriveXOnlyPubKey,
+    Rand32,
+    mkRand32,
+    Bip340Sig,
+    signBip340,
+    verifyBip340,
   )
 where
 
diff --git a/src/Crypto/Secp256k1/Internal/Base.hs b/src/Crypto/Secp256k1/Internal/Base.hs
--- a/src/Crypto/Secp256k1/Internal/Base.hs
+++ b/src/Crypto/Secp256k1/Internal/Base.hs
@@ -40,6 +40,12 @@
     ecdsaSignatureSerializeCompact,
     ecdsaSignatureSerializeDer,
     ecdsaVerify,
+    keyPairCreate,
+    schnorrSign,
+    schnorrVerify,
+    xOnlyPubKeyFromPubKey,
+    xOnlyPubKeyParse,
+    xOnlyPubKeySerialize,
   )
 import Crypto.Secp256k1.Internal.Context (Ctx (..))
 import Crypto.Secp256k1.Internal.ForeignTypes
@@ -57,6 +63,7 @@
   )
 import Data.ByteString (ByteString)
 import Data.ByteString qualified as BS
+import Data.ByteString.Unsafe qualified as BU
 import Data.Hashable (Hashable (..))
 import Data.Maybe (fromJust, fromMaybe, isJust)
 import Data.String (IsString (..))
@@ -445,3 +452,122 @@
       valid_bs = bs_gen `suchThat` isJust
       bs_gen = secKey . BS.pack <$> replicateM 32 arbitraryBoundedRandom
       gen_key = fromJust <$> valid_bs
+
+-- | An x-only pubkey corresponds to the keys @(x,y)@ and @(x, -y)@.  The
+-- equality test only checks the x-coordinate.  An x-only pubkey serializes to 32
+-- bytes.
+--
+-- @since 1.3.0
+newtype XOnlyPubKey = XOnlyPubKey {get :: ByteString}
+  deriving (Eq, Hashable)
+
+instance Show XOnlyPubKey where
+  showsPrec _ = showsHex . (.get)
+
+newtype Bip340Sig = Bip340Sig {get :: ByteString}
+  deriving (Eq)
+
+instance Show Bip340Sig where
+  showsPrec _ = showsHex . (.get)
+
+newtype Rand32 = Rand32 {get :: ByteString}
+
+-- | Create a 'Rand32' value from a 32-byte 'ByteString'
+mkRand32 :: ByteString -> Maybe Rand32
+mkRand32 bytes
+  | BS.length bytes == 32 = Just $ Rand32 bytes
+  | otherwise = Nothing
+
+-- | Encode an x-only pubkey to bytes
+--
+-- @since 1.3.0
+exportXOnlyPubKey :: Ctx -> XOnlyPubKey -> ByteString
+exportXOnlyPubKey (Ctx fctx) pubKey = unsafePerformIO $
+  withForeignPtr fctx $ \ctx ->
+    unsafeUseByteString pubKey.get $ \(pub_key, _) ->
+      allocaBytes 32 $ \out_ptr -> do
+        result <- xOnlyPubKeySerialize ctx out_ptr pub_key
+        if isSuccess result
+          then packByteString (out_ptr, 32)
+          else free out_ptr >> error "Unable to serialize x-only pubkey"
+
+-- |  Import an x-only pubkey from bytes
+--
+-- @since 1.3.0
+importXOnlyPubKey :: Ctx -> ByteString -> Maybe XOnlyPubKey
+importXOnlyPubKey (Ctx fctx) inputBytes
+  | BS.length inputBytes == 32 = unsafePerformIO $
+      withForeignPtr fctx $ \ctx ->
+        unsafeUseByteString inputBytes $ \(input, _) -> do
+          pub_key <- mallocBytes 64
+          result <- xOnlyPubKeyParse ctx pub_key input
+          if isSuccess result
+            then Just . XOnlyPubKey <$> unsafePackByteString (pub_key, 64)
+            else pure Nothing
+  | otherwise = Nothing
+
+-- | Derive an x-only key from a pub key
+--
+-- @since 1.3.0
+deriveXOnlyPubKey :: Ctx -> PubKey -> XOnlyPubKey
+deriveXOnlyPubKey (Ctx fctx) pubKey = unsafePerformIO $
+  withForeignPtr fctx $ \ctx ->
+    unsafeUseByteString pubKey.get $ \(pub_key, _) -> do
+      xonly_pub_key <- mallocBytes 64
+      result <- xOnlyPubKeyFromPubKey ctx xonly_pub_key nullPtr pub_key
+      if isSuccess result
+        then XOnlyPubKey <$> unsafePackByteString (xonly_pub_key, 64)
+        else free xonly_pub_key >> error "Unable to derive x-only pubkey"
+
+-- | Sign a message according to BIP 340
+--
+-- @since 1.3.0
+signBip340 ::
+  Ctx ->
+  -- | Secret key
+  SecKey ->
+  -- | Message
+  Msg ->
+  -- | Randomness
+  Maybe Rand32 ->
+  -- | Signature
+  Maybe Bip340Sig
+signBip340 (Ctx fctx) theSecKey message rand32 = unsafePerformIO $
+  withForeignPtr fctx $ \ctx ->
+    unsafeUseByteString theSecKey.get $ \(sec_key, _) ->
+      allocaBytes 96 $ \key_pair -> do
+        -- The 'SecKey' API guarantees that the following call will succeed
+        _ <- keyPairCreate ctx key_pair sec_key
+        unsafeUseByteString message.get $ \(msg_ptr, _) -> do
+          withRand32 $ \(rand_ptr, _) -> do
+            sig_ptr <- mallocBytes 64
+            result <- schnorrSign ctx sig_ptr msg_ptr key_pair rand_ptr
+            if isSuccess result
+              then Just . Bip340Sig <$> unsafePackByteString (sig_ptr, 64)
+              else Nothing <$ free sig_ptr
+  where
+    withRand32 = maybe ($ (nullPtr, 0)) (unsafeUseByteString . (.get)) rand32
+
+-- | Verify a message according to BIP 340
+--
+-- @since 1.3.0
+verifyBip340 ::
+  Ctx ->
+  XOnlyPubKey ->
+  -- | Message
+  Msg ->
+  -- | Signature
+  Bip340Sig ->
+  Bool
+verifyBip340 (Ctx fctx) xOnlyPubKey message sig = unsafePerformIO $
+  withForeignPtr fctx $ \ctx ->
+    unsafeUseByteString xOnlyPubKey.get $ \(pub_key, _) ->
+      unsafeUseByteString message.get $ \(msg_ptr, messageSize) ->
+        unsafeUseByteString sig.get $ \(sig_ptr, _) ->
+          isSuccess
+            <$> schnorrVerify
+              ctx
+              sig_ptr
+              msg_ptr
+              (fromIntegral messageSize)
+              pub_key
diff --git a/src/Crypto/Secp256k1/Internal/BaseOps.hs b/src/Crypto/Secp256k1/Internal/BaseOps.hs
--- a/src/Crypto/Secp256k1/Internal/BaseOps.hs
+++ b/src/Crypto/Secp256k1/Internal/BaseOps.hs
@@ -11,6 +11,7 @@
 
 import Crypto.Secp256k1.Internal.ForeignTypes
   ( Compact64,
+    KeyPair,
     LCtx,
     Msg32,
     NonceFun,
@@ -21,6 +22,7 @@
     SerFlags,
     Sig64,
     Tweak32,
+    XOPubKey32,
   )
 import Foreign (FunPtr, Ptr)
 import Foreign.C
@@ -170,3 +172,67 @@
     -- | number of public keys
     CInt ->
     IO Ret
+
+foreign import ccall unsafe "secp256k1.h secp256k1_schnorrsig_sign"
+  schnorrSign ::
+    Ptr LCtx ->
+    -- | Signature
+    Ptr Sig64 ->
+    -- | Message
+    Ptr Msg32 ->
+    -- | Keypair
+    Ptr KeyPair ->
+    -- | Randomness
+    Ptr CUChar ->
+    IO CInt
+
+foreign import ccall unsafe "secp256k1.h secp256k1_schnorrsig_verify"
+  schnorrVerify ::
+    Ptr LCtx ->
+    -- | Signature
+    Ptr Sig64 ->
+    -- | Message
+    Ptr Msg32 ->
+    -- | Message length
+    CSize ->
+    -- | X-Only pubkey
+    Ptr XOPubKey32 ->
+    IO CInt
+
+foreign import ccall unsafe "secp256k1.h secp256k1_keypair_create"
+  keyPairCreate ::
+    Ptr LCtx ->
+    -- | Keypair
+    Ptr KeyPair ->
+    -- | Secret key
+    Ptr SecKey32 ->
+    IO CInt
+
+foreign import ccall unsafe "secp256k1.h secp256k1_xonly_pubkey_parse"
+  xOnlyPubKeyParse ::
+    Ptr LCtx ->
+    -- | Parsed X-Only pubkey
+    Ptr XOPubKey32 ->
+    -- | Input buffer
+    Ptr CUChar ->
+    IO CInt
+
+foreign import ccall unsafe "secp256k1.h secp256k1_xonly_pubkey_serialize"
+  xOnlyPubKeySerialize ::
+    Ptr LCtx ->
+    -- | Output buffer
+    Ptr CUChar ->
+    -- | X-Only pubkey
+    Ptr XOPubKey32 ->
+    IO CInt
+
+foreign import ccall unsafe "secp256k1.h secp256k1_xonly_pubkey_from_pubkey"
+  xOnlyPubKeyFromPubKey ::
+    Ptr LCtx ->
+    -- | X-only pubkey
+    Ptr XOPubKey32 ->
+    -- | Parity
+    Ptr CInt ->
+    -- | Pubkey
+    Ptr PubKey64 ->
+    IO CInt
diff --git a/src/Crypto/Secp256k1/Internal/ForeignTypes.hs b/src/Crypto/Secp256k1/Internal/ForeignTypes.hs
--- a/src/Crypto/Secp256k1/Internal/ForeignTypes.hs
+++ b/src/Crypto/Secp256k1/Internal/ForeignTypes.hs
@@ -16,6 +16,10 @@
 
 data PubKey64
 
+data XOPubKey32
+
+data KeyPair
+
 data Msg32
 
 data Sig64
diff --git a/test/Crypto/Secp256k1Spec.hs b/test/Crypto/Secp256k1Spec.hs
--- a/test/Crypto/Secp256k1Spec.hs
+++ b/test/Crypto/Secp256k1Spec.hs
@@ -72,6 +72,11 @@
       property $ combinePubKeyEmptyListTest ctx
     it "negates tweak" $ \ctx ->
       property $ negateTweakTest ctx
+  describe "BIP 340" $ do
+    it "verifies a signed message (null rand32)" $ \ctx ->
+      property $ bip340SigTestNull ctx
+    it "verifies a signed message (not-null rand32)" $ \ctx ->
+      property $ bip340SigTest ctx
 
 hexToBytes :: String -> BS.ByteString
 hexToBytes = decodeBase16 . assertBase16 . B8.pack
@@ -310,3 +315,24 @@
     Just minusOneTwk = tweakNegate ctx oneTwk
     Just twoKey = tweakAddSecKey ctx oneKey oneTwk
     Just subtracted = tweakAddSecKey ctx twoKey minusOneTwk
+
+bip340SigTestNull :: Ctx -> Assertion
+bip340SigTestNull ctx =
+  assertBool "verifies signature" $
+    verifyBip340 ctx pk theMsg sig
+  where
+    Just sk = secKey $ BS.replicate 32 0x01
+    pk = deriveXOnlyPubKey ctx $ derivePubKey ctx sk
+    Just theMsg = msg $ BS.replicate 32 0x02
+    Just sig = signBip340 ctx sk theMsg Nothing
+
+bip340SigTest :: Ctx -> Assertion
+bip340SigTest ctx =
+  assertBool "verifies signature" $
+    verifyBip340 ctx pk theMsg sig
+  where
+    Just sk = secKey $ BS.replicate 32 0x01
+    pk = deriveXOnlyPubKey ctx $ derivePubKey ctx sk
+    Just theMsg = msg $ BS.replicate 32 0x02
+    Just r = mkRand32 $ BS.replicate 32 0x03
+    Just sig = signBip340 ctx sk theMsg (Just r)
