mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-17 10:15:51 +00:00
expose what's necessary for stealth addresses too"
This commit is contained in:
committed by
Alain Brenzikofer
parent
3ab14f5a93
commit
2bbc55619f
@@ -6,6 +6,10 @@ Crypto:
|
||||
Keccak-256, EIP-55 addresses and EIP-712 typed data hashing. Client-side
|
||||
signing only - no transaction construction and no chain writes; the resolver
|
||||
path remains read-only. See `plans/2026-08-05-eth-crypto-bindings.md`.
|
||||
- ERC-5564 stealth addresses (`Simplex.Messaging.Eth.Stealth`): a recipient
|
||||
publishes a spend/view meta-address, a sender derives a one-time address from
|
||||
it non-interactively, and only the recipient can find or spend from it. Adds
|
||||
`publicKeyTweakMul` and `publicKeyTweakAdd` to the secp256k1 bindings.
|
||||
|
||||
# 6.5.1
|
||||
|
||||
|
||||
@@ -275,3 +275,51 @@ published vectors rather than our own output:
|
||||
The EIP-712 and BIP-44 expectations were additionally reproduced by an
|
||||
independent pure-Python secp256k1 reference written for the purpose, so they are
|
||||
not just our implementation agreeing with itself.
|
||||
|
||||
## Addendum: ERC-5564 stealth addresses
|
||||
|
||||
`Simplex.Messaging.Eth.Stealth`, added for the names v2 gifting flow (rc3 §7.4).
|
||||
A recipient publishes a meta-address — a spending public key and a viewing
|
||||
public key — and a sender derives a one-time destination from it with no
|
||||
handshake. Only the viewing key finds those destinations; only the spending key
|
||||
spends from them.
|
||||
|
||||
### Why not `secp256k1_ecdh`
|
||||
|
||||
The ECDH module hashes the shared secret point with SHA-256 and offers no way to
|
||||
substitute a hash without a C callback. ERC-5564 hashes with keccak256. So the
|
||||
module stays disabled and the two core-API point operations are bound instead:
|
||||
|
||||
- `secp256k1_ec_pubkey_tweak_mul` → `publicKeyTweakMul`, for `r · P_view`
|
||||
- `secp256k1_ec_pubkey_tweak_add` → `publicKeyTweakAdd`, for `P_spend + s_h · G`
|
||||
|
||||
Both are in `secp256k1.h`, so no build flag changed. The recipient's key,
|
||||
`p_spend + s_h`, reuses the existing `privateKeyTweakAdd`.
|
||||
|
||||
### The parts the EIP does not specify
|
||||
|
||||
ERC-5564 fixes the algebra but not the encoding, and getting either wrong
|
||||
produces a wallet that is self-consistent and interoperable with nothing. From
|
||||
the EIP author's reference implementation
|
||||
(`Nerolation/EIP-Stealth-Address-ERC`, `minimal_poc.ipynb`):
|
||||
|
||||
- the shared secret point is serialized **uncompressed with the SEC1 prefix
|
||||
removed**, `x || y`, 64 bytes;
|
||||
- it is hashed with **keccak256**;
|
||||
- the **view tag is the first byte** of that hash.
|
||||
|
||||
That is the same encoding Ethereum uses to turn a public key into an address, so
|
||||
`addressFromPublicKey` performs the final step unchanged.
|
||||
|
||||
### Tests
|
||||
|
||||
13 examples in `CoreTests.EthCryptoTests`, 111 in the module overall. Beyond the
|
||||
round-trip and negative cases, two carry the weight:
|
||||
|
||||
- **Batch scanning.** A recipient scans 512 announcements addressed to someone
|
||||
else; about two pass the one-byte view tag by chance and none yields an address
|
||||
they control. The complementary test confirms they find all 64 of their own.
|
||||
This exercises the scan loop rather than a single derivation.
|
||||
- **Independent agreement.** The pinned vector was reproduced by a from-scratch
|
||||
pure-Python secp256k1 implementing the reference algorithm directly, sharing no
|
||||
code with libsecp256k1. Without that, a pin only records our own output.
|
||||
|
||||
@@ -152,6 +152,7 @@ library
|
||||
Simplex.Messaging.Eth.Address
|
||||
Simplex.Messaging.Eth.EIP712
|
||||
Simplex.Messaging.Eth.Keccak
|
||||
Simplex.Messaging.Eth.Stealth
|
||||
Simplex.Messaging.Names.Record
|
||||
Simplex.Messaging.Notifications.Client
|
||||
Simplex.Messaging.Notifications.Protocol
|
||||
|
||||
@@ -28,6 +28,8 @@ module Simplex.Messaging.Crypto.Secp256k1
|
||||
parsePublicKey,
|
||||
serializePublicKey,
|
||||
privateKeyTweakAdd,
|
||||
publicKeyTweakMul,
|
||||
publicKeyTweakAdd,
|
||||
signRecoverable,
|
||||
recoverPublicKey,
|
||||
isLowS,
|
||||
@@ -147,6 +149,11 @@ foreign import ccall "secp256k1_ec_pubkey_serialize"
|
||||
foreign import ccall "secp256k1_ec_seckey_tweak_add"
|
||||
c_ec_seckey_tweak_add :: Ptr Ctx -> Ptr Word8 -> Ptr Word8 -> IO CInt
|
||||
|
||||
foreign import ccall "secp256k1_ec_pubkey_tweak_mul"
|
||||
c_ec_pubkey_tweak_mul :: Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt
|
||||
foreign import ccall "secp256k1_ec_pubkey_tweak_add"
|
||||
c_ec_pubkey_tweak_add :: Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt
|
||||
|
||||
foreign import ccall "secp256k1_ecdsa_sign_recoverable"
|
||||
c_ecdsa_sign_recoverable :: Ptr Ctx -> Ptr RecSigRaw -> Ptr Word8 -> Ptr Word8 -> Ptr () -> Ptr () -> IO CInt
|
||||
|
||||
@@ -280,6 +287,36 @@ privateKeyTweakAdd (PrivateKey sk) tweak
|
||||
then Just . PrivateKey <$> packPtr skPtr privateKeySize
|
||||
else pure Nothing
|
||||
|
||||
-- | @tweak * P@. The scalar multiplication behind an ECDH shared secret.
|
||||
--
|
||||
-- Deliberately exposed instead of @secp256k1_ecdh@: that function hashes the
|
||||
-- resulting point with SHA-256, while ERC-5564 hashes it with keccak256 over
|
||||
-- the uncompressed coordinates. Returning the point leaves the hash to the
|
||||
-- caller.
|
||||
--
|
||||
-- 'Nothing' when the tweak is zero or out of range.
|
||||
publicKeyTweakMul :: PublicKey -> ByteString -> Maybe PublicKey
|
||||
publicKeyTweakMul = tweakPubKey c_ec_pubkey_tweak_mul
|
||||
|
||||
-- | @P + tweak * G@, the point addition stealth address derivation needs.
|
||||
--
|
||||
-- 'Nothing' when the tweak is out of range or the result is the point at
|
||||
-- infinity.
|
||||
publicKeyTweakAdd :: PublicKey -> ByteString -> Maybe PublicKey
|
||||
publicKeyTweakAdd = tweakPubKey c_ec_pubkey_tweak_add
|
||||
|
||||
tweakPubKey :: (Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt) -> PublicKey -> ByteString -> Maybe PublicKey
|
||||
tweakPubKey f pk tweak
|
||||
| B.length tweak /= privateKeySize = Nothing
|
||||
| otherwise = unsafePerformIO $
|
||||
allocaBytes pubKeyInternalSize $ \pkPtr ->
|
||||
withBS tweak $ \twPtr -> do
|
||||
withPubKeyRaw pk $ \src -> copyBytes (castPtr pkPtr) (castPtr src) pubKeyInternalSize
|
||||
rc <- f secp256k1Ctx pkPtr twPtr
|
||||
if rc == 1
|
||||
then Just . PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
else pure Nothing
|
||||
|
||||
-- | Sign a 32-byte digest. Deterministic (RFC 6979) and always low-@s@.
|
||||
signRecoverable :: PrivateKey -> ByteString -> Either String RecoverableSignature
|
||||
signRecoverable (PrivateKey sk) digest
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | ERC-5564 stealth addresses on secp256k1, scheme id 1 ("with view tags").
|
||||
--
|
||||
-- A recipient publishes a __meta-address__: two public keys, spending and
|
||||
-- viewing. A sender picks a random ephemeral key, derives a one-time address
|
||||
-- from it and the meta-address, and publishes the ephemeral public key. Only
|
||||
-- the recipient — who holds the viewing key — can tell which one-time addresses
|
||||
-- are theirs, and only they can spend from them.
|
||||
--
|
||||
-- The meta-address is not an address and never appears on chain, so publishing
|
||||
-- it discloses nothing beyond the ability to send to its owner.
|
||||
--
|
||||
-- == Interoperability
|
||||
--
|
||||
-- ERC-5564 specifies the algebra but /not/ how the shared-secret point is
|
||||
-- serialized before hashing, nor which hash is used. Those come from the EIP
|
||||
-- author's reference implementation
|
||||
-- (<https://github.com/Nerolation/EIP-Stealth-Address-ERC> @minimal_poc.ipynb@):
|
||||
--
|
||||
-- * the shared secret point is serialized __uncompressed with no SEC1 prefix__,
|
||||
-- as @x || y@, 64 bytes;
|
||||
-- * it is hashed with __keccak256__, not SHA-256 — which is why this module
|
||||
-- multiplies points directly rather than calling @secp256k1_ecdh@, whose
|
||||
-- built-in hash is SHA-256;
|
||||
-- * the __view tag is the first byte__ of that hash.
|
||||
--
|
||||
-- Encoding the point the same way an Ethereum address encodes a public key is
|
||||
-- not a coincidence, and it means 'Simplex.Messaging.Eth.Address' already
|
||||
-- performs the last step unchanged.
|
||||
module Simplex.Messaging.Eth.Stealth
|
||||
( StealthMetaAddress (..),
|
||||
ViewTag,
|
||||
StealthDestination (..),
|
||||
metaAddress,
|
||||
metaAddressBytes,
|
||||
parseMetaAddress,
|
||||
metaAddressSize,
|
||||
stealthDestination,
|
||||
stealthMatch,
|
||||
stealthPrivateKey,
|
||||
sharedSecretHash,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.Word (Word8)
|
||||
import Simplex.Messaging.Eth.Address (Address, addressFromPublicKey)
|
||||
import Simplex.Messaging.Eth.Keccak (keccak256)
|
||||
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
|
||||
-- | A recipient's published key pair: spending key, then viewing key.
|
||||
data StealthMetaAddress = StealthMetaAddress
|
||||
{ smaSpend :: S.PublicKey,
|
||||
smaView :: S.PublicKey
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | The first byte of the hashed shared secret. Lets a recipient discard about
|
||||
-- 255 announcements in 256 with one point multiplication and one hash, instead
|
||||
-- of also deriving an address for each.
|
||||
type ViewTag = Word8
|
||||
|
||||
-- | What a sender produces and publishes.
|
||||
data StealthDestination = StealthDestination
|
||||
{ -- | Where to send. Unlinkable to the meta-address it came from.
|
||||
sdAddress :: Address,
|
||||
-- | The ephemeral public key, compressed. Must reach the recipient, either
|
||||
-- in an announcement event or a message, or the destination is
|
||||
-- undiscoverable.
|
||||
sdEphemeralPubKey :: ByteString,
|
||||
sdViewTag :: ViewTag
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
metaAddress :: S.PrivateKey -> S.PrivateKey -> StealthMetaAddress
|
||||
metaAddress spend view =
|
||||
StealthMetaAddress {smaSpend = S.publicKey spend, smaView = S.publicKey view}
|
||||
|
||||
metaAddressSize :: Int
|
||||
metaAddressSize = 2 * S.compressedSize
|
||||
|
||||
-- | Spending key then viewing key, both compressed. 66 bytes.
|
||||
metaAddressBytes :: StealthMetaAddress -> ByteString
|
||||
metaAddressBytes ma = pub (smaSpend ma) <> pub (smaView ma)
|
||||
where
|
||||
pub = S.serializePublicKey S.Compressed
|
||||
|
||||
parseMetaAddress :: ByteString -> Either String StealthMetaAddress
|
||||
parseMetaAddress bs
|
||||
| B.length bs /= metaAddressSize =
|
||||
Left $ "meta-address: expected " <> show metaAddressSize <> " bytes, got " <> show (B.length bs)
|
||||
| otherwise = do
|
||||
let (spend, view) = B.splitAt S.compressedSize bs
|
||||
StealthMetaAddress <$> S.parsePublicKey spend <*> S.parsePublicKey view
|
||||
|
||||
-- | @keccak256(x || y)@ of @sk * P@ — the value both sides arrive at, the
|
||||
-- sender from the ephemeral key and the recipient from the viewing key.
|
||||
sharedSecretHash :: S.PrivateKey -> S.PublicKey -> Either String ByteString
|
||||
sharedSecretHash sk pk =
|
||||
case S.publicKeyTweakMul pk (S.unPrivateKey sk) of
|
||||
Nothing -> Left "stealth: shared secret is not a valid point"
|
||||
Just p -> Right . keccak256 . B.drop 1 $ S.serializePublicKey S.Uncompressed p
|
||||
|
||||
-- | Sender side. @ephemeral@ must be freshly random and used once: reusing it
|
||||
-- across recipients lets them link the destinations, and reusing it for one
|
||||
-- recipient produces the same address twice.
|
||||
stealthDestination :: S.PrivateKey -> StealthMetaAddress -> Either String StealthDestination
|
||||
stealthDestination ephemeral ma = do
|
||||
sh <- sharedSecretHash ephemeral (smaView ma)
|
||||
stealthPub <- tweakSpend (smaSpend ma) sh
|
||||
pure
|
||||
StealthDestination
|
||||
{ sdAddress = addressFromPublicKey stealthPub,
|
||||
sdEphemeralPubKey = S.serializePublicKey S.Compressed (S.publicKey ephemeral),
|
||||
sdViewTag = B.head sh
|
||||
}
|
||||
|
||||
-- | Recipient side. Returns the address when this announcement is ours.
|
||||
--
|
||||
-- The view tag is checked before the point addition, which is the whole reason
|
||||
-- it exists — a non-match costs one multiplication and one hash.
|
||||
stealthMatch :: S.PrivateKey -> S.PublicKey -> ByteString -> ViewTag -> Either String (Maybe Address)
|
||||
stealthMatch view spend ephemeralPub tag = do
|
||||
eph <- S.parsePublicKey ephemeralPub
|
||||
sh <- sharedSecretHash view eph
|
||||
if B.head sh /= tag
|
||||
then pure Nothing
|
||||
else Just . addressFromPublicKey <$> tweakSpend spend sh
|
||||
|
||||
-- | Recipient side. The key that controls a matched destination: @p_spend + s_h@.
|
||||
--
|
||||
-- Needs the spending key, which is why a viewing key can be delegated for
|
||||
-- scanning without granting the ability to spend.
|
||||
stealthPrivateKey :: S.PrivateKey -> S.PrivateKey -> ByteString -> Either String S.PrivateKey
|
||||
stealthPrivateKey spend view ephemeralPub = do
|
||||
eph <- S.parsePublicKey ephemeralPub
|
||||
sh <- sharedSecretHash view eph
|
||||
case S.privateKeyTweakAdd spend sh of
|
||||
Nothing -> Left "stealth: derived key out of range"
|
||||
Just sk -> Right sk
|
||||
|
||||
tweakSpend :: S.PublicKey -> ByteString -> Either String S.PublicKey
|
||||
tweakSpend spend sh = case S.publicKeyTweakAdd spend sh of
|
||||
Nothing -> Left "stealth: derived point out of range"
|
||||
Just p -> Right p
|
||||
@@ -26,6 +26,7 @@ import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Messaging.Eth.Address
|
||||
import Simplex.Messaging.Eth.EIP712
|
||||
import Simplex.Messaging.Eth.Keccak (keccak256)
|
||||
import Simplex.Messaging.Eth.Stealth
|
||||
import Test.Hspec hiding (fit, it)
|
||||
import Util
|
||||
|
||||
@@ -38,6 +39,7 @@ ethCryptoTests = do
|
||||
describe "BIP-44 derivation" derivationTests
|
||||
describe "EIP-55 addresses" eip55Tests
|
||||
describe "EIP-712 typed data" eip712Tests
|
||||
describe "ERC-5564 stealth addresses" stealthTests
|
||||
|
||||
-- helpers
|
||||
|
||||
@@ -396,3 +398,105 @@ bip39Vectors =
|
||||
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
|
||||
"01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998" )
|
||||
]
|
||||
|
||||
-- ERC-5564 stealth addresses.
|
||||
--
|
||||
-- The EIP fixes the algebra but not the serialization or the hash, so the
|
||||
-- pinned vector below is the interoperability contract: it follows the EIP
|
||||
-- author's reference implementation (keccak256 over the shared secret point as
|
||||
-- x||y, view tag = first byte). Anything that changes it breaks compatibility
|
||||
-- with every other ERC-5564 wallet, which is why it is pinned rather than
|
||||
-- computed.
|
||||
stealthTests :: Spec
|
||||
stealthTests = do
|
||||
it "sender and recipient derive the same address" $ do
|
||||
let d = right $ stealthDestination ephemeralKey aliceMeta
|
||||
right (stealthMatch aliceView (smaSpend aliceMeta) (sdEphemeralPubKey d) (sdViewTag d))
|
||||
`shouldBe` Just (sdAddress d)
|
||||
|
||||
it "the recipient's derived key controls that address" $ do
|
||||
let d = right $ stealthDestination ephemeralKey aliceMeta
|
||||
sk = right $ stealthPrivateKey aliceSpend aliceView (sdEphemeralPubKey d)
|
||||
addressFromPrivateKey sk `shouldBe` sdAddress d
|
||||
|
||||
it "the derived key actually signs for it" $ do
|
||||
let d = right $ stealthDestination ephemeralKey aliceMeta
|
||||
sk = right $ stealthPrivateKey aliceSpend aliceView (sdEphemeralPubKey d)
|
||||
digest = keccak256 "transfer"
|
||||
sig = right $ S.signRecoverable sk digest
|
||||
addressFromPublicKey (right $ S.recoverPublicKey sig digest) `shouldBe` sdAddress d
|
||||
|
||||
it "the view tag is the first byte of the hashed shared secret" $ do
|
||||
let d = right $ stealthDestination ephemeralKey aliceMeta
|
||||
sh = right $ sharedSecretHash aliceView (right . S.parsePublicKey $ sdEphemeralPubKey d)
|
||||
sdViewTag d `shouldBe` B.head sh
|
||||
|
||||
it "a different ephemeral key gives an unrelated address" $ do
|
||||
let d1 = right $ stealthDestination ephemeralKey aliceMeta
|
||||
d2 = right $ stealthDestination ephemeralKey2 aliceMeta
|
||||
sdAddress d1 `shouldNotBe` sdAddress d2
|
||||
|
||||
it "the viewing key alone does not spend" $ do
|
||||
-- Using the viewing key where the spending key belongs must not produce the
|
||||
-- address: this is what makes delegated scanning safe.
|
||||
let d = right $ stealthDestination ephemeralKey aliceMeta
|
||||
wrong = right $ stealthPrivateKey aliceView aliceView (sdEphemeralPubKey d)
|
||||
addressFromPrivateKey wrong `shouldNotBe` sdAddress d
|
||||
|
||||
it "another recipient never matches, over a batch of announcements" $ do
|
||||
-- Bob scans 512 announcements addressed to Alice. About two will pass the
|
||||
-- one-byte view tag by chance; none may yield an address Bob controls.
|
||||
let ds = [right $ stealthDestination (ephemeralN i) aliceMeta | i <- [1 .. 512 :: Int]]
|
||||
matches =
|
||||
[ a
|
||||
| d <- ds,
|
||||
Just a <- [right $ stealthMatch bobView (smaSpend bobMeta) (sdEphemeralPubKey d) (sdViewTag d)]
|
||||
]
|
||||
filter (`elem` map sdAddress ds) matches `shouldBe` []
|
||||
|
||||
it "the recipient finds their own in the same batch" $ do
|
||||
let ds = [right $ stealthDestination (ephemeralN i) aliceMeta | i <- [1 .. 64 :: Int]]
|
||||
found =
|
||||
[ a
|
||||
| d <- ds,
|
||||
Just a <- [right $ stealthMatch aliceView (smaSpend aliceMeta) (sdEphemeralPubKey d) (sdViewTag d)]
|
||||
]
|
||||
found `shouldBe` map sdAddress ds
|
||||
|
||||
it "agrees with an independent implementation of the scheme" $ do
|
||||
-- Cross-checked against a from-scratch pure-Python secp256k1 implementing
|
||||
-- the reference algorithm directly (scratchpad @stealth_ref.py@), sharing
|
||||
-- no code with libsecp256k1. Agreement here is what makes this an
|
||||
-- interoperability vector rather than a record of our own output.
|
||||
let d = right $ stealthDestination ephemeralKey aliceMeta
|
||||
checksumAddress (sdAddress d) `shouldBe` "0xbC287a4f0345cD7Fea8d523fBa25Aec4f0B29a6c"
|
||||
toHex (sdEphemeralPubKey d) `shouldBe` "029ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b"
|
||||
sdViewTag d `shouldBe` 224
|
||||
|
||||
describe "meta-address encoding" $ do
|
||||
it "round-trips" $
|
||||
parseMetaAddress (metaAddressBytes aliceMeta) `shouldBe` Right aliceMeta
|
||||
it "is 66 bytes, spending key first" $ do
|
||||
let bs = metaAddressBytes aliceMeta
|
||||
B.length bs `shouldBe` 66
|
||||
B.take 33 bs `shouldBe` S.serializePublicKey S.Compressed (smaSpend aliceMeta)
|
||||
it "rejects a wrong length" $
|
||||
parseMetaAddress (B.take 65 $ metaAddressBytes aliceMeta) `shouldSatisfy` isLeft
|
||||
it "rejects points not on the curve" $
|
||||
parseMetaAddress (B.replicate 66 0xAA) `shouldSatisfy` isLeft
|
||||
|
||||
aliceSpend, aliceView, bobSpend, bobView, ephemeralKey, ephemeralKey2 :: S.PrivateKey
|
||||
aliceSpend = right $ S.mkPrivateKey (hx "1111111111111111111111111111111111111111111111111111111111111111")
|
||||
aliceView = right $ S.mkPrivateKey (hx "2222222222222222222222222222222222222222222222222222222222222222")
|
||||
bobSpend = right $ S.mkPrivateKey (hx "3333333333333333333333333333333333333333333333333333333333333333")
|
||||
bobView = right $ S.mkPrivateKey (hx "4444444444444444444444444444444444444444444444444444444444444444")
|
||||
ephemeralKey = right $ S.mkPrivateKey (hx "5555555555555555555555555555555555555555555555555555555555555555")
|
||||
ephemeralKey2 = right $ S.mkPrivateKey (hx "6666666666666666666666666666666666666666666666666666666666666666")
|
||||
|
||||
aliceMeta, bobMeta :: StealthMetaAddress
|
||||
aliceMeta = metaAddress aliceSpend aliceView
|
||||
bobMeta = metaAddress bobSpend bobView
|
||||
|
||||
-- Distinct ephemeral keys for batch tests.
|
||||
ephemeralN :: Int -> S.PrivateKey
|
||||
ephemeralN i = right . S.mkPrivateKey . keccak256 . BC.pack $ "ephemeral " <> show i
|
||||
|
||||
Reference in New Issue
Block a user