mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 15:07:32 +00:00
638 lines
31 KiB
Haskell
638 lines
31 KiB
Haskell
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
|
|
module Bots.StoreService.NamesWorkerTests (namesWorkerTests) where
|
|
|
|
import Bots.StoreService.FakeEthNode
|
|
import Bots.StoreService.NamesContractsTests (availableCall, maxCommitmentAgeCall, minCommitmentAgeCall, registrarOf, unhex)
|
|
import Bots.StoreService.NamesOrdersTests
|
|
import Bots.StoreService.NamesOwnerTests (answerExpiry, answerOwner, answerUnowned, byB, codeIn, committedPhase, dynamisOrder, failedPhase, labelBytes, ownerOfSelector, paidIn, rewrite, stored)
|
|
import Bots.StoreService.NamesPhasesTests (held, prompt, registrationPath, woken)
|
|
import Bots.StoreService.NamesStoreTests (reveal)
|
|
import Bots.StoreService.WebTests (bounded, capturingStderr, jsonObject, statusOf, webGet)
|
|
import Control.Applicative ((<|>))
|
|
import Control.Concurrent.Async (wait, withAsync)
|
|
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
|
|
import Control.Concurrent.STM (atomically, check)
|
|
import Control.Monad (replicateM_, void)
|
|
import Data.ByteString (ByteString)
|
|
import qualified Data.Aeson as J
|
|
import Data.IORef (readIORef)
|
|
import Data.Maybe (fromJust, fromMaybe, isJust)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime)
|
|
import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
|
|
import Data.Word (Word64)
|
|
import Network.HTTP.Client (defaultManagerSettings, newManager)
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
|
import Simplex.Messaging.Agent.Store.Common (withTransaction)
|
|
import Simplex.Messaging.Crypto (keccak256)
|
|
import Simplex.Messaging.Crypto.Secp256k1 (Secp256k1PrivateKey, signRecoverable)
|
|
import StoreService.Names.Abi (abiBytes, abiTuple, addressWord, AbiValue (..), selector, uint256)
|
|
import StoreService.Names.Chain (Block (..), ChainClient, Receipt (..), TxHash (..), TxRequest (..), blockAt, decodeSignedTx, newChainClient)
|
|
import StoreService.Names.Orders (Landing (..), OrdersEnv (..), confirmLanded)
|
|
import StoreService.Names.Phases (awaitChange)
|
|
import StoreService.Names.Registration (RegisterIntent (..), commitCalldata, registerIntentDigest, sponsorDomain)
|
|
import StoreService.Names.Reveal (intentOf)
|
|
import StoreService.Names.Sender
|
|
import StoreService.Names.Store
|
|
import StoreService.Names.Turns (labelTurnCount, withLabelTurn)
|
|
import StoreService.Names.Worker
|
|
import StoreService.Web.Http (holdMicros, readLimit)
|
|
import StoreService.Web.Names (Route (..), namesApp)
|
|
import StoreService.Web.Names.Registration (registrationHandler)
|
|
import StoreService.Web.Server (WebEnv (..), webApp)
|
|
import System.Timeout (timeout)
|
|
import Test.Hspec
|
|
|
|
namesWorkerTests :: Spec
|
|
namesWorkerTests = describe "StoreService names: worker" $ do
|
|
describe "commit" $ do
|
|
it "commits, waits for the reveal and revealAfter, and registers a code order, spending its code and publishing each change" testCommitWaitRegister
|
|
it "waits for a pending commit without sending another, following its fee-bumped replacement" testPendingCommit
|
|
it "goes to committed when the commit reverts UnexpiredCommitmentExists, in its dry run or mined, with the commitment's times" testUnexpiredCommitment
|
|
it "sends a commit mined out of gas again" testCommitOutOfGas
|
|
it "waits a second at the lapse second, and again when the dry run still reverts, then commits" testLapseSecond
|
|
it "clears a requeued order's stale intent before committing again after a lapse, and a third party's commitment's" testStaleIntent
|
|
it "keeps the phase while the latest block needs more than the fee cap, then commits" testFeeAboveCap
|
|
describe "writes" $ do
|
|
it "writes nothing when the registration changed since it was read" testLostRace
|
|
it "writes in the name's turn" testInNameTurn
|
|
describe "register" $ do
|
|
it "ends the later of two wallets' paid orders for one name taken, with its value kept" testTwoWallets
|
|
it "ends taken on any revert while the name is not available, releasing a code" testRevertTaken
|
|
it "ends failed on a revert while the name is available, alerting on InsufficientAllowance and keeping a code" testRevertFailed
|
|
it "clears the reveal on InvalidNonce, staying committed" testInvalidNonce
|
|
it "relays no intent past its deadline, ending failed" testExpiredIntent
|
|
it "writes failed only once no register can still be mined, and registered when the owner holds the name" testFailedOnlySettled
|
|
describe "lapse" $ do
|
|
it "ends a committed order not revealed by revealBefore failed, keeping its code" testLapseFailed
|
|
it "wakes a held poll with failed at the lapse, and answers an old version at once after it" testLapsePoll
|
|
describe "restart" $
|
|
it "resumes every phase after a restart, a commit saved but not recorded included" testRestart
|
|
describe "lanes" $ do
|
|
it "starts a worker for each registration in its phases" testRunWorkers
|
|
it "releases the codes of code orders failed for 30 days, and no others" testCodeSweep
|
|
describe "confirmation depth" $
|
|
it "judges a register landed by ownerOf only once the owner holds the name at the confirmation depth" testLandedAtDepth
|
|
describe "sender" $
|
|
it "bounds the wait for a transaction's outcome" testOutcomeBound
|
|
|
|
-- fixtures
|
|
|
|
data W = W
|
|
{ wCtx :: Ctx,
|
|
wChain :: ChainClient,
|
|
wSender :: Sender,
|
|
wWorker :: Worker
|
|
}
|
|
|
|
-- | The worker over the orders' store and fake node, with the registration poll as namesRoutes registers it.
|
|
withWorker :: (W -> IO a) -> IO a
|
|
withWorker action = withOrders $ \ctx@Ctx {cNode, cOrders, cWeb} -> do
|
|
answerCall cNode fakeController minCommitmentAgeCall $ Returns $ uint256 minAge
|
|
answerCall cNode fakeController maxCommitmentAgeCall $ Returns $ uint256 maxAge
|
|
answerUnowned ctx "alicebakery"
|
|
let web = cWeb {weOrders = Just cOrders}
|
|
routes path = registrationRoute path <|> codeRoutes cOrders (weProviders web) (weWaiters web) path
|
|
registrationRoute = \case
|
|
["api", "registration", rid] -> Just Route {rMethod = "GET", rLimit = readLimit, rHandler = registrationHandler cOrders holdMicros rid}
|
|
_ -> Nothing
|
|
app req respond = fromMaybe (webApp web req respond) (namesApp (weHttp web) routes req respond)
|
|
mgr <- newManager defaultManagerSettings
|
|
Warp.testWithApplication (pure app) $ \port -> started ctx {cWeb = web, cClient = (mgr, "http://127.0.0.1:" <> show port)} >>= action
|
|
|
|
-- | A new sender and worker over the same store, as after a restart.
|
|
started :: Ctx -> IO W
|
|
started wCtx@Ctx {cNode, cStore, cRandom, cClock, cOrders} = do
|
|
wChain <- newChainClient cRandom $ fakeChainConfig cNode
|
|
wSender <- newSender cRandom wChain cStore (readIORef cClock)
|
|
wWorker <- newWorker cOrders wChain wSender
|
|
pure W {wCtx, wChain, wSender, wWorker}
|
|
|
|
minAge, maxAge :: Word64
|
|
minAge = 60
|
|
maxAge = 86400
|
|
|
|
orderSeconds :: Word64
|
|
orderSeconds = floor $ utcTimeToPOSIXSeconds orderTime
|
|
|
|
expiry :: Word64
|
|
expiry = orderSeconds + 2 * 31536000
|
|
|
|
at :: Word64 -> UTCTime
|
|
at = posixSecondsToUTCTime . fromIntegral
|
|
|
|
day :: NominalDiffTime
|
|
day = 86400
|
|
|
|
-- | Committed a minute before the order.
|
|
openPhase :: NameRegistration -> NameRegistration
|
|
openPhase r = (committedPhase r) {nrRevealAfter = Just (orderSeconds - minAge), nrRevealBefore = Just (orderSeconds - minAge + maxAge)}
|
|
|
|
-- | The reveal the page sends for the label, its intent signed with the owner's key.
|
|
signedReveal :: Ctx -> Secp256k1PrivateKey -> Text -> NameRegistration -> IO NameReveal
|
|
signedReveal Ctx {cRandom, cOrders} key label r = do
|
|
let rv = reveal {rvLabel = label}
|
|
RegisterIntent {riRegistration} = intentOf (oeConfig cOrders) r rv
|
|
sig <- either (error . show) id <$> signRecoverable cRandom key (registerIntentDigest (sponsorDomain fakeChainId fakeSponsor) riRegistration (rvNonce rv) (rvDeadline rv))
|
|
pure rv {rvSignature = sig}
|
|
|
|
-- | The registration with the page's reveal accepted.
|
|
revealed :: Ctx -> Secp256k1PrivateKey -> Text -> NameRegistration -> IO NameRegistration
|
|
revealed ctx key label r = signedReveal ctx key label r >>= \rv -> rewrite ctx r (\o -> o {nrReveal = Just rv})
|
|
|
|
-- | Steps until the worker has nothing to do at once.
|
|
steps :: W -> NameRegistration -> IO Next
|
|
steps W {wWorker} NameRegistration {nrId} = go (10 :: Int)
|
|
where
|
|
go 0 = fail "the worker kept stepping"
|
|
go n =
|
|
stepRegistration wWorker nrId >>= \case
|
|
Again -> go (n - 1)
|
|
next -> pure next
|
|
|
|
shape :: Next -> String
|
|
shape = \case
|
|
Done -> "Done"
|
|
Again -> "Again"
|
|
WaitUntil v t -> "WaitUntil " <> show v <> " " <> show t
|
|
Retry v reason -> "Retry " <> show v <> " " <> T.unpack reason
|
|
Await p -> "Await " <> show (ptHash p)
|
|
|
|
awaiting :: Next -> IO PendingTx
|
|
awaiting = \case
|
|
Await p -> pure p
|
|
next -> fail $ "not awaiting a transaction: " <> shape next
|
|
|
|
waitingUntil :: Next -> IO UTCTime
|
|
waitingUntil = \case
|
|
WaitUntil _ t -> pure t
|
|
next -> fail $ "not waiting: " <> shape next
|
|
|
|
retrying :: Next -> IO ()
|
|
retrying = \case
|
|
Retry {} -> pure ()
|
|
next -> fail $ "not retrying: " <> shape next
|
|
|
|
done :: Next -> IO ()
|
|
done = \case
|
|
Done -> pure ()
|
|
next -> fail $ "not done: " <> shape next
|
|
|
|
-- | The raw transactions the node received, in the order they were sent.
|
|
sent :: W -> IO [ByteString]
|
|
sent W {wCtx = Ctx {cNode}} = sentTransactions cNode
|
|
|
|
newestHash :: W -> IO TxHash
|
|
newestHash w = TxHash . keccak256 . last <$> sent w
|
|
|
|
-- | Mines the newest transaction sent, with this status, and the blocks after it that settle it; the receipt's block.
|
|
mineNewest :: W -> Bool -> IO Word64
|
|
mineNewest w@W {wCtx = Ctx {cNode}, wSender} ok = do
|
|
TxHash h <- newestHash w
|
|
block <- mine cNode [(h, ok)]
|
|
replicateM_ 2 $ mine cNode []
|
|
followTransactions wSender
|
|
pure block
|
|
|
|
blockTime :: W -> Word64 -> IO Word64
|
|
blockTime W {wChain} n = either (error . show) blockTimestamp <$> blockAt wChain n
|
|
|
|
commitmentsCall :: NameRegistration -> ByteString
|
|
commitmentsCall NameRegistration {nrCommitment} = selector "commitments(bytes32)" <> nrCommitment
|
|
|
|
-- | The commitment's time on chain, 0 for none.
|
|
answerCommitment :: W -> NameRegistration -> Word64 -> IO ()
|
|
answerCommitment W {wCtx = Ctx {cNode}} r t = answerCall cNode fakeController (commitmentsCall r) $ Returns $ uint256 t
|
|
|
|
answerAvailable :: W -> Bool -> IO ()
|
|
answerAvailable W {wCtx = Ctx {cNode}} available = answerCall cNode fakeController availableCall $ Returns $ uint256 (if available then 1 else 0 :: Int)
|
|
|
|
-- | The dry runs revert with the contracts' error of this signature.
|
|
dryRunsRevert :: W -> ByteString -> ByteString -> IO ()
|
|
dryRunsRevert W {wCtx = Ctx {cNode}} signature args = setFault cNode "eth_estimateGas" $ revertFault $ selector signature <> args
|
|
|
|
dryRunsPass :: W -> IO ()
|
|
dryRunsPass W {wCtx = Ctx {cNode}} = clearFault cNode "eth_estimateGas"
|
|
|
|
-- | The action's result, failing unless it published the version it wrote to a poll held at the version before.
|
|
published :: W -> NameRegistration -> IO a -> IO a
|
|
published W {wCtx = ctx} r action = do
|
|
NameRegistration {nrId, nrVersion} <- stored ctx r
|
|
poll <- held ctx (0, 1) $ registrationPath nrId <> "?seenVersion=" <> show nrVersion
|
|
a <- action
|
|
(status, view) <- woken "the registration poll" poll
|
|
NameRegistration {nrVersion = written} <- stored ctx r
|
|
(status, field "version" view, written > nrVersion) `shouldBe` (200, Just (J.toJSON written), True)
|
|
pure a
|
|
|
|
traverseFst :: (a -> IO c) -> (a, b) -> IO (c, b)
|
|
traverseFst f (a, b) = (,b) <$> f a
|
|
|
|
codeOf :: Ctx -> PrepaidCode -> IO PrepaidCode
|
|
codeOf Ctx {cStore} PrepaidCode {pcId} = withTransaction cStore $ \db -> fromJust <$> getPrepaidCodeById db pcId
|
|
|
|
nowOf :: Ctx -> IO UTCTime
|
|
nowOf Ctx {cClock} = readIORef cClock
|
|
|
|
-- tests
|
|
|
|
testCommitWaitRegister :: IO ()
|
|
testCommitWaitRegister = withWorker $ \w@W {wCtx = ctx} -> do
|
|
(r, code) <- codeIn ctx 8 aliceOrder id
|
|
answerExpiry ctx "alicebakery" expiry
|
|
answerCommitment w r 0
|
|
void $ published w r $ steps w r >>= awaiting
|
|
[commitRaw] <- sent w
|
|
committing <- stored ctx r
|
|
(nrPhase committing, nrCommitTx committing, nrVersion committing) `shouldBe` (PCommitting, Just (TxHash $ keccak256 commitRaw), nrVersion r + 1)
|
|
-- the commit in flight is waited for
|
|
steps w r >>= awaiting >>= (`shouldBe` TxHash (keccak256 commitRaw)) . ptHash
|
|
length <$> sent w `shouldReturn` 1
|
|
commitBlock <- mineNewest w True
|
|
t <- blockTime w commitBlock
|
|
answerCommitment w r t
|
|
-- committed, waiting for the reveal until the lapse
|
|
published w r (steps w r >>= waitingUntil) `shouldReturn` at (t + maxAge + 1)
|
|
committed <- stored ctx r
|
|
(nrPhase committed, nrRevealAfter committed, nrRevealBefore committed, nrCommitTx committed, nrCommitBlock committed)
|
|
`shouldBe` (PCommitted, Just (t + minAge), Just (t + maxAge), Just (TxHash $ keccak256 commitRaw), Just commitBlock)
|
|
-- the page reveals before revealAfter
|
|
void $ revealed ctx keyA "alicebakery" r
|
|
steps w r >>= waitingUntil >>= (`shouldBe` at (t + minAge))
|
|
advance ctx $ fromIntegral (t + minAge) - fromIntegral orderSeconds
|
|
void $ published w r $ steps w r >>= awaiting
|
|
registerTx <- newestHash w
|
|
registering <- stored ctx r
|
|
(nrPhase registering, nrRegisterTx registering) `shouldBe` (PRegistering, Just registerTx)
|
|
registerBlock <- mineNewest w True
|
|
now <- nowOf ctx
|
|
published w r (steps w r) >>= done
|
|
x <- stored ctx r
|
|
(nrPhase x, nrRegisterTx x, nrRegisterBlock x, nrExpiresAt x, nrRegisteredAt x, nrLive x) `shouldBe` (PRegistered, Just registerTx, Just registerBlock, Just expiry, Just now, False)
|
|
isJust . pcSpentAt <$> codeOf ctx code `shouldReturn` True
|
|
steps w r >>= done
|
|
|
|
testPendingCommit :: IO ()
|
|
testPendingCommit = withWorker $ \w@W {wCtx = ctx@Ctx {cNode}, wSender} -> do
|
|
r <- paidIn ctx aliceOrder id
|
|
answerCommitment w r 0
|
|
first <- ptHash <$> (steps w r >>= awaiting)
|
|
replicateM_ 3 $ steps w r >>= awaiting >>= (`shouldBe` first) . ptHash
|
|
length <$> sent w `shouldReturn` 1
|
|
-- the market's fees pass the commit's, so the sender replaces it
|
|
setFees cNode (2 * fakeTip) (2 * fakeBaseFee)
|
|
advance ctx 181
|
|
followTransactions wSender
|
|
replacement <- newestHash w
|
|
replacement `shouldNotBe` first
|
|
inFlight <- stored ctx r
|
|
steps w r >>= awaiting >>= (`shouldBe` replacement) . ptHash
|
|
x <- stored ctx r
|
|
(nrPhase x, nrCommitTx x, nrVersion x) `shouldBe` (PCommitting, Just replacement, nrVersion inFlight + 1)
|
|
length <$> sent w `shouldReturn` 2
|
|
|
|
testUnexpiredCommitment :: IO ()
|
|
testUnexpiredCommitment = withWorker $ \w@W {wCtx = ctx@Ctx {cNode}} -> do
|
|
-- an earlier commit of the commitment mined between the read and the dry run
|
|
r <- paidIn ctx aliceOrder id
|
|
let t = orderSeconds - 600
|
|
answerCommitment w r t
|
|
answerCallOnce cNode fakeController (commitmentsCall r) $ Returns $ uint256 (0 :: Int)
|
|
dryRunsRevert w "UnexpiredCommitmentExists(bytes32)" (nrCommitment r)
|
|
steps w r >>= waitingUntil >>= (`shouldBe` addUTCTime 1 orderTime)
|
|
sent w `shouldReturn` []
|
|
stored ctx r `shouldReturn` r
|
|
dryRunsPass w
|
|
steps w r >>= waitingUntil >>= (`shouldBe` at (t + maxAge + 1))
|
|
x <- stored ctx r
|
|
(nrPhase x, nrRevealAfter x, nrRevealBefore x, nrCommitTx x) `shouldBe` (PCommitted, Just (t + minAge), Just (t + maxAge), Nothing)
|
|
-- a commit sent, mined with status 0 after another commit of it
|
|
r2 <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 2} id
|
|
answerCommitment w r2 0
|
|
void $ steps w r2 >>= awaiting
|
|
void $ mineNewest w False
|
|
let t2 = orderSeconds - 30
|
|
answerCommitment w r2 t2
|
|
steps w r2 >>= waitingUntil >>= (`shouldBe` at (t2 + maxAge + 1))
|
|
x2 <- stored ctx r2
|
|
(nrPhase x2, nrRevealBefore x2, nrCommitTx x2, nrCommitBlock x2) `shouldBe` (PCommitted, Just (t2 + maxAge), Nothing, Nothing)
|
|
length <$> sent w `shouldReturn` 1
|
|
|
|
testCommitOutOfGas :: IO ()
|
|
testCommitOutOfGas = withWorker $ \w@W {wCtx = ctx} -> do
|
|
r <- paidIn ctx aliceOrder id
|
|
answerCommitment w r 0
|
|
first <- ptHash <$> (steps w r >>= awaiting)
|
|
void $ mineNewest w False
|
|
second <- ptHash <$> (steps w r >>= awaiting)
|
|
second `shouldNotBe` first
|
|
[_, raw] <- sent w
|
|
trNonce <$> either error pure (decodeSignedTx raw) `shouldReturn` 1
|
|
x <- stored ctx r
|
|
(nrPhase x, nrCommitTx x) `shouldBe` (PCommitting, Just second)
|
|
|
|
testLapseSecond :: IO ()
|
|
testLapseSecond = withWorker $ \w@W {wCtx = ctx} -> do
|
|
r <- paidIn ctx aliceOrder id
|
|
-- a live commitment with less than the reveal margin left is not reused
|
|
answerCommitment w r (orderSeconds + 600 - maxAge)
|
|
steps w r >>= waitingUntil >>= (`shouldBe` addUTCTime 601 orderTime)
|
|
-- at the lapse second it can neither register nor be committed again
|
|
advance ctx 600
|
|
steps w r >>= waitingUntil >>= (`shouldBe` addUTCTime 601 orderTime)
|
|
stored ctx r `shouldReturn` r
|
|
advance ctx 1
|
|
-- the node's latest block is still at the lapse second
|
|
dryRunsRevert w "UnexpiredCommitmentExists(bytes32)" (nrCommitment r)
|
|
steps w r >>= waitingUntil >>= (`shouldBe` addUTCTime 602 orderTime)
|
|
sent w `shouldReturn` []
|
|
stored ctx r `shouldReturn` r
|
|
dryRunsPass w
|
|
advance ctx 1
|
|
void $ steps w r >>= awaiting
|
|
nrPhase <$> stored ctx r `shouldReturn` PCommitting
|
|
length <$> sent w `shouldReturn` 1
|
|
|
|
testStaleIntent :: IO ()
|
|
testStaleIntent = withWorker $ \w@W {wCtx = ctx} -> do
|
|
-- requeued by Try again with its intent, its commitment lapsed
|
|
r <- paidIn ctx aliceOrder $ \o -> (openPhase o) {nrPhase = PQueued, nrReveal = Just reveal}
|
|
answerCommitment w r (orderSeconds - maxAge - 1)
|
|
void $ steps w r >>= awaiting
|
|
x <- stored ctx r
|
|
commitTx <- newestHash w
|
|
(nrPhase x, nrReveal x, nrRevealAfter x, nrRevealBefore x, nrCommitTx x, nrVersion x) `shouldBe` (PCommitting, Nothing, Nothing, Nothing, Just commitTx, nrVersion r + 2)
|
|
-- requeued with its intent while a third party's commit of the commitment is live: not ours, so the intent goes
|
|
r2 <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 2} $ \o -> (openPhase o) {nrPhase = PQueued, nrReveal = Just reveal}
|
|
answerCommitment w r2 (orderSeconds - 60)
|
|
steps w r2 >>= waitingUntil >>= (`shouldBe` at (orderSeconds - 60 + maxAge + 1))
|
|
x2 <- stored ctx r2
|
|
(nrPhase x2, nrReveal x2, nrCommitTx x2, nrRevealBefore x2) `shouldBe` (PCommitted, Nothing, Nothing, Just (orderSeconds - 60 + maxAge))
|
|
length <$> sent w `shouldReturn` 1
|
|
|
|
testFeeAboveCap :: IO ()
|
|
testFeeAboveCap = withWorker $ \w@W {wCtx = ctx@Ctx {cNode}} -> do
|
|
r <- paidIn ctx aliceOrder id
|
|
answerCommitment w r 0
|
|
setFees cNode fakeTip fakeMaxFeePerGas
|
|
steps w r >>= retrying
|
|
stored ctx r `shouldReturn` r
|
|
sent w `shouldReturn` []
|
|
setFees cNode fakeTip fakeBaseFee
|
|
void $ steps w r >>= awaiting
|
|
nrPhase <$> stored ctx r `shouldReturn` PCommitting
|
|
|
|
testLostRace :: IO ()
|
|
testLostRace = withWorker $ \w@W {wCtx = ctx@Ctx {cNode}, wWorker} -> do
|
|
r <- paidIn ctx aliceOrder openPhase >>= revealed ctx keyA "alicebakery"
|
|
answerAvailable w False
|
|
dryRunsRevert w "NameNotAvailable(string)" $ abiTuple [Dynamic $ abiBytes "alicebakery"]
|
|
-- held at the register check's ownerOf read, after the registration was read
|
|
(stepping, gate) <- pausedAt cNode "eth_call" $ stepRegistration wWorker (nrId r)
|
|
changed <- rewrite ctx r $ \o -> o {nrRevealAfter = (+ 1) <$> nrRevealAfter o}
|
|
releaseGate gate
|
|
timeout prompt (wait stepping) >>= maybe (fail "the step did not finish") (pure . shape) >>= (`shouldBe` "Again")
|
|
stored ctx r `shouldReturn` changed
|
|
-- held at the commitment's read, a fresh worker's first chain read
|
|
r2 <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 2} id
|
|
answerCommitment w r2 $ orderSeconds - 60
|
|
W {wWorker = fresh} <- started ctx
|
|
(stepping', gate') <- pausedAt cNode "eth_call" $ stepRegistration fresh (nrId r2)
|
|
changed' <- rewrite ctx r2 $ \o -> o {nrError = Just "changed"}
|
|
releaseGate gate'
|
|
timeout prompt (wait stepping') >>= maybe (fail "the step did not finish") (pure . shape) >>= (`shouldBe` "Again")
|
|
stored ctx r2 `shouldReturn` changed'
|
|
|
|
testInNameTurn :: IO ()
|
|
testInNameTurn = withWorker $ \w@W {wCtx = ctx@Ctx {cOrders}} -> do
|
|
r <- paidIn ctx aliceOrder committedPhase
|
|
advance ctx $ day + 1
|
|
holding <- newEmptyMVar
|
|
release <- newEmptyMVar
|
|
withAsync (withLabelTurn (oeTurns cOrders) (nrLabelHash r) $ putMVar holding () >> takeMVar release) $ \_ -> do
|
|
takeMVar holding
|
|
withAsync (steps w r) $ \stepping -> do
|
|
-- the lapse's write waits for the turn the holder has
|
|
timeout prompt (atomically $ labelTurnCount (oeTurns cOrders) (nrLabelHash r) >>= check . (== 2)) >>= maybe (fail "the worker did not queue on the turn") pure
|
|
stored ctx r `shouldReturn` r
|
|
putMVar release ()
|
|
timeout prompt (wait stepping) >>= maybe (fail "the step did not finish") done
|
|
nrPhase <$> stored ctx r `shouldReturn` PFailed
|
|
|
|
testTwoWallets :: IO ()
|
|
testTwoWallets = withWorker $ \w@W {wCtx = ctx} -> do
|
|
a <- paidIn ctx aliceOrder openPhase >>= revealed ctx keyA "alicebakery"
|
|
b <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 2} openPhase >>= revealed ctx keyB "alicebakery"
|
|
answerExpiry ctx "alicebakery" expiry
|
|
void $ steps w a >>= awaiting
|
|
void $ mineNewest w True
|
|
steps w a >>= done
|
|
nrPhase <$> stored ctx a `shouldReturn` PRegistered
|
|
answerOwner ctx "alicebakery" (nrOwner a)
|
|
answerAvailable w False
|
|
dryRunsRevert w "NameNotAvailable(string)" $ abiTuple [Dynamic $ abiBytes "alicebakery"]
|
|
published w b (steps w b) >>= done
|
|
x <- stored ctx b
|
|
(nrPhase x, nrLive x, nrPrice x, nrPayment x, nrError x) `shouldBe` (PTaken, True, nrPrice b, nrPayment b, Nothing)
|
|
length <$> sent w `shouldReturn` 1
|
|
|
|
testRevertTaken :: IO ()
|
|
testRevertTaken = withWorker $ \w@W {wCtx = ctx@Ctx {cNode}} -> do
|
|
(r, code) <- codeIn ctx 8 aliceOrder openPhase >>= traverseFst (revealed ctx keyA "alicebakery")
|
|
answerAvailable w False
|
|
-- a reason kept in the message without data: not one of the contracts' errors
|
|
setFault cNode "eth_estimateGas" $ RpcError 3 "execution reverted: ERC721: invalid token ID" Nothing
|
|
published w r (steps w r) >>= done
|
|
x <- stored ctx r
|
|
(nrPhase x, nrLive x) `shouldBe` (PTaken, False)
|
|
PrepaidCode {pcReservedBy, pcSpentAt} <- codeOf ctx code
|
|
(pcReservedBy, pcSpentAt) `shouldBe` (Nothing, Nothing)
|
|
sent w `shouldReturn` []
|
|
|
|
testRevertFailed :: IO ()
|
|
testRevertFailed = withWorker $ \w@W {wCtx = ctx@Ctx {cNode}} -> do
|
|
(r, code) <- codeIn ctx 8 aliceOrder openPhase >>= traverseFst (revealed ctx keyA "alicebakery")
|
|
answerAvailable w True
|
|
-- viem's encodeErrorResult of InsufficientAllowance(10000, 2500)
|
|
setFault cNode "eth_estimateGas" $ revertFault $ unhex "2a1b2dd8000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000009c4"
|
|
logs <- capturingStderr $ steps w r >>= done
|
|
logs `shouldSatisfy` T.isInfixOf "setRegistrarAllowance"
|
|
now <- nowOf ctx
|
|
x <- stored ctx r
|
|
(nrPhase x, nrFailedAt x, nrError x, nrLive x) `shouldBe` (PFailed, Just now, Just "The name could not be registered", True)
|
|
pcReservedBy <$> codeOf ctx code `shouldReturn` Just (nrId r)
|
|
-- a revert without data while the name is available fails a paid order too
|
|
p <- paidIn ctx dynamisOrder openPhase >>= revealed ctx keyA "dynamis"
|
|
answerCall cNode fakeController (selector "available(string)" <> abiTuple [Dynamic $ abiBytes "dynamis"]) $ Returns $ uint256 (1 :: Int)
|
|
answerUnowned ctx "dynamis"
|
|
setFault cNode "eth_estimateGas" $ RpcError (-32000) "execution reverted" Nothing
|
|
steps w p >>= done
|
|
nrPhase <$> stored ctx p `shouldReturn` PFailed
|
|
sent w `shouldReturn` []
|
|
|
|
testInvalidNonce :: IO ()
|
|
testInvalidNonce = withWorker $ \w@W {wCtx = ctx} -> do
|
|
r <- paidIn ctx aliceOrder openPhase >>= revealed ctx keyA "alicebakery"
|
|
dryRunsRevert w "InvalidNonce()" ""
|
|
published w r (steps w r >>= waitingUntil) `shouldReturn` at (orderSeconds - minAge + maxAge + 1)
|
|
x <- stored ctx r
|
|
(nrPhase x, nrReveal x, nrVersion x, nrRevealBefore x) `shouldBe` (PCommitted, Nothing, nrVersion r + 1, nrRevealBefore r)
|
|
sent w `shouldReturn` []
|
|
|
|
testExpiredIntent :: IO ()
|
|
testExpiredIntent = withWorker $ \w@W {wCtx = ctx} -> do
|
|
r <- paidIn ctx aliceOrder (\o -> (openPhase o) {nrRevealBefore = Just (rvDeadline reveal)}) >>= revealed ctx keyA "alicebakery"
|
|
answerAvailable w True
|
|
advance ctx $ fromIntegral (rvDeadline reveal - orderSeconds) + 1
|
|
steps w r >>= done
|
|
nrPhase <$> stored ctx r `shouldReturn` PFailed
|
|
sent w `shouldReturn` []
|
|
|
|
testFailedOnlySettled :: IO ()
|
|
testFailedOnlySettled = withWorker $ \w@W {wCtx = ctx@Ctx {cNode}, wSender} -> do
|
|
r <- paidIn ctx aliceOrder openPhase >>= revealed ctx keyA "alicebakery"
|
|
r2 <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 2} (\o -> (openPhase o) {nrRevealBefore = Just (orderSeconds + 3 * 86400)}) >>= revealed ctx keyB "alicebakery"
|
|
answerAvailable w True
|
|
p <- steps w r >>= awaiting
|
|
-- past revealBefore, the register in flight can still be mined
|
|
advance ctx $ 2 * day
|
|
steps w r >>= awaiting >>= (`shouldBe` ptHash p) . ptHash
|
|
nrPhase <$> stored ctx r `shouldReturn` PRegistering
|
|
void $ mineNewest w False
|
|
steps w r >>= done
|
|
nrPhase <$> stored ctx r `shouldReturn` PFailed
|
|
-- a register whose nonce another transaction took, while the owner holds the name: it went through
|
|
p2 <- steps w r2 >>= awaiting
|
|
useNextNonce cNode
|
|
followTransactions wSender
|
|
steps w r2 >>= awaiting >>= (`shouldBe` ptHash p2) . ptHash
|
|
replicateM_ 2 $ mine cNode []
|
|
followTransactions wSender
|
|
answerOwner ctx "alicebakery" (nrOwner r2)
|
|
answerExpiry ctx "alicebakery" expiry
|
|
steps w r2 >>= done
|
|
x2 <- stored ctx r2
|
|
(nrPhase x2, nrExpiresAt x2) `shouldBe` (PRegistered, Just expiry)
|
|
|
|
testLapseFailed :: IO ()
|
|
testLapseFailed = withWorker $ \w@W {wCtx = ctx} -> do
|
|
(r, code) <- codeIn ctx 8 aliceOrder committedPhase
|
|
steps w r >>= waitingUntil >>= (`shouldBe` at (orderSeconds + maxAge + 1))
|
|
advance ctx $ day + 1
|
|
published w r (steps w r) >>= done
|
|
now <- nowOf ctx
|
|
x <- stored ctx r
|
|
(nrPhase x, nrFailedAt x, nrError x, nrLive x) `shouldBe` (PFailed, Just now, Just "The name was not signed in time", True)
|
|
pcReservedBy <$> codeOf ctx code `shouldReturn` Just (nrId r)
|
|
|
|
testLapsePoll :: IO ()
|
|
testLapsePoll = withWorker $ \w@W {wCtx = ctx@Ctx {cClient}} -> do
|
|
r <- paidIn ctx aliceOrder committedPhase
|
|
advance ctx $ day + 1
|
|
let path = registrationPath (nrId r) <> "?seenVersion=" <> show (nrVersion r)
|
|
-- the lapse alone changes no version, so a poll with the version seen before it is held
|
|
poll <- held ctx (0, 1) path
|
|
steps w r >>= done
|
|
(status, view) <- woken "the poll held at the lapse" poll
|
|
(status, field "version" view, field "phase" view) `shouldBe` (200, Just (J.toJSON $ nrVersion r + 1), Just "failed")
|
|
started' <- getCurrentTime
|
|
(status', view') <- timeout prompt (webGet cClient path >>= \resp -> (statusOf resp,) <$> jsonObject resp) >>= maybe (fail "the old version was held") pure
|
|
waited <- (`diffUTCTime` started') <$> getCurrentTime
|
|
(status', field "phase" view', waited < 2) `shouldBe` (200, Just "failed", True)
|
|
|
|
testRestart :: IO ()
|
|
testRestart = withWorker $ \w@W {wCtx = ctx@Ctx {cStore}, wSender} -> do
|
|
(r, code) <- codeIn ctx 8 aliceOrder id
|
|
answerExpiry ctx "alicebakery" expiry
|
|
answerCommitment w r 0
|
|
-- the commit saved and sent, the service stopped before recording committing
|
|
Right p <- submitTransaction wSender (TxCommit $ nrId r) fakeController (commitCalldata $ nrCommitment r)
|
|
w1 <- started ctx
|
|
steps w1 r >>= awaiting >>= (`shouldBe` ptHash p) . ptHash
|
|
x <- stored ctx r
|
|
(nrPhase x, nrCommitTx x) `shouldBe` (PCommitting, Just (ptHash p))
|
|
length <$> sent w `shouldReturn` 1
|
|
commitBlock <- mineNewest w1 True
|
|
t <- blockTime w1 commitBlock
|
|
answerCommitment w r t
|
|
w2 <- started ctx
|
|
void $ steps w2 r >>= waitingUntil
|
|
nrPhase <$> stored ctx r `shouldReturn` PCommitted
|
|
void $ revealed ctx keyA "alicebakery" r
|
|
advance ctx $ fromIntegral (t + minAge) - fromIntegral orderSeconds
|
|
register <- steps w2 r >>= awaiting
|
|
w3 <- started ctx
|
|
steps w3 r >>= awaiting >>= (`shouldBe` ptHash register) . ptHash
|
|
nrPhase <$> stored ctx r `shouldReturn` PRegistering
|
|
void $ mineNewest w3 True
|
|
w4 <- started ctx
|
|
steps w4 r >>= done
|
|
nrPhase <$> stored ctx r `shouldReturn` PRegistered
|
|
isJust . pcSpentAt <$> codeOf ctx code `shouldReturn` True
|
|
map ntStatus <$> withTransaction cStore (`registrationTransactions` nrId r) `shouldReturn` [TSMined, TSMined]
|
|
length <$> sent w `shouldReturn` 2
|
|
|
|
testRunWorkers :: IO ()
|
|
testRunWorkers = withWorker $ \w@W {wCtx = ctx@Ctx {cNode, cOrders}, wWorker} -> do
|
|
r <- paidIn ctx aliceOrder id
|
|
(unpaid, _) <- placed ctx (byB aliceOrder) {oCommitment = commitmentOf 2}
|
|
answerCommitment w r 0
|
|
withAsync (runWorkers wWorker) $ \_ -> do
|
|
timeout prompt (awaitSent cNode 1) >>= maybe (fail "no commit was sent") (const $ pure ())
|
|
changed <- awaitChange (oePhases cOrders) (nrId r) (nrVersion <$> stored ctx r) (nrVersion r) prompt
|
|
changed `shouldBe` True
|
|
nrPhase <$> stored ctx r `shouldReturn` PCommitting
|
|
nrPhase <$> invoiceRegistration ctx unpaid `shouldReturn` PUnpaid
|
|
length <$> sent w `shouldReturn` 1
|
|
|
|
testCodeSweep :: IO ()
|
|
testCodeSweep = withWorker $ \W {wCtx = ctx, wWorker} -> do
|
|
(old, oldCode) <- codeIn ctx 8 aliceOrder failedPhase
|
|
(recent, recentCode) <- codeIn ctx 7 dynamisOrder $ \x -> (failedPhase x) {nrFailedAt = Just $ addUTCTime (2 * day) orderTime}
|
|
-- failed long ago, then requeued by Try again
|
|
(requeued, requeuedCode) <- codeIn ctx 8 (byB aliceOrder) {oCommitment = commitmentOf 2} $ \x -> (failedPhase x) {nrPhase = PQueued}
|
|
advance ctx $ 31 * day
|
|
sweepFailedCodes wWorker
|
|
map pcReservedBy <$> mapM (codeOf ctx) [oldCode, recentCode, requeuedCode] `shouldReturn` [Nothing, Just (nrId recent), Just (nrId requeued)]
|
|
mapM (stored ctx) [old, recent, requeued] `shouldReturn` [old, recent, requeued]
|
|
|
|
testLandedAtDepth :: IO ()
|
|
testLandedAtDepth = withOrders $ \ctx@Ctx {cNode, cOrders} -> do
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
answerUnowned ctx "alicebakery"
|
|
answerExpiry ctx "alicebakery" expiry
|
|
-- the owner's own register, mined in the latest block
|
|
latest <- mine cNode []
|
|
answerCallFrom cNode latest (registrarOf cNode) (ownerOfSelector <> labelBytes "alicebakery") $ Returns $ addressWord $ nrOwner r
|
|
confirmLanded cOrders r `shouldReturn` LNotLanded
|
|
stored ctx r `shouldReturn` r
|
|
-- confirmations 3: two more blocks put it at the depth
|
|
replicateM_ 2 $ mine cNode []
|
|
confirmLanded cOrders r `shouldReturn` LRegistered (nrVersion r + 1)
|
|
x <- stored ctx r
|
|
(nrPhase x, nrExpiresAt x, nrRegisterTx x) `shouldBe` (PRegistered, Just expiry, Nothing)
|
|
|
|
testOutcomeBound :: IO ()
|
|
testOutcomeBound = bounded "the outcome's wait" $ withOrders $ \ctx@Ctx {cNode, cStore, cRandom, cClock} -> do
|
|
r@NameRegistration {nrId, nrCommitment} <- paidIn ctx aliceOrder id
|
|
chain <- newChainClient cRandom $ fakeChainConfig cNode
|
|
sender <- newSender cRandom chain cStore (readIORef cClock)
|
|
Right p <- submitTransaction sender (TxCommit nrId) fakeController (commitCalldata nrCommitment)
|
|
started' <- getCurrentTime
|
|
awaitOutcome shortWait p `shouldReturn` Nothing
|
|
waited <- (`diffUTCTime` started') <$> getCurrentTime
|
|
waited `shouldSatisfy` (< 2)
|
|
let TxHash h = ptHash p
|
|
block <- mine cNode [(h, True)]
|
|
replicateM_ 2 $ mine cNode []
|
|
followTransactions sender
|
|
awaitOutcome shortWait p `shouldReturn` Just (TOConfirmed (ptHash p) Receipt {receiptSuccess = True, receiptBlock = block})
|
|
stored ctx r `shouldReturn` r
|
|
where
|
|
shortWait = 300000
|