Skip to content

Commit d882ffb

Browse files
authored
Merge pull request #122 from mlabs-haskell/misha/await-status-change-rework
Misha/await status change rework
2 parents 9acd0b0 + bcea30f commit d882ffb

File tree

10 files changed

+267
-63
lines changed

10 files changed

+267
-63
lines changed

bot-plutus-interface.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ test-suite bot-plutus-interface-test
177177
Spec.BotPlutusInterface.Contract
178178
Spec.BotPlutusInterface.ContractStats
179179
Spec.BotPlutusInterface.Server
180+
Spec.BotPlutusInterface.TxStatusChange
180181
Spec.BotPlutusInterface.UtxoParser
181182
Spec.MockContract
182183

src/BotPlutusInterface/Config.hs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import BotPlutusInterface.Effects (
1414
ShellArgs (..),
1515
callLocalCommand,
1616
)
17-
import BotPlutusInterface.Types (CLILocation (..), LogLevel (..), PABConfig (..))
17+
import BotPlutusInterface.Types (
18+
CLILocation (..),
19+
LogLevel (..),
20+
PABConfig (..),
21+
TxStatusPolling (TxStatusPolling, spBlocksTimeOut, spInterval),
22+
)
1823

1924
import Cardano.Api (NetworkId (Mainnet, Testnet), unNetworkMagic)
2025
import Config (Section (Section), Value (Atom, Sections, Text))
@@ -24,6 +29,7 @@ import Config.Schema (
2429
atomSpec,
2530
generateDocs,
2631
naturalSpec,
32+
reqSection',
2733
sectionsSpec,
2834
trueOrFalseSpec,
2935
(<!>),
@@ -75,6 +81,29 @@ logLevelSpec =
7581
<!> Info <$ atomSpec "info"
7682
<!> Debug <$ atomSpec "debug"
7783

84+
instance ToValue TxStatusPolling where
85+
toValue (TxStatusPolling interval timeout) =
86+
Sections
87+
()
88+
[ Section () "pollingInterval" $ toValue interval
89+
, Section () "pollingTimeout" $ toValue timeout
90+
]
91+
92+
txStatusPollingSpec :: ValueSpec TxStatusPolling
93+
txStatusPollingSpec =
94+
sectionsSpec "TxStatusPolling configuration" $ do
95+
spInterval <-
96+
reqSection'
97+
"microseconds"
98+
naturalSpec
99+
"Interval between chain-index queries for transactions status change detection"
100+
spBlocksTimeOut <-
101+
reqSection'
102+
"blocks"
103+
naturalSpec
104+
"Timeout (in blocks) after which awaiting of transaction status change will be cancelled and current Status returned"
105+
pure $ TxStatusPolling {..}
106+
78107
{- ORMOLU_DISABLE -}
79108
instance ToValue PABConfig where
80109
toValue
@@ -98,6 +127,7 @@ instance ToValue PABConfig where
98127
pcCollectStats
99128
pcCollectLogs
100129
pcBudgetMultiplier
130+
pcTxStatusPolling
101131
) =
102132
Sections
103133
()
@@ -121,6 +151,7 @@ instance ToValue PABConfig where
121151
, Section () "collectStats" $ toValue pcCollectStats
122152
, Section () "collectLogs" $ toValue pcCollectLogs
123153
, Section () "budgetMultiplier" $ toValue pcBudgetMultiplier
154+
, Section () "pcTxStatusPolling" $ toValue pcTxStatusPolling
124155
]
125156
{- ORMOLU_ENABLE -}
126157

@@ -225,6 +256,13 @@ pabConfigSpec = sectionsSpec "PABConfig" $ do
225256
customRationalSpec
226257
"Multiplier on the budgets automatically calculated"
227258

259+
pcTxStatusPolling <-
260+
sectionWithDefault'
261+
(pcTxStatusPolling def)
262+
"pcTxStatusPolling"
263+
txStatusPollingSpec
264+
"Set interval between `chain-index` queries and number of blocks to wait until timeout while await Transaction status to change"
265+
228266
pure PABConfig {..}
229267

230268
docPABConfig :: String

src/BotPlutusInterface/Contract.hs

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import Control.Monad.Trans.Class (lift)
4545
import Control.Monad.Trans.Either (EitherT, eitherT, firstEitherT, newEitherT)
4646
import Data.Aeson (ToJSON, Value (Array, Bool, Null, Number, Object, String))
4747
import Data.Aeson.Extras (encodeByteString)
48-
import Data.Either (fromRight)
48+
import Data.Function (fix)
4949
import Data.HashMap.Strict qualified as HM
5050
import Data.Kind (Type)
5151
import Data.Map qualified as Map
@@ -203,34 +203,62 @@ handlePABReq contractEnv req = do
203203
printBpiLog @w Debug $ pretty resp
204204
pure resp
205205

206+
{- | Await till transaction status change to something from `Unknown`.
207+
Uses `chain-index` to query transaction by id.
208+
Important notes:
209+
* if transaction is not found in `chain-index` status considered to be `Unknown`
210+
* if transaction is found but `transactionStatus` failed to make status - status considered to be `Unknown`
211+
* uses `TxStatusPolling` to set `chain-index` polling interval and number of blocks to wait until timeout,
212+
if timeout is reached, returns whatever status it was able to get during last check
213+
-}
206214
awaitTxStatusChange ::
207215
forall (w :: Type) (effs :: [Type -> Type]).
208216
Member (PABEffect w) effs =>
209217
ContractEnvironment w ->
210218
Ledger.TxId ->
211219
Eff effs TxStatus
212220
awaitTxStatusChange contractEnv txId = do
213-
-- The depth (in blocks) after which a transaction cannot be rolled back anymore (from Plutus.ChainIndex.TxIdState)
214-
let chainConstant = 8
215-
216-
mTx <- queryChainIndexForTxState
217-
case mTx of
218-
Nothing -> pure Unknown
219-
Just txState -> do
220-
printBpiLog @w Debug $ "Found transaction in node, waiting" <+> pretty chainConstant <+> " blocks for it to settle."
221-
awaitNBlocks @w contractEnv (chainConstant + 1)
222-
-- Check if the tx is still present in chain-index, in case of a rollback
223-
-- we might not find it anymore.
224-
ciTxState' <- queryChainIndexForTxState
225-
case ciTxState' of
226-
Nothing -> pure Unknown
227-
Just _ -> do
228-
blk <- fromInteger <$> currentBlock contractEnv
229-
-- This will set the validity correctly based on the txState.
230-
-- The tx will always be committed, as we wait for chainConstant + 1 blocks
231-
let status = transactionStatus blk txState txId
232-
pure $ fromRight Unknown status
221+
checkStartedBlock <- currentBlock contractEnv
222+
printBpiLog @w Debug $ pretty $ "Awaiting status change for " ++ show txId
223+
224+
let txStatusPolling = contractEnv.cePABConfig.pcTxStatusPolling
225+
pollInterval = fromIntegral $ txStatusPolling.spInterval
226+
pollTimeout = txStatusPolling.spBlocksTimeOut
227+
cutOffBlock = checkStartedBlock + fromIntegral pollTimeout
228+
229+
fix $ \loop -> do
230+
currBlock <- currentBlock contractEnv
231+
txStatus <- getStatus
232+
case (txStatus, currBlock > cutOffBlock) of
233+
(status, True) -> do
234+
logDebug . mconcat . fmap mconcat $
235+
[ ["Timeout for waiting `TxId ", show txId, "` status change reached"]
236+
, [" - waited ", show pollTimeout, " blocks."]
237+
, [" Current status: ", show status]
238+
]
239+
return status
240+
(Unknown, _) -> do
241+
threadDelay @w pollInterval
242+
loop
243+
(status, _) -> return status
233244
where
245+
getStatus = do
246+
mTx <- queryChainIndexForTxState
247+
case mTx of
248+
Nothing -> do
249+
logDebug $ "TxId " ++ show txId ++ " not found in index"
250+
return Unknown
251+
Just txState -> do
252+
logDebug $ "TxId " ++ show txId ++ " found in index, checking status"
253+
blk <- fromInteger <$> currentBlock contractEnv
254+
case transactionStatus blk txState txId of
255+
Left e -> do
256+
logDebug $ "Status check for TxId " ++ show txId ++ " failed with " ++ show e
257+
return Unknown
258+
Right st -> do
259+
logDebug $ "Status for TxId " ++ show txId ++ " is " ++ show st
260+
return st
261+
234262
queryChainIndexForTxState :: Eff effs (Maybe TxIdState)
235263
queryChainIndexForTxState = do
236264
mTx <- join . preview _TxIdResponse <$> (queryChainIndex @w $ TxFromTxId txId)
@@ -240,6 +268,8 @@ awaitTxStatusChange contractEnv txId = do
240268
pure . Just $ fromTx blk tx
241269
Nothing -> pure Nothing
242270

271+
logDebug = printBpiLog @w Debug . pretty
272+
243273
-- | This will FULLY balance a transaction
244274
balanceTx ::
245275
forall (w :: Type) (effs :: [Type -> Type]).
@@ -355,25 +385,25 @@ awaitSlot contractEnv s@(Slot n) = do
355385
| n < tip'.slot -> pure $ Slot tip'.slot
356386
_ -> awaitSlot contractEnv s
357387

358-
-- | Wait for n Blocks.
359-
awaitNBlocks ::
360-
forall (w :: Type) (effs :: [Type -> Type]).
361-
Member (PABEffect w) effs =>
362-
ContractEnvironment w ->
363-
Integer ->
364-
Eff effs ()
365-
awaitNBlocks contractEnv n = do
366-
current <- currentBlock contractEnv
367-
go current
368-
where
369-
go :: Integer -> Eff effs ()
370-
go start = do
371-
threadDelay @w (fromIntegral contractEnv.cePABConfig.pcTipPollingInterval)
372-
tip <- CardanoCLI.queryTip @w contractEnv.cePABConfig
373-
case tip of
374-
Right tip'
375-
| start + n <= tip'.block -> pure ()
376-
_ -> go start
388+
-- -- | Wait for n Blocks.
389+
-- awaitNBlocks ::
390+
-- forall (w :: Type) (effs :: [Type -> Type]).
391+
-- Member (PABEffect w) effs =>
392+
-- ContractEnvironment w ->
393+
-- Integer ->
394+
-- Eff effs ()
395+
-- awaitNBlocks contractEnv n = do
396+
-- current <- currentBlock contractEnv
397+
-- go current
398+
-- where
399+
-- go :: Integer -> Eff effs ()
400+
-- go start = do
401+
-- threadDelay @w (fromIntegral contractEnv.cePABConfig.pcTipPollingInterval)
402+
-- tip <- CardanoCLI.queryTip @w contractEnv.cePABConfig
403+
-- case tip of
404+
-- Right tip'
405+
-- | start + n <= tip'.block -> pure ()
406+
-- _ -> go start
377407

378408
{- | Wait at least until the given time. Uses the awaitSlot under the hood, so the same constraints
379409
are applying here as well.

src/BotPlutusInterface/QueryNode.hs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,15 @@ flattenQueryResult = \case
7878
connectionInfo :: NodeInfo -> C.LocalNodeConnectInfo C.CardanoMode
7979
connectionInfo (NodeInfo netId socket) =
8080
C.LocalNodeConnectInfo
81-
( C.CardanoModeParams
82-
(C.EpochSlots 21600) -- TODO: this probably should be settable somehow?
83-
)
81+
(C.CardanoModeParams epochSlots)
8482
netId
8583
socket
84+
where
85+
-- This parameter needed only for the Byron era. Since the Byron
86+
-- era is over and the parameter has never changed it is ok to
87+
-- hardcode this. See comment on `Cardano.Api.ConsensusModeParams` in
88+
-- cardano-node.
89+
epochSlots = C.EpochSlots 21600
8690

8791
toQueryError :: Show e => e -> NodeQueryError
8892
toQueryError = NodeQueryError . pack . show

src/BotPlutusInterface/TimeSlot.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ posixTimeRangeToContainedSlotRangeIO
148148
NegInf -> pure NegInf
149149
PosInf -> pure PosInf
150150

151-
-- helper to calulate bound's closure
151+
-- helper to calculate bound's closure
152152
-- if bound is not `NegInf` or `PosInf`, then `Closure` need to be calculated
153153
-- https://github.com/input-output-hk/plutus-apps/blob/e51f57fa99f4cc0942ba6476b0689e43f0948eb3/plutus-ledger/src/Ledger/TimeSlot.hs#L125-L130
154154
getExtClosure ::
@@ -186,7 +186,7 @@ posixTimeToSlot sysStart eraHist pTime = do
186186
toUtc (Ledger.POSIXTime milliseconds) =
187187
posixSecondsToUTCTime
188188
. secondsToNominalDiffTime
189-
$ fromInteger (milliseconds `div` 1000)
189+
$ fromInteger milliseconds / 1000
190190

191191
-- helper functions --
192192

src/BotPlutusInterface/Types.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module BotPlutusInterface.Types (
2222
SpendBudgets,
2323
MintBudgets,
2424
ContractStats (..),
25+
TxStatusPolling (..),
2526
LogsList (..),
2627
addBudget,
2728
) where
@@ -89,6 +90,21 @@ data PABConfig = PABConfig
8990
, -- | Collect logs inside ContractEnvironment, doesn't depend on log level
9091
pcCollectLogs :: !Bool
9192
, pcBudgetMultiplier :: !Rational
93+
, pcTxStatusPolling :: !TxStatusPolling
94+
}
95+
deriving stock (Show, Eq)
96+
97+
{- | Settings for `Contract.awaitTxStatusChange` implementation.
98+
See also `BotPlutusInterface.Contract.awaitTxStatusChange`
99+
-}
100+
data TxStatusPolling = TxStatusPolling
101+
{ -- | Interval between `chain-index` queries, microseconds
102+
spInterval :: !Natural
103+
, -- | Number of blocks to wait until timeout.
104+
-- Timeout is required because transaction can be silently discarded from node mempool
105+
-- and never appear in `chain-index` even if it was submitted successfully to the node
106+
-- (chain-sync protocol won't help here also)
107+
spBlocksTimeOut :: !Natural
92108
}
93109
deriving stock (Show, Eq)
94110

@@ -240,6 +256,7 @@ instance Default PABConfig where
240256
, pcCollectStats = False
241257
, pcCollectLogs = False
242258
, pcBudgetMultiplier = 1
259+
, pcTxStatusPolling = TxStatusPolling 1_000_000 8
243260
}
244261

245262
data RawTx = RawTx

test/Spec.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Spec.BotPlutusInterface.Balance qualified
44
import Spec.BotPlutusInterface.Contract qualified
55
import Spec.BotPlutusInterface.ContractStats qualified
66
import Spec.BotPlutusInterface.Server qualified
7+
import Spec.BotPlutusInterface.TxStatusChange qualified
78
import Spec.BotPlutusInterface.UtxoParser qualified
89
import Test.Tasty (TestTree, defaultMain, testGroup)
910
import Prelude
@@ -25,4 +26,5 @@ tests =
2526
, Spec.BotPlutusInterface.Balance.tests
2627
, Spec.BotPlutusInterface.Server.tests
2728
, Spec.BotPlutusInterface.ContractStats.tests
29+
, Spec.BotPlutusInterface.TxStatusChange.tests
2830
]

test/Spec/BotPlutusInterface/Config.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module Spec.BotPlutusInterface.Config (tests) where
44

55
import BotPlutusInterface.Config (loadPABConfig, savePABConfig)
6-
import BotPlutusInterface.Types (CLILocation (..), LogLevel (..), PABConfig (..))
6+
import BotPlutusInterface.Types (CLILocation (..), LogLevel (..), PABConfig (..), TxStatusPolling (TxStatusPolling))
77
import Cardano.Api (
88
AnyPlutusScriptVersion (..),
99
CostModel (..),
@@ -115,4 +115,5 @@ pabConfigExample =
115115
, pcCollectStats = False
116116
, pcCollectLogs = False
117117
, pcBudgetMultiplier = 1
118+
, pcTxStatusPolling = TxStatusPolling 1_000_000 8
118119
}

0 commit comments

Comments
 (0)