From 45302749f5a3ef71ce60765e6bc3a1b7b950b537 Mon Sep 17 00:00:00 2001 From: shum Date: Fri, 23 Oct 2026 14:17:01 +0000 Subject: [PATCH] store: sign and store the agreed terms --- .../src/StoreService/Names/Orders.hs | 18 +++- .../src/StoreService/Names/Signatures.hs | 9 +- .../src/StoreService/Names/Store.hs | 30 ++++--- .../StoreService/GroupIntegrationTests.hs | 3 +- tests/Bots/StoreService/NamesBalancesTests.hs | 4 +- tests/Bots/StoreService/NamesOrdersTests.hs | 80 +++++++++++++---- tests/Bots/StoreService/NamesOwnerTests.hs | 23 +++-- .../Bots/StoreService/NamesSignatureTests.hs | 88 +++++++++++-------- tests/Bots/StoreService/NamesSpendTests.hs | 12 ++- tests/Bots/StoreService/NamesStoreTests.hs | 13 ++- tests/Bots/StoreService/NamesWorkerTests.hs | 3 +- 11 files changed, 196 insertions(+), 87 deletions(-) diff --git a/apps/simplex-store-service/src/StoreService/Names/Orders.hs b/apps/simplex-store-service/src/StoreService/Names/Orders.hs index 711ef57165..aeab67a4c5 100644 --- a/apps/simplex-store-service/src/StoreService/Names/Orders.hs +++ b/apps/simplex-store-service/src/StoreService/Names/Orders.hs @@ -47,6 +47,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.KeyMap as KM import Data.Bifunctor (first) import Data.ByteString (ByteString) +import Data.Char (isAsciiLower, isAsciiUpper, isDigit) import Data.Int (Int64) import Data.Maybe (catMaybes, isJust) import Data.Scientific (toBoundedInteger) @@ -129,6 +130,10 @@ data Landing = LNotLanded | LRegistered Int64 | LChanged | LUnread maxSealedChars :: Int maxSealedChars = 11095 +-- | The terms version, of up to 32 characters, and a suffix such as /community. +maxAgreedTermsChars :: Int +maxAgreedTermsChars = 48 + -- | How far a signed request's timestamp may be from the service's clock. signedRequestSeconds :: Integer signedRequestSeconds = 600 @@ -149,9 +154,16 @@ parseOrderFields o = do ofCommitment <- bytes32FromHex =<< textField o "commitment" ofSealed <- textField o "sealed" unless (T.length ofSealed <= maxSealedChars) Nothing + ofAgreedTerms <- textField o "terms" + unless (agreedTermsFormat ofAgreedTerms) Nothing timestamp <- integerField =<< KM.lookup "timestamp" o signature <- either (const Nothing) Just . parseSignature =<< textField o "signature" - pure (OrderFields {ofLabelHash, ofLength, ofDuration, ofCommitment, ofSealed}, timestamp, signature) + pure (OrderFields {ofLabelHash, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms}, timestamp, signature) + +agreedTermsFormat :: Text -> Bool +agreedTermsFormat t = not (T.null t) && T.length t <= maxAgreedTermsChars && T.all termsChar t + where + termsChar c = isAsciiUpper c || isAsciiLower c || isDigit c || c `elem` ("._/-" :: String) textField :: J.Object -> J.Key -> Maybe Text textField o k = case KM.lookup k o of @@ -284,8 +296,8 @@ movable NameRegistration {nrPhase, nrLinksRefusedAt} = case nrPhase of _ -> False orderFieldsOf :: NameRegistration -> OrderFields -orderFieldsOf NameRegistration {nrLabelHash, nrLength, nrDuration, nrCommitment, nrSealed} = - OrderFields {ofLabelHash = nrLabelHash, ofLength = nrLength, ofDuration = nrDuration, ofCommitment = nrCommitment, ofSealed = nrSealed} +orderFieldsOf NameRegistration {nrLabelHash, nrLength, nrDuration, nrCommitment, nrSealed, nrAgreedTerms} = + OrderFields {ofLabelHash = nrLabelHash, ofLength = nrLength, ofDuration = nrDuration, ofCommitment = nrCommitment, ofSealed = nrSealed, ofAgreedTerms = nrAgreedTerms} -- | Any other live order of the new commitment, or of the owner and the new label hash, refuses the move. checkMoveMeets :: DBStore -> NameRegistration -> OrderFields -> UTCTime -> IO (Either OrderRefusal ()) diff --git a/apps/simplex-store-service/src/StoreService/Names/Signatures.hs b/apps/simplex-store-service/src/StoreService/Names/Signatures.hs index 008624e4a2..8e7151dac3 100644 --- a/apps/simplex-store-service/src/StoreService/Names/Signatures.hs +++ b/apps/simplex-store-service/src/StoreService/Names/Signatures.hs @@ -49,13 +49,14 @@ data AddressError | ADChecksum deriving (Eq, Show) --- | The fields an owner signs in an order and in another name for it. +-- | The fields an owner signs in an order and in another name for it, with the terms the owner agreed to for that name. data OrderFields = OrderFields { ofLabelHash :: LabelHash, ofLength :: Int, ofDuration :: Word64, ofCommitment :: ByteString, - ofSealed :: Text + ofSealed :: Text, + ofAgreedTerms :: Text } deriving (Eq) @@ -108,8 +109,8 @@ message :: [Text] -> ByteString message = encodeUtf8 . T.unwords . ("simplex.domains" :) orderWords :: OrderFields -> [Text] -orderWords OrderFields {ofLabelHash = LabelHash labelHash, ofLength, ofDuration, ofCommitment, ofSealed} = - [hex labelHash, tshow ofLength, tshow ofDuration, hex ofCommitment, hex $ sha256Hash $ encodeUtf8 ofSealed] +orderWords OrderFields {ofLabelHash = LabelHash labelHash, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} = + [hex labelHash, tshow ofLength, tshow ofDuration, hex ofCommitment, hex $ sha256Hash $ encodeUtf8 ofSealed, ofAgreedTerms] -- the address parser checks EIP-55 only on mixed-case hex, so one-case text is refused here by comparing the encoding parseChecksumAddress :: Text -> Either AddressError Address diff --git a/apps/simplex-store-service/src/StoreService/Names/Store.hs b/apps/simplex-store-service/src/StoreService/Names/Store.hs index f28117b8b7..44e7c3419e 100644 --- a/apps/simplex-store-service/src/StoreService/Names/Store.hs +++ b/apps/simplex-store-service/src/StoreService/Names/Store.hs @@ -195,6 +195,8 @@ data NameRegistration = NameRegistration nrOwner :: Address, nrCommitment :: ByteString, nrSealed :: Text, + -- | '' for an order stored before orders signed the terms + nrAgreedTerms :: Text, nrPhase :: NamePhase, nrYears :: Int, nrPrice :: Int, @@ -476,8 +478,8 @@ orderHashFieldBytes = 4 -- | Each field is length-prefixed, so no two different orders encode the same. orderHash :: Address -> OrderFields -> OrderTerms -> ByteString -orderHash owner OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} terms = - sha256Hash . B.concat . map field $ [tag, lh, num ofLength, num ofDuration, unAddress owner, ofCommitment, encodeUtf8 ofSealed] <> payment +orderHash owner OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} terms = + sha256Hash . B.concat . map field $ [tag, lh, num ofLength, num ofDuration, unAddress owner, ofCommitment, encodeUtf8 ofSealed, encodeUtf8 ofAgreedTerms] <> payment where (tag, payment) = case terms of OTInvoice method price -> ("name-invoice", [encodeUtf8 $ methodText method, num price]) @@ -557,15 +559,16 @@ insertRegistration db NewRegistration {nrgId = RegistrationId rid, nrgOwner, nrg [sql| INSERT INTO sx_badge_service_name_registrations (registration_id, version, label_hash, length, duration, owner, commitment, sealed, phase, years, - price, method, name_prepaid_code_id, live, order_hash, moves, created_at, updated_at, balance_source, balance_used) - VALUES (?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?) + price, method, name_prepaid_code_id, live, order_hash, moves, created_at, updated_at, balance_source, balance_used, agreed_terms) + VALUES (?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?, ?) ON CONFLICT DO NOTHING |] ( (rid, initialVersion, Binary lh, ofLength, int64 ofDuration, Binary $ unAddress nrgOwner, Binary ofCommitment, ofSealed, phase, nrgYears) :. (price, methodText <$> method, codeId, BI True, Binary hash, 0 :: Int, now, now, (\(RegistrationId s, _) -> s) <$> fromBalance, maybe 0 snd fromBalance) + :. Only ofAgreedTerms ) where - OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} = nrgOrder + OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} = nrgOrder registrationColumns :: Query registrationColumns = @@ -573,7 +576,8 @@ registrationColumns = SELECT r.registration_id, r.version, r.label_hash, r.length, r.duration, r.owner, r.commitment, r.sealed, r.phase, r.years, r.price, r.method, ni.invoice_id, r.name_prepaid_code_id, r.label, r.secret, r.contact_link, r.channel_link, r.intent_nonce, r.intent_deadline, r.intent_signature, r.commit_tx, r.commit_block, r.register_tx, r.register_block, r.reveal_after, r.reveal_before, r.expires_at, r.registered_at, - r.failed_at, r.error, r.live, r.order_hash, r.moves, r.links_refused_at, r.created_at, r.updated_at, r.balance_source, r.balance_used + r.failed_at, r.error, r.live, r.order_hash, r.moves, r.links_refused_at, r.created_at, r.updated_at, r.balance_source, r.balance_used, + r.agreed_terms |] registrationFrom :: Query @@ -592,6 +596,7 @@ type RegistrationRow = :. (Int, Maybe Text, Maybe Text, Maybe Int64, Maybe Text, Maybe (Binary ByteString), Maybe Text, Maybe Text, Maybe Int64, Maybe Int64) :. (Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe Int64, Maybe (Binary ByteString), Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) :. (Maybe UTCTime, Maybe Text, BoolInt, Binary ByteString, Int, Maybe UTCTime, UTCTime, UTCTime, Maybe Text, Int) + :. Only Text toRegistration :: RegistrationRow -> Either Text NameRegistration toRegistration @@ -599,6 +604,7 @@ toRegistration :. (nrPrice, method_, invoiceId_, codeId_, label_, secret_, nrContactLink, nrChannelLink, nonce_, deadline_) :. (signature_, commitTx, commitBlock, registerTx, registerBlock, revealAfter, revealBefore, expiresAt, nrRegisteredAt) :. (nrFailedAt, nrError, BI nrLive, Binary nrOrderHash, nrMoves, nrLinksRefusedAt, nrCreatedAt, nrUpdatedAt, source_, nrBalanceUsed) + :. Only nrAgreedTerms ) = do nrOwner <- decodeAddress owner nrPayment <- case (method_, invoiceId_, codeId_) of @@ -622,6 +628,7 @@ toRegistration nrOwner, nrCommitment, nrSealed, + nrAgreedTerms, nrPhase, nrYears, nrPrice, @@ -734,7 +741,7 @@ updateRegistration db r now = do db [sql| UPDATE sx_badge_service_name_registrations - SET version = version + 1, label_hash = ?, length = ?, duration = ?, commitment = ?, sealed = ?, phase = ?, price = ?, + SET version = version + 1, label_hash = ?, length = ?, duration = ?, commitment = ?, sealed = ?, agreed_terms = ?, phase = ?, price = ?, label = ?, secret = ?, contact_link = ?, channel_link = ?, intent_nonce = ?, intent_deadline = ?, intent_signature = ?, commit_tx = ?, commit_block = ?, register_tx = ?, register_block = ?, reveal_after = ?, reveal_before = ?, expires_at = ?, registered_at = ?, failed_at = ?, error = ?, live = ?, moves = ?, links_refused_at = ?, updated_at = ? @@ -744,7 +751,7 @@ updateRegistration db r now = do WHERE o.live = 1 AND o.registration_id <> ? AND (o.commitment = ? OR (o.owner = ? AND o.label_hash = ?)) ))) |] - ( (Binary lh, nrLength, int64 nrDuration, Binary nrCommitment, nrSealed, nrPhase, nrPrice) + ( (Binary lh, nrLength, int64 nrDuration, Binary nrCommitment, nrSealed, nrAgreedTerms, nrPhase, nrPrice) :. (rvLabel <$> nrReveal, Binary . rvSecret <$> nrReveal, rvContactLink =<< nrReveal, rvChannelLink =<< nrReveal, int64 . rvNonce <$> nrReveal, int64 . rvDeadline <$> nrReveal, Binary . serializeSignature . rvSignature <$> nrReveal) :. (txHashField <$> nrCommitTx, int64 <$> nrCommitBlock, txHashField <$> nrRegisterTx, int64 <$> nrRegisterBlock, int64 <$> nrRevealAfter, int64 <$> nrRevealBefore, int64 <$> nrExpiresAt) :. (truncateToSecond <$> nrRegisteredAt, truncateToSecond <$> nrFailedAt, nrError, BI live, nrMoves, truncateToSecond <$> nrLinksRefusedAt, truncateToSecond now) @@ -752,7 +759,7 @@ updateRegistration db r now = do ) if changed > 0 then pure $ Written (nrVersion + 1) else refusal where - NameRegistration {nrId = RegistrationId rid, nrVersion, nrLabelHash = LabelHash lh, nrLength, nrDuration, nrOwner, nrCommitment, nrSealed, nrPhase, nrPrice, nrReveal} = r + NameRegistration {nrId = RegistrationId rid, nrVersion, nrLabelHash = LabelHash lh, nrLength, nrDuration, nrOwner, nrCommitment, nrSealed, nrAgreedTerms, nrPhase, nrPrice, nrReveal} = r NameRegistration {nrCommitTx, nrCommitBlock, nrRegisterTx, nrRegisterBlock, nrRevealAfter, nrRevealBefore, nrExpiresAt} = r NameRegistration {nrRegisteredAt, nrFailedAt, nrError, nrMoves, nrLinksRefusedAt} = r live = orderLive r @@ -761,10 +768,10 @@ updateRegistration db r now = do [(version, BI storedLive)] | version == nrVersion -> pure $ if storedLive then LiveOrderExists else NotLive _ -> pure VersionChanged --- | Another name, or other links: queued again with the reveal, transactions, reveal times and link refusal cleared. +-- | Another name, or other links, with the terms agreed for it: queued again with the reveal, transactions, reveal times and link refusal cleared. -- The order hash stays the original order's, so a resend of that order still finds it. retargetRegistration :: DB.Connection -> NameRegistration -> OrderFields -> Int -> UTCTime -> IO VersionWrite -retargetRegistration db r OrderFields {ofLabelHash, ofLength, ofDuration, ofCommitment, ofSealed} price = +retargetRegistration db r OrderFields {ofLabelHash, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} price = updateRegistration db r @@ -773,6 +780,7 @@ retargetRegistration db r OrderFields {ofLabelHash, ofLength, ofDuration, ofComm nrDuration = ofDuration, nrCommitment = ofCommitment, nrSealed = ofSealed, + nrAgreedTerms = ofAgreedTerms, nrPrice = price, nrPhase = PQueued, nrMoves = nrMoves r + 1, diff --git a/tests/Bots/StoreService/GroupIntegrationTests.hs b/tests/Bots/StoreService/GroupIntegrationTests.hs index 25fbdabe85..241012962b 100644 --- a/tests/Bots/StoreService/GroupIntegrationTests.hs +++ b/tests/Bots/StoreService/GroupIntegrationTests.hs @@ -24,7 +24,7 @@ import StoreService.Service (ServiceState (..), checkDevChain, newServiceState, import StoreService.Store (ManagedGroup (..), getManagedGroup) import StoreService.Waiters (newWaiters) import Bots.StoreService.BotTests (badgeBotName, badgeCmdUsage, badgeProfile, badgeTypeOf, credentialOf, devKeysIni, entryOf, issueCode, issueRaw, mkStoreServiceOpts, newPurchaseKeys, operatorCmd, redeemWithQueue, requestObject, revokeRaw, serviceDbPrefix, shouldAnswerError, statementOf, stopStoreService, testIssuerKeyIdx, withStoreService) -import Bots.StoreService.ConfigTests (chainSection, controllerAddr, namesSection) +import Bots.StoreService.ConfigTests (chainSection, controllerAddr, namesSection, termsVersion) import Bots.StoreService.FakeEthNode (CallAnswer (..), FakeEthNode (..), Fault (..), answerCall, awaitSent, setFault, withFakeEthNode) import Bots.StoreService.WebTests (capturingStderr, digestFixture) import Bots.StoreService.NamesSearchTests (aliceHex) @@ -220,6 +220,7 @@ testNamesBoot ps = do "sealed" J..= ("sealed" :: Text), "price" J..= (10000 :: Int), "balance" J..= (10000 :: Int), + "terms" J..= termsVersion, "timestamp" J..= (1792574100 :: Int), "signature" J..= ("0x" <> T.replicate 32 "11" <> T.replicate 32 "22" <> "1b") ] diff --git a/tests/Bots/StoreService/NamesBalancesTests.hs b/tests/Bots/StoreService/NamesBalancesTests.hs index 44350728e8..82f6380e7c 100644 --- a/tests/Bots/StoreService/NamesBalancesTests.hs +++ b/tests/Bots/StoreService/NamesBalancesTests.hs @@ -7,6 +7,7 @@ module Bots.StoreService.NamesBalancesTests (namesBalancesTests, cardPaid, coveredSpend, credit, creditLate, fieldsN, figures, holdPrice, holder, invariantHolds, lateCredit, payment, placeOrderRow, settleHolder, source) where +import Bots.StoreService.ConfigTests (termsVersion) import Bots.StoreService.NamesOrdersTests (Ctx (..), withOrders) import Bots.StoreService.NamesStoreTests (at, btc, invoiceStatus, newReg, ownerA, ownerB, paidOrder, reg, signature, t0, write) import Bots.StoreService.WebTests (capturingStderr, rcv, withServiceStore, withServiceStores) @@ -101,7 +102,8 @@ fieldsN label = ofLength = T.length label, ofDuration = 69292800, ofCommitment = C.sha256Hash $ encodeUtf8 label, - ofSealed = "sealed-" <> label + ofSealed = "sealed-" <> label, + ofAgreedTerms = termsVersion } -- | A crypto invoice row, or a card one with its client secret in place of an address. diff --git a/tests/Bots/StoreService/NamesOrdersTests.hs b/tests/Bots/StoreService/NamesOrdersTests.hs index 32256230dc..aeb198d4f2 100644 --- a/tests/Bots/StoreService/NamesOrdersTests.hs +++ b/tests/Bots/StoreService/NamesOrdersTests.hs @@ -10,6 +10,8 @@ module Bots.StoreService.NamesOrdersTests withOrdersVia, withOrdersOn, withOrdersCancelling, + withSettings, + configured, aliceOrder, commitmentOf, fieldsOf, @@ -34,6 +36,7 @@ module Bots.StoreService.NamesOrdersTests field, advance, refusedWith, + malformedTerms, ) where @@ -226,6 +229,7 @@ data Order = Order oDuration :: Word64, oMethod :: Text, oPrice :: Int, + oTerms :: Text, oTimestamp :: Word64 } @@ -240,6 +244,7 @@ aliceOrder = oDuration = durationAt orderTime, oMethod = "btc", oPrice = alicePrice, + oTerms = termsVersion, oTimestamp = seconds orderTime } @@ -251,8 +256,8 @@ commitmentOf :: Int -> ByteString commitmentOf n = C.sha256Hash $ BC.pack $ "commitment " <> show n fieldsOf :: Order -> OrderFields -fieldsOf Order {oLabel, oCommitment, oSealed, oDuration} = - OrderFields {ofLabelHash = labelHash oLabel, ofLength = T.length oLabel, ofDuration = oDuration, ofCommitment = oCommitment, ofSealed = oSealed} +fieldsOf Order {oLabel, oCommitment, oSealed, oDuration, oTerms} = + OrderFields {ofLabelHash = labelHash oLabel, ofLength = T.length oLabel, ofDuration = oDuration, ofCommitment = oCommitment, ofSealed = oSealed, ofAgreedTerms = oTerms} signedBody :: TVar ChaChaDRG -> Order -> IO J.Object signedBody g o@Order {oKey, oTimestamp} = do @@ -271,10 +276,11 @@ unsignedBody o@Order {oOwner, oMethod, oPrice, oTimestamp} = ("sealed", J.String ofSealed), ("method", J.String oMethod), ("price", J.toJSON oPrice), + ("terms", J.String ofAgreedTerms), ("timestamp", J.toJSON oTimestamp) ] where - OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} = fieldsOf o + OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} = fieldsOf o withSignature :: Text -> J.Object -> J.Object withSignature sig = KM.insert "signature" (J.String sig) @@ -352,6 +358,20 @@ withOrdersOn cOpenStore via action = cOpenStore >>= \cStore -> withFakeEthNode $ Warp.testWithApplicationSettings (webSettings (listener cfg)) (pure $ ordersApp cWeb cOrders) $ \port -> action Ctx {cStore, cOpenStore, cNode, cOrders, cWeb, cClient = (mgr, "http://127.0.0.1:" <> show port), cStub, cClock, cRandom, cIps} +-- | The context's store, chain, providers and clock with its names settings changed, as after a restart. +configured :: (NamesConfig -> NamesConfig) -> Ctx -> Ctx +configured change ctx@Ctx {cOrders, cWeb} = ctx {cOrders = orders, cWeb = cWeb {weOrders = Just orders}} + where + orders = cOrders {oeConfig = change (oeConfig cOrders)} + +-- | Another server of the orders routes over the context with its names settings changed. +withSettings :: (NamesConfig -> NamesConfig) -> Ctx -> (Ctx -> IO a) -> IO a +withSettings change ctx action = do + let ctx'@Ctx {cOrders, cWeb} = configured change ctx + mgr <- newManager defaultManagerSettings + Warp.testWithApplicationSettings (webSettings (listener $ weConfig cWeb)) (pure $ ordersApp cWeb cOrders) $ \port -> + action ctx' {cClient = (mgr, "http://127.0.0.1:" <> show port)} + ordersApp :: WebEnv -> OrdersEnv -> Application ordersApp web orders req respond = fromMaybe (webApp web req respond) (namesApp (weHttp web) (codeRoutes orders (weProviders web) (weWaiters web)) req respond) @@ -463,24 +483,35 @@ testPaidOrder = withOrders $ \ctx -> do reg <- invoiceRegistration ctx invId (nrPhase reg, nrLive reg, nrVersion reg, nrYears reg, nrPrice reg, nrPayment reg) `shouldBe` (PUnpaid, True, 1, 2, 10000, RPInvoice (SPMCrypto CCBtc) invId) nrOrderHash reg `shouldBe` orderHash ownerA (fieldsOf aliceOrder) (OTInvoice (SPMCrypto CCBtc) 10000) + nrAgreedTerms reg `shouldBe` termsVersion --- | The body of the webapp's order vector, with viem's signature over it (NamesSignatureTests). +-- | The body of the contract's Appendix C order vector, with viem's signature over it (NamesSignatureTests). testViemOrder :: IO () -testViemOrder = withOrders $ \ctx -> do - let dynamis = - aliceOrder - { oLabel = "dynamis", - oCommitment = unhex "9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1", - oSealed = "q3Zk0Yx7c2VhbGVkLW9yZGVyLWZpeHR1cmU", - oDuration = 69259500, - oPrice = 20000 - } - viemSignature = "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a84df9ffdccc39c5cb827cde4d846c7662f591665f1b868ab1c15778646d3f414e1b" - signedBody (cRandom ctx) dynamis >>= (`shouldBe` withSignature viemSignature (unsignedBody dynamis)) - (status, v) <- postOrder ctx $ withSignature viemSignature (unsignedBody dynamis) +testViemOrder = withOrders $ \ctx0 -> withSettings vectorTermStart ctx0 $ \ctx -> do + writeIORef (cClock ctx) (posixSecondsToUTCTime $ fromIntegral $ oTimestamp vectorOrder) + signedBody (cRandom ctx) vectorOrder >>= (`shouldBe` withSignature vectorSignature (unsignedBody vectorOrder)) + (status, v) <- postOrder ctx $ withSignature vectorSignature (unsignedBody vectorOrder) status `shouldBe` 200 (field "phase" . registrationOf =<< v) `shouldBe` Just "unpaid" +-- | Appendix C's order: alicebakery with no links, signed with the terms version. +vectorOrder :: Order +vectorOrder = + aliceOrder + { oCommitment = unhex "5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5", + oSealed = "AAAAAAAAAAAAAAAACWgxqMk-UKfpra_Zh6vc4KNBR6SX1WYpGb4COoWCCbM0bisYCH14WLzghp8hqi_-KyXSi4qf_DAdP_M4bFetD0s", + oDuration = 69292800, + oTimestamp = 1792828800 + } + +vectorSignature :: Text +vectorSignature = "0x17107b11494e25098efc348aca64a2469ffb832bcc79dc57377a1b26a4004b6701ad73648f50af8b39d4079820a3de7e681ddf15f8cf15b5cff02b4a48cf1fe61c" + +-- | Appendix C's duration is the page's at 21 October 2026 00:00 UTC, before its timestamp, +-- so the term starts later by as much for the vector to pass the duration check at its timestamp. +vectorTermStart :: NamesConfig -> NamesConfig +vectorTermStart cfg = cfg {nTermStart = posixSecondsToUTCTime $ fromIntegral $ oTimestamp vectorOrder + oDuration vectorOrder - 2 * yearSeconds} + testExactResend :: IO () testExactResend = withOrders $ \ctx -> do body <- signedBody (cRandom ctx) aliceOrder @@ -545,7 +576,8 @@ testOneFieldDiffers = withOrders $ \ctx -> do aliceOrder {oDuration = oDuration aliceOrder + 1}, aliceOrder {oMethod = "xmr"}, aliceOrder {oMethod = "card"}, - aliceOrder {oPrice = alicePrice + 1} + aliceOrder {oPrice = alicePrice + 1}, + aliceOrder {oTerms = termsVersion <> "/community"} ] forM_ variants $ \o -> placeOrder ctx o `shouldReturn` refusedWith 409 "order_exists" length <$> stubCalls (cStub ctx) `shouldReturn` 1 @@ -695,13 +727,20 @@ testFormats = withOrders $ \ctx -> do with "duration" (J.Number (-1)), with "timestamp" "1792574100", with "length" (J.Number 1e30), + KM.delete "terms" good, + with "terms" (J.Number 20261023), KM.delete "kind" good, with "kind" "badge" ] $ \body -> postOrder ctx body `shouldReturn` refusedWith 400 "bad_request" + forM_ malformedTerms $ \t -> placeOrder ctx aliceOrder {oTerms = t} `shouldReturn` refusedWith 400 "bad_request" forM_ [5, 64] $ \len -> postOrder ctx (with "length" (J.Number len)) `shouldReturn` refusedWith 400 "invalid_name" stubCalls (cStub ctx) `shouldReturn` [] +-- | Signed as sent, so only their format refuses them. +malformedTerms :: [Text] +malformedTerms = ["", T.replicate 49 "a", "2026-10-23 community", "2026-10-23+community", "2026-10-23/communit\233"] + testTwoWallets :: IO () testTwoWallets = withOrders $ \ctx -> do (a, _) <- placed ctx aliceOrder @@ -757,7 +796,7 @@ testCodeOrder = withOrders $ \ctx -> do rid <- registered ctx code aliceOrder r <- registrationById ctx rid (nrPhase r, nrLive r, nrVersion r, nrYears r, nrPrice r, nrPayment r) `shouldBe` (PQueued, True, 1, 2, 0, RPCode pcId) - (nrOwner r, nrDuration r, nrCommitment r, nrSealed r) `shouldBe` (ownerA, durationAt orderTime, oCommitment aliceOrder, oSealed aliceOrder) + (nrOwner r, nrDuration r, nrCommitment r, nrSealed r, nrAgreedTerms r) `shouldBe` (ownerA, durationAt orderTime, oCommitment aliceOrder, oSealed aliceOrder, termsVersion) nrOrderHash r `shouldBe` orderHash ownerA (fieldsOf aliceOrder) (OTCode pcCodeHash) held <- readCode ctx issued (pcReservedBy held, pcReservedAt held, pcSpentAt held) `shouldBe` (Just rid, Just orderTime, Nothing) @@ -791,6 +830,7 @@ testCodeOneFieldDiffers = withOrders $ \ctx -> do (code, aliceOrder {oCommitment = commitmentOf 2}), (code, aliceOrder {oSealed = "sealed-alicebakery-2"}), (code, aliceOrder {oDuration = oDuration aliceOrder + 1}), + (code, aliceOrder {oTerms = termsVersion <> "/community"}), (other, aliceOrder) ] forM_ variants $ \(c, o) -> register ctx c o `shouldReturn` refusedWith 409 "order_exists" @@ -922,9 +962,11 @@ testCodeSizes = withOrders $ \ctx -> do with "code" (J.Number 8), with "labelHash" (J.String $ T.toUpper $ hex $ (\(LabelHash h) -> h) alice), with "owner" (J.String $ T.toLower ownerAText), - with "timestamp" "1792574100" + with "timestamp" "1792574100", + KM.delete "terms" good ] $ \body -> postRegister ctx body `shouldReturn` refusedWith 400 "bad_request" + forM_ malformedTerms $ \t -> register ctx code aliceOrder {oTerms = t} `shouldReturn` refusedWith 400 "bad_request" forM_ ["[]", "{"] $ \body -> post ctx "/api/name/register" body `shouldReturn` (400, errorOnly "bad_request") -- the formats and the length come before the code forM_ [5, 64] $ \len -> postRegister ctx (KM.insert "code" "not a code" $ with "length" (J.Number len)) `shouldReturn` refusedWith 400 "invalid_name" diff --git a/tests/Bots/StoreService/NamesOwnerTests.hs b/tests/Bots/StoreService/NamesOwnerTests.hs index 26ce722004..8d0cb4f2f3 100644 --- a/tests/Bots/StoreService/NamesOwnerTests.hs +++ b/tests/Bots/StoreService/NamesOwnerTests.hs @@ -28,6 +28,7 @@ module Bots.StoreService.NamesOwnerTests ) where +import Bots.StoreService.ConfigTests (termsVersion) import Bots.StoreService.FakeEthNode import Bots.StoreService.NamesContractsTests (registrarOf, unhex) import Bots.StoreService.NamesOrdersTests @@ -88,6 +89,7 @@ namesOwnerTests = describe "StoreService names: owner" $ do it "refuses a move from registered, queued, taken, and committed without a refused reveal" testMoveRefusedPhases it "moves a failed order, a lapsed one, and a committed one whose links were refused, to the same name with other links too" testMoveAccepted it "answers a body equal to the current fields with the view unchanged, counting no move, whatever its timestamp" testMoveRepeat + it "moves an order by a body differing only in terms, storing them" testMoveTermsOnly it "answers the fourth move move_limit with the current view, changing nothing" testMoveLimit it "refuses a move meeting another live order of the wallet's or the commitment with a bare order_exists, before the price" testMoveMeetsOrder it "runs the order checks on the new fields: invalid_name, the duration, name_taken, name_check_unavailable" testMoveOrderChecks @@ -238,13 +240,13 @@ linksRefusedPhase r = (committedPhase r) {nrLinksRefusedAt = Just orderTime} moveTo :: Ctx -> Text -> Int -> IO OrderFields moveTo Ctx {cClock} label n = do t <- readIORef cClock - pure OrderFields {ofLabelHash = labelHash label, ofLength = T.length label, ofDuration = yearsDuration 2 t, ofCommitment = commitmentOf n, ofSealed = "sealed-" <> label <> "-" <> tshow n} + pure OrderFields {ofLabelHash = labelHash label, ofLength = T.length label, ofDuration = yearsDuration 2 t, ofCommitment = commitmentOf n, ofSealed = "sealed-" <> label <> "-" <> tshow n, ofAgreedTerms = termsVersion} moveBody :: Ctx -> Secp256k1PrivateKey -> RegistrationId -> OrderFields -> IO J.Object moveBody ctx key rid fields = clockSeconds ctx >>= moveBodyAt ctx key rid fields moveBodyAt :: Ctx -> Secp256k1PrivateKey -> RegistrationId -> OrderFields -> Word64 -> IO J.Object -moveBodyAt ctx key rid fields@OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} timestamp = do +moveBodyAt ctx key rid fields@OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} timestamp = do sig <- signWith ctx key $ retargetMessage rid fields timestamp pure $ KM.fromList @@ -253,6 +255,7 @@ moveBodyAt ctx key rid fields@OrderFields {ofLabelHash = LabelHash lh, ofLength, ("duration", J.toJSON ofDuration), ("commitment", J.String $ hex ofCommitment), ("sealed", J.String ofSealed), + ("terms", J.String ofAgreedTerms), ("timestamp", J.toJSON timestamp), ("signature", J.String sig) ] @@ -285,8 +288,8 @@ postFind :: Ctx -> J.Object -> IO (Int, Maybe J.Object) postFind ctx = post ctx "/api/registration/find" . J.encode currentFields :: NameRegistration -> OrderFields -currentFields NameRegistration {nrLabelHash, nrLength, nrDuration, nrCommitment, nrSealed} = - OrderFields {ofLabelHash = nrLabelHash, ofLength = nrLength, ofDuration = nrDuration, ofCommitment = nrCommitment, ofSealed = nrSealed} +currentFields NameRegistration {nrLabelHash, nrLength, nrDuration, nrCommitment, nrSealed, nrAgreedTerms} = + OrderFields {ofLabelHash = nrLabelHash, ofLength = nrLength, ofDuration = nrDuration, ofCommitment = nrCommitment, ofSealed = nrSealed, ofAgreedTerms = nrAgreedTerms} fieldsIn :: J.Object -> [(Text, J.Value)] -> Expectation fieldsIn v = mapM_ (\(k, value) -> (k, field k v) `shouldBe` (k, Just value)) @@ -459,6 +462,13 @@ testMoveRepeat = withOwner $ \ctx -> do stored ctx r `shouldReturn` movedOnce nrMoves movedOnce `shouldBe` 1 +testMoveTermsOnly :: IO () +testMoveTermsOnly = withOwner $ \ctx -> do + r <- paidIn ctx aliceOrder $ \p -> (failedPhase p) {nrAgreedTerms = "2026-10-01"} + (status, v) <- move ctx keyA r (currentFields r) {ofAgreedTerms = termsVersion} + (status, field "phase" =<< v, field "moves" =<< v) `shouldBe` (200, Just "queued", Just (J.Number 1)) + (nrAgreedTerms &&& nrMoves) <$> stored ctx r `shouldReturn` (termsVersion, 1) + testMoveLimit :: IO () testMoveLimit = withOwner $ \ctx -> do answerFree ctx "alicebakers" @@ -603,7 +613,7 @@ testMoveKeepsFromBalance = withOwner $ \ctx@Ctx {cStore} -> do answerFree ctx "alicebakers" paying <- paidIn ctx (byB aliceOrder) failedPhase withTransaction cStore $ \db -> creditBalance db (nrId paying) 5000 orderTime - let fields = OrderFields {ofLabelHash = labelHash "dynamis", ofLength = 7, ofDuration = yearsDuration 2 orderTime, ofCommitment = commitmentOf 3, ofSealed = "sealed-dynamis-3"} + let fields = OrderFields {ofLabelHash = labelHash "dynamis", ofLength = 7, ofDuration = yearsDuration 2 orderTime, ofCommitment = commitmentOf 3, ofSealed = "sealed-dynamis-3", ofAgreedTerms = termsVersion} new = NewRegistration {nrgId = RegistrationId "diff", nrgOwner = ownerA, nrgOrder = fields, nrgYears = 2} withTransaction cStore $ \db -> do insertInvoiceRow db "inv-diff" (addUTCTime 3600 orderTime) "open" @@ -667,8 +677,9 @@ testMoveSizes = withOwner $ \ctx -> do postMove ctx (nrId r) (KM.insert "owner" (J.String $ T.replicate 5300 "0") body) `shouldReturn` refusedWith 400 "bad_request" move ctx keyA r to {ofSealed = T.replicate 11096 "s"} `shouldReturn` refusedWith 400 "bad_request" valid <- moveBody ctx keyA (nrId r) to - forM_ [KM.insert "labelHash" (J.String $ T.toUpper $ hex $ (\(LabelHash h) -> h) $ ofLabelHash to) valid, KM.delete "signature" valid, KM.insert "length" "11" valid] $ \malformed -> + forM_ [KM.insert "labelHash" (J.String $ T.toUpper $ hex $ (\(LabelHash h) -> h) $ ofLabelHash to) valid, KM.delete "signature" valid, KM.insert "length" "11" valid, KM.delete "terms" valid] $ \malformed -> postMove ctx (nrId r) malformed `shouldReturn` refusedWith 400 "bad_request" + forM_ malformedTerms $ \t -> move ctx keyA r to {ofAgreedTerms = t} `shouldReturn` refusedWith 400 "bad_request" post ctx (registrationPath (nrId r) <> "/name") "[]" `shouldReturn` refusedWith 400 "bad_request" stored ctx r `shouldReturn` r -- an owner in the body is ignored diff --git a/tests/Bots/StoreService/NamesSignatureTests.hs b/tests/Bots/StoreService/NamesSignatureTests.hs index dfbeef24b0..0277542ac9 100644 --- a/tests/Bots/StoreService/NamesSignatureTests.hs +++ b/tests/Bots/StoreService/NamesSignatureTests.hs @@ -36,30 +36,49 @@ namesSignatureTests = describe "StoreService names: signatures" $ do it "hashes ClearRecords" $ clearRecordsDigest (resolverDomain chainId resolver) (namehash TLDSimplex "dynamis") 4 99 `shouldBe` hx "0xca4bd243288f27f38e3b45384ba36ce685ff155352cf8125f031e6fb189a46dd" describe "EIP-191 messages" $ do - it "signs an order" $ + -- the contract's Appendix C, which the webapp computed with viem and pins in test/terms-vectors.test.ts + it "signs an order with the terms version" $ testMessage - (orderMessage dynamisOrder timestamp) - "simplex.domains order 0x78b602f03f164366531da963764c1f2fdfd588d0487daf2aa261ca799e5ffe33 7 69259500 0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1 0xb1971beb567c8dc1b2f01f0d823754c6168d8d6830db2221702a7678e1804674 1792574100" - "0x5deee1b3589df9e53665e34aa65a491a4eabfd76a39207b0e9303457f7c44b7a" - "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a84df9ffdccc39c5cb827cde4d846c7662f591665f1b868ab1c15778646d3f414e1b" + (orderMessage termsOrder termsTimestamp) + "simplex.domains order 0xbcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6 11 69292800 0x5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5 0x22709445157a2a1f7cb7e284e3ccf6c7ebc3b4d857002af452853c4a65907fe2 2026-10-23 1792828800" + "0x50dfb031e558f78b83fa07c7ea0207c02a98d73751e43bc754dd26b7bc92b840" + "0x17107b11494e25098efc348aca64a2469ffb832bcc79dc57377a1b26a4004b6701ad73648f50af8b39d4079820a3de7e681ddf15f8cf15b5cff02b4a48cf1fe61c" + it "signs an order with the community terms" $ + testMessage + (orderMessage communityOrder termsTimestamp) + "simplex.domains order 0xbcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6 11 69292800 0x5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5 0x22709445157a2a1f7cb7e284e3ccf6c7ebc3b4d857002af452853c4a65907fe2 2026-10-23/community 1792828800" + "0xf8521525761939c4cef8979cb54217e9e32b1a57999fc2f03e486f4eb1903802" + "0x03eccb591b79ab4e3e6f22ccbeb77a6ac32c6dad8cb318557836258c390bc31d78852a3279c6c8cc2ef8d1025641d45ba4101f54dfd84c264deb2ab391c1cbc61c" it "signs find with the owner in lowercase" $ testMessage (findMessage owner timestamp) "simplex.domains find 0x014f9c09cceedef1b713110d04d3a07d5b5d9b73 1792574100" "0x08a989fbf0457984c062b44d7237be9c7948f2cf23d48410609777a09b515646" "0xfa0908260c3f21c8e47bd37e2c3cf3bd6bc62966591ab1a2c4e31695646548f47d2e79b34413be27b4b07330682de642f48a8f5e641449d7d79728eeb4ab14da1c" - it "signs another name for an order, with its duration" $ + it "signs another name for an order with the terms version" $ testMessage - (retargetMessage registrationId dynamisOrder {ofLabelHash = labelHash "another"} timestamp) - "simplex.domains name t2Jk8RmQ4vXw9NcP3yLb6g 0x5bfbbf75311ec002834031d73fa30bfe8bdf675f4a3df72443841490c4b49d02 7 69259500 0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1 0xb1971beb567c8dc1b2f01f0d823754c6168d8d6830db2221702a7678e1804674 1792574100" - "0x8bc79aa98fbd53013e8961db27554ecd17139e14ec168b86afbaf2fc910bfc52" - "0xe3d680bcc62255179d5260beeb920319c86bd919f6b01927bee01509cf6c1fe3121151947fb9e3a310e6652c0c9a9bf7ceb48e8d903774a38a3162d916e8b4591b" - it "signs a spend from an order's balance, as the webapp signs it" $ + (retargetMessage registrationId termsOrder termsTimestamp) + "simplex.domains name t2Jk8RmQ4vXw9NcP3yLb6g 0xbcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6 11 69292800 0x5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5 0x22709445157a2a1f7cb7e284e3ccf6c7ebc3b4d857002af452853c4a65907fe2 2026-10-23 1792828800" + "0x01eda78f875428396a940b658b910b6eb6dc0bce82860c17d59f8f16e7767d25" + "0x1d29abf2489da963d59d0982074b5af770efbaf96be3c756afbd6a7fea672b507e756944da0901724e2a5d7cfe2419ccb7573310ae65e65f4fd35d7c6fe494b51c" + it "signs another name for an order with the community terms" $ testMessage - (spendMessage registrationId spendOrder timestamp) - "simplex.domains spend t2Jk8RmQ4vXw9NcP3yLb6g 0x5bfbbf75311ec002834031d73fa30bfe8bdf675f4a3df72443841490c4b49d02 7 69259500 0xbb87d2d7aa14251a04cdd68b5d878d0b4c44c6c76486d90a534dab9525105e33 0x51431a705eb4903084cc8dbf3c1b966c9372914511c4623c889d49fdb8047da4 1792574100" - "0x72c2f4b7d84839fb97eb78668355a02fc0b18e211f264fca97d19a8f36decde3" - "0x8668cb4b72295479b4f9bcc2fd07fabfec2b4b8ae9230f2553536acb93c3e0cf6b2dad928a9a11b003c43ab43edeec117ce01c9214b8e99fe75fd547425e74481b" + (retargetMessage registrationId communityOrder termsTimestamp) + "simplex.domains name t2Jk8RmQ4vXw9NcP3yLb6g 0xbcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6 11 69292800 0x5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5 0x22709445157a2a1f7cb7e284e3ccf6c7ebc3b4d857002af452853c4a65907fe2 2026-10-23/community 1792828800" + "0x4f1af02904f9f8863e92169f6e1a1d6733a3a25c5d22ffde7ac00f09a660c022" + "0x30d027224b85cd9ac638edec1266748f21f46279eda72e68448f97b8fce73ca06c401035ad5416fc1e48f39646d0637fcaad1588ec74562ff842821b33e299361b" + it "signs a spend from an order's balance with the terms version" $ + testMessage + (spendMessage registrationId termsOrder termsTimestamp) + "simplex.domains spend t2Jk8RmQ4vXw9NcP3yLb6g 0xbcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6 11 69292800 0x5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5 0x22709445157a2a1f7cb7e284e3ccf6c7ebc3b4d857002af452853c4a65907fe2 2026-10-23 1792828800" + "0x109f0bd4f5eb1aa054f17c82a0b04dbd5d1e7b23c5ee91cf69464fb1435eb034" + "0x20e930097b35350861332afa4b0b5dcff746aa879875269cc784575e6bb871711d3f6b1e11437d0a616a37a8b5b1ebbdd4c98aee04e09405ac3fa55a6af6ff831b" + it "signs a spend from an order's balance with the community terms" $ + testMessage + (spendMessage registrationId communityOrder termsTimestamp) + "simplex.domains spend t2Jk8RmQ4vXw9NcP3yLb6g 0xbcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6 11 69292800 0x5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5 0x22709445157a2a1f7cb7e284e3ccf6c7ebc3b4d857002af452853c4a65907fe2 2026-10-23/community 1792828800" + "0xd114398466a7a6d7d9a183feb4624e06c41d1dcdfb6b69e896ff9d74c70845c3" + "0x90d2ab549b2cf5fd51442ac3f410b934e7fa45bd12853cfbfa2f8ab4876ed5600b9fe3a90459140f180949353e7dc3f56cd1447de31ae5c05e12f7bf6a113f061c" describe "signature format" $ do it "refuses a high s, though recovery accepts it" testHighS it "refuses v other than 27 or 28" $ @@ -112,10 +131,11 @@ testHighS :: IO () testHighS = do parseSignature highS `shouldBe` Left SEHighS let (rsR, (rsS, v)) = B.splitAt 32 <$> B.splitAt 32 (hx highS) - recoverAddress RecoverableSignature {rsR, rsS, rsRecId = B.head v - 27} (personalDigest $ orderMessage dynamisOrder timestamp) + recoverAddress RecoverableSignature {rsR, rsS, rsRecId = B.head v - 27} (personalDigest $ orderMessage termsOrder termsTimestamp) `shouldReturn` Right owner where - highS = "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a8b206002333c63a347d8321b27b93899bc51d768793c21589fe7ae62862f6fff31c" + -- the order vector's signature with s replaced by n - s and v flipped + highS = "0x17107b11494e25098efc348aca64a2469ffb832bcc79dc57377a1b26a4004b67fe528c9b70af5074c62bf867df5c21805290fdd0b6798a85efe233428767215b1b" -- fixtures @@ -132,26 +152,20 @@ dynamis = rChannelLink = Nothing } -dynamisOrder :: OrderFields -dynamisOrder = +-- Appendix C's derived inputs: alicebakery with no links, its commitment as the webapp builds it, and a new seal under the fixture seed and a zero IV +termsOrder :: OrderFields +termsOrder = OrderFields - { ofLabelHash = labelHash "dynamis", - ofLength = 7, - ofDuration = duration, - ofCommitment = commitment, - ofSealed = "q3Zk0Yx7c2VhbGVkLW9yZGVyLWZpeHR1cmU" + { ofLabelHash = labelHash "alicebakery", + ofLength = 11, + ofDuration = 69292800, + ofCommitment = hx "0x5e62a4cb7da596cb38169a6a552b5f69db2acee279b53be6693a78e03c3dfaa5", + ofSealed = "AAAAAAAAAAAAAAAACWgxqMk-UKfpra_Zh6vc4KNBR6SX1WYpGb4COoWCCbM0bisYCH14WLzghp8hqi_-KyXSi4qf_DAdP_M4bFetD0s", + ofAgreedTerms = "2026-10-23" } --- the webapp's spend vector (test/flow.test.ts, SPEND): "another" with no links, sealed under a zero IV -spendOrder :: OrderFields -spendOrder = - OrderFields - { ofLabelHash = labelHash "another", - ofLength = 7, - ofDuration = duration, - ofCommitment = hx "0xbb87d2d7aa14251a04cdd68b5d878d0b4c44c6c76486d90a534dab9525105e33", - ofSealed = "AAAAAAAAAAAAAAAACWgxqMk-UKfpra_bgbzR57AIDvSNmCR_G7IYbN7DX70sL2FVRHB1VfC0wdF-tc3ajHM3o8koM159Ja8hyg" - } +communityOrder :: OrderFields +communityOrder = termsOrder {ofAgreedTerms = "2026-10-23/community"} commitment :: ByteString commitment = hx "0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1" @@ -163,6 +177,10 @@ node = hx "0x833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb" timestamp :: Word64 timestamp = 1792574100 +-- 24 October 2026 08:00 UTC +termsTimestamp :: Word64 +termsTimestamp = 1792828800 + -- 2 years of 365 days and the free days until 1 January 2027 duration :: Word64 duration = 69259500 @@ -171,7 +189,7 @@ registrationId :: RegistrationId registrationId = RegistrationId "t2Jk8RmQ4vXw9NcP3yLb6g" orderSignature :: Text -orderSignature = "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a84df9ffdccc39c5cb827cde4d846c7662f591665f1b868ab1c15778646d3f414e1b" +orderSignature = "0x17107b11494e25098efc348aca64a2469ffb832bcc79dc57377a1b26a4004b6701ad73648f50af8b39d4079820a3de7e681ddf15f8cf15b5cff02b4a48cf1fe61c" retString :: ByteString retString = hx "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002168747470733a2f2f736d70382e73696d706c65782e696d2f6123636f6e7461637400000000000000000000000000000000000000000000000000000000000000" diff --git a/tests/Bots/StoreService/NamesSpendTests.hs b/tests/Bots/StoreService/NamesSpendTests.hs index 1758925684..b100e228d7 100644 --- a/tests/Bots/StoreService/NamesSpendTests.hs +++ b/tests/Bots/StoreService/NamesSpendTests.hs @@ -9,7 +9,7 @@ import Bots.StoreService.BotTests (operatorCmd, withStoreService) import Bots.StoreService.FakeBTCPay (FakeBTCPay (..), setInvoiceState, setRates, webhookEvent, webhookSigHeader, withFakeBTCPay) import Bots.StoreService.FakeEthNode import Bots.StoreService.NamesContractsTests (reservedNamesCall) -import Bots.StoreService.NamesOrdersTests (Ctx (..), Order (..), advance, aliceOrder, commitmentOf, field, fieldsOf, invoiceRegistration, keyA, keyB, orderTime, ownerAText, placeOrder, placed, post, refusedWith, registrationById, registrationOf, registrationsOf, settleNow, withOrdersOn, yearsDuration) +import Bots.StoreService.NamesOrdersTests (Ctx (..), Order (..), advance, aliceOrder, commitmentOf, field, fieldsOf, invoiceRegistration, keyA, keyB, malformedTerms, orderTime, ownerAText, placeOrder, placed, post, refusedWith, registrationById, registrationOf, registrationsOf, settleNow, withOrdersOn, yearsDuration) import Bots.StoreService.NamesOwnerTests (findBody, postFind) import Bots.StoreService.NamesStoreTests (at, order, ownerA, placePaid, reg, t0, write) import Bots.StoreService.WebTests (StubCall (..), capturingStderr, StubState (..), jsonObject, rcv, sampleProviderInvoice, statusOf, stubCalls, webGet, webRequestBody, withServiceStores) @@ -17,7 +17,7 @@ import ChatTests.DBUtils (TestParams) import Control.Concurrent.Async (Async, async, cancel, concurrently, wait) import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) import Control.Concurrent.STM (atomically, check) -import Control.Monad (join, replicateM_, void) +import Control.Monad (forM_, join, replicateM_, void) import qualified Data.Aeson as J import qualified Data.Aeson.KeyMap as KM import qualified Data.Aeson.Types as JT @@ -182,12 +182,13 @@ spendBody Ctx {cRandom} source SpendBody {sbOrder = o@Order {oKey, oPrice, oTime ("sealed", J.String ofSealed), ("price", J.toJSON oPrice), ("balance", J.toJSON sbBalance), + ("terms", J.String ofAgreedTerms), ("timestamp", J.toJSON oTimestamp), ("signature", J.String $ hex $ serializeSignature sig) ] <> maybe [] (\m -> [("method", J.String m)]) sbMethod where - fields@OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} = fieldsOf o + fields@OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} = fieldsOf o postSpend :: Ctx -> RegistrationId -> J.Object -> IO (Int, Maybe J.Object) postSpend ctx (RegistrationId rid) = post ctx ("/api/registration/" <> T.unpack rid <> "/spend") . J.encode @@ -259,6 +260,7 @@ testCoveredSpend = withSpend $ \ctx -> do r <- registrationById ctx rid (nrPhase r, nrLive r, nrPrice r, nrPayment r, nrBalanceSource r, nrBalanceUsed r) `shouldBe` (PQueued, True, 10000, RPBalance, Just src, 10000) nrOrderHash r `shouldBe` orderHash ownerA (fieldsOf aliceOrder) (OTBalance src 10000 15000 Nothing) + nrAgreedTerms r `shouldBe` oTerms aliceOrder view <- viewOf ctx rid (field "phase" view, field "price" view, field "fromBalance" view, field "balance" view) `shouldBe` (Just "queued", Just (J.Number 10000), Just (J.Number 10000), Just J.Null) amountOf ctx src `shouldReturn` Just 5000 @@ -282,6 +284,7 @@ testDifferenceSpend = withSpend $ \ctx -> do holder <- invoiceRegistration ctx invId (nrPhase holder, nrLive holder, nrPrice holder, nrPayment holder, nrBalanceSource holder, nrBalanceUsed holder) `shouldBe` (PUnpaid, True, 10000, RPInvoice btc invId, Just src, 6000) nrOrderHash holder `shouldBe` orderHash ownerA (fieldsOf aliceOrder) (OTBalance src 10000 6000 (Just btc)) + nrAgreedTerms holder `shouldBe` oTerms aliceOrder fmap irAmount <$> getInvoice (cStore ctx) invId `shouldReturn` Just (CurrencyAmount 4000) field "balance" <$> viewOf ctx src `shouldReturn` Just (J.object ["amount" J..= (6000 :: Int), "held" J..= True, "heldUntil" J..= expiry, "refunded" J..= (0 :: Int)]) @@ -432,6 +435,9 @@ testSpendSizes = withSpend $ \ctx -> do src <- sourceOf ctx "src" 15000 missing <- KM.delete "sealed" <$> spendBody ctx src (covered 15000) postSpend ctx src missing `shouldReturn` refusedWith 400 "bad_request" + noTerms <- KM.delete "terms" <$> spendBody ctx src (covered 15000) + postSpend ctx src noTerms `shouldReturn` refusedWith 400 "bad_request" + forM_ malformedTerms $ \t -> spend ctx src (covered 15000) {sbOrder = aliceOrder {oTerms = t}} `shouldReturn` refusedWith 400 "bad_request" spend ctx src (covered 15000) {sbOrder = aliceOrder {oSealed = T.replicate 11096 "a"}} `shouldReturn` refusedWith 400 "bad_request" padded <- KM.insert "padding" (J.String $ T.replicate 16384 "a") <$> spendBody ctx src (covered 15000) postSpend ctx src padded `shouldReturn` refusedWith 400 "bad_request" diff --git a/tests/Bots/StoreService/NamesStoreTests.hs b/tests/Bots/StoreService/NamesStoreTests.hs index 985f3438c4..46938f2df3 100644 --- a/tests/Bots/StoreService/NamesStoreTests.hs +++ b/tests/Bots/StoreService/NamesStoreTests.hs @@ -5,6 +5,7 @@ module Bots.StoreService.NamesStoreTests (namesStoreTests, at, btc, insertInvoiceRow, invoiceStatus, issue, newReg, order, ownerA, ownerB, paidOrder, placeCode, placePaid, price, reg, reveal, signature, t0, usable, write) where +import Bots.StoreService.ConfigTests (termsVersion) import Bots.StoreService.WebTests (columnsOf, digestFixture, runMigrations, withServiceStore) import Control.Arrow ((&&&)) import Control.Monad (forM_, void) @@ -118,7 +119,8 @@ order label n = ofLength = T.length label, ofDuration = 69292800, ofCommitment = digestFixture n, - ofSealed = "sealed-" <> label <> "-" <> T.pack (show n) + ofSealed = "sealed-" <> label <> "-" <> T.pack (show n), + ofAgreedTerms = termsVersion } newReg :: Text -> Address -> OrderFields -> NewRegistration @@ -319,6 +321,7 @@ testOrderHashFields = do ("owner", orderHash ownerB o paid), ("commitment", orderHash ownerA o {ofCommitment = digestFixture 2} paid), ("sealed", orderHash ownerA o {ofSealed = ofSealed o <> "x"} paid), + ("terms", orderHash ownerA o {ofAgreedTerms = ofAgreedTerms o <> "/community"} paid), ("method", orderHash ownerA o (OTInvoice (SPMCard CPStripe) price)), ("price", orderHash ownerA o (OTInvoice btc (price + 1))), ("code instead of invoice", orderHash ownerA o (OTCode $ digestFixture 3)), @@ -332,6 +335,7 @@ testOrderHashBoundaries :: IO () testOrderHashBoundaries = do let o = order "alicebakery" 1 orderHash ownerA o {ofSealed = "ab"} (OTCode "c") `shouldNotBe` orderHash ownerA o {ofSealed = "a"} (OTCode "bc") + orderHash ownerA o {ofSealed = "ab", ofAgreedTerms = "c"} (OTCode "d") `shouldNotBe` orderHash ownerA o {ofSealed = "a", ofAgreedTerms = "bc"} (OTCode "d") testCreatePaid :: IO () testCreatePaid = withServiceStore $ \st -> do @@ -348,6 +352,7 @@ testCreatePaid = withServiceStore $ \st -> do nrOwner = ownerA, nrCommitment = ofCommitment o, nrSealed = ofSealed o, + nrAgreedTerms = termsVersion, nrPhase = PUnpaid, nrYears = twoYears, nrPrice = price, @@ -512,7 +517,8 @@ testRoundTrip = withServiceStore $ \st -> do nrFailedAt = Just (at 2), nrError = Just "The name was not signed in time", nrLinksRefusedAt = Just (at 4), - nrMoves = 2 + nrMoves = 2, + nrAgreedTerms = termsVersion <> "/community" } write st changed `shouldReturn` Written 2 reg st "r1" `shouldReturn` changed {nrVersion = 2, nrLive = False, nrUpdatedAt = at 1} @@ -598,7 +604,7 @@ testWriteMeetsLiveOrder = withServiceStore $ \st -> do testRetarget :: IO () testRetarget = withServiceStore $ \st -> do let o = order "alicebakery" 1 - o' = order "alicebakers" 2 + o' = (order "alicebakers" 2) {ofAgreedTerms = termsVersion <> "/community"} placePaid st "r1" ownerA o (at 60) `shouldReturn` Right () r <- reg st "r1" let failed = @@ -624,6 +630,7 @@ testRetarget = withServiceStore $ \st -> do nrLabelHash = ofLabelHash o', nrCommitment = ofCommitment o', nrSealed = ofSealed o', + nrAgreedTerms = ofAgreedTerms o', nrPrice = 10000, nrPhase = PQueued, nrMoves = 1, diff --git a/tests/Bots/StoreService/NamesWorkerTests.hs b/tests/Bots/StoreService/NamesWorkerTests.hs index 043ea190fd..fe4748f279 100644 --- a/tests/Bots/StoreService/NamesWorkerTests.hs +++ b/tests/Bots/StoreService/NamesWorkerTests.hs @@ -5,6 +5,7 @@ module Bots.StoreService.NamesWorkerTests (namesWorkerTests) where +import Bots.StoreService.ConfigTests (termsVersion) import Bots.StoreService.FakeEthNode import Bots.StoreService.NamesContractsTests (availableCall, maxCommitmentAgeCall, minCommitmentAgeCall, noncesCall, registrarOf, unhex) import Bots.StoreService.NamesOrdersTests @@ -505,7 +506,7 @@ testTwoWallets = withWorker $ \w@W {wCtx = ctx} -> do -- | The order of alicebakery, by owner A, with this commitment. aliceFields :: Int -> OrderFields -aliceFields n = OrderFields {ofLabelHash = labelHash "alicebakery", ofLength = 11, ofDuration = yearsDuration 2 orderTime, ofCommitment = commitmentOf n, ofSealed = "sealed-alicebakery-" <> tshow n} +aliceFields n = OrderFields {ofLabelHash = labelHash "alicebakery", ofLength = 11, ofDuration = yearsDuration 2 orderTime, ofCommitment = commitmentOf n, ofSealed = "sealed-alicebakery-" <> tshow n, ofAgreedTerms = termsVersion} aliceRegistration :: Text -> Int -> NewRegistration aliceRegistration rid n = NewRegistration {nrgId = RegistrationId rid, nrgOwner = ownerA, nrgOrder = aliceFields n, nrgYears = 2}