solana-haskell-sdk-1.2.0.0: src/Network/Solana/NativePrograms/Secp256k1.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Client for the Secp256k1 native precompile program. The precompile
-- verifies secp256k1 (ECDSA/@ecrecover@) signatures included directly in an
-- instruction's data against a message and Ethereum address, also included
-- in that data; it does not compute a signature itself. Signing (producing
-- the signature and recovery id from a private key) is out of scope for this
-- SDK — callers must supply an already-computed signature.
module Network.Solana.NativePrograms.Secp256k1 where
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Network.Solana.Core.Crypto (SolanaPublicKey)
import Network.Solana.Core.Instruction
-- | Secp256k1 native precompile program address.
secp256k1ProgramId :: SolanaPublicKey
secp256k1ProgramId = "KeccakSecp256k11111111111111111111111111111"
-- | Offsets (within the transaction's instructions) of the signature,
-- Ethereum address, and message data used to verify a single secp256k1
-- signature. The u8 instruction indexes are absolute indexes into the
-- transaction's instruction list; index @0@ means the transaction's FIRST
-- instruction. Serialized as an 11-byte little-endian packed struct,
-- matching the Rust SDK layout.
data SecpSignatureOffsets = SecpSignatureOffsets
{ -- | Offset to the [signature, recovery_id] bytes (64 + 1 bytes).
ssoSignatureOffset :: Word16,
-- | Instruction index to find the signature.
ssoSignatureInstructionIndex :: Word8,
-- | Offset to the Ethereum address (20 bytes).
ssoEthAddressOffset :: Word16,
-- | Instruction index to find the Ethereum address.
ssoEthAddressInstructionIndex :: Word8,
-- | Offset to the start of the message data.
ssoMessageDataOffset :: Word16,
-- | Size of the message data, in bytes.
ssoMessageDataSize :: Word16,
-- | Instruction index to find the message data.
ssoMessageInstructionIndex :: Word8
}
deriving (Eq, Show)
instance Binary SecpSignatureOffsets where
put :: SecpSignatureOffsets -> Put
put (SecpSignatureOffsets sigOffset sigIdx ethOffset ethIdx msgOffset msgSize msgIdx) = do
putWord16le sigOffset
putWord8 sigIdx
putWord16le ethOffset
putWord8 ethIdx
putWord16le msgOffset
putWord16le msgSize
putWord8 msgIdx
get :: Get SecpSignatureOffsets
get =
SecpSignatureOffsets
<$> getWord16le
<*> getWord8
<*> getWord16le
<*> getWord8
<*> getWord16le
<*> getWord16le
<*> getWord8
-- | Secp256k1 instruction data: a pre-built byte string. The 'Binary'
-- instance mirrors 'Network.Solana.SplPrograms.Memo.MemoData': 'put' emits
-- the bytes verbatim (no discriminant, no length prefix) and 'get' consumes
-- all remaining input, so that 'mkInstruction' reproduces exactly the bytes
-- built by 'newSecp256k1Instruction'.
newtype Secp256k1InstructionData = Secp256k1InstructionData BS.ByteString
deriving (Eq, Show)
instance Binary Secp256k1InstructionData where
put :: Secp256k1InstructionData -> Put
put (Secp256k1InstructionData bs) = putByteString bs
get :: Get Secp256k1InstructionData
get = Secp256k1InstructionData . BL.toStrict <$> getRemainingLazyByteString
-- | Creates a Secp256k1 signature-verification instruction that checks a
-- single, precomputed ECDSA signature: this SDK's counterpart of the Rust
-- SDK's @new_secp256k1_instruction_with_signature@. Receives the 20-byte
-- Ethereum address the signature is expected to recover to, the 64-byte
-- compact ECDSA signature, the recovery id, and the RAW message bytes: the
-- precompile applies Keccak-256 to the message itself during verification,
-- so the supplied signature must have been produced over
-- @keccak256(message)@. The signature, address, and message are all
-- embedded in this single instruction's data (instruction index @0@), so
-- the precompile takes no accounts.
--
-- The recovery id selects which public key the signature recovers to; only
-- values 0-3 verify on-chain, but that range is not validated here,
-- matching the Rust builder.
--
-- Throws via 'error' if @ethAddress@ is not exactly 20 bytes or @signature@
-- is not exactly 64 bytes, mirroring the assertions in the Rust builder.
--
-- The built instruction hardcodes all instruction indexes to 0, so it must
-- be placed as the FIRST instruction of the transaction for verification to
-- read its own embedded data (matching the Rust SDK's
-- @new_secp256k1_instruction@).
newSecp256k1Instruction :: BS.ByteString -> BS.ByteString -> Word8 -> BS.ByteString -> Instruction
newSecp256k1Instruction ethAddress signature recoveryId message
| BS.length ethAddress /= 20 =
error ("newSecp256k1Instruction: ethAddress must be 20 bytes, got " <> show (BS.length ethAddress))
| BS.length signature /= 64 =
error ("newSecp256k1Instruction: signature must be 64 bytes, got " <> show (BS.length signature))
| otherwise =
mkInstruction secp256k1ProgramId [] (Secp256k1InstructionData (BL.toStrict (runPut body)))
where
offsets =
SecpSignatureOffsets
{ ssoSignatureOffset = 32,
ssoSignatureInstructionIndex = 0,
ssoEthAddressOffset = 12,
ssoEthAddressInstructionIndex = 0,
ssoMessageDataOffset = 97,
ssoMessageDataSize = fromIntegral (BS.length message),
ssoMessageInstructionIndex = 0
}
body = do
putWord8 1 -- count: a single signature to verify
put offsets
putByteString ethAddress
putByteString signature
putWord8 recoveryId
putByteString message