Skip to content

Commit

Permalink
#4928 Add CLI command for printing slot number for UTC Time
Browse files Browse the repository at this point in the history
  • Loading branch information
carbolymer committed May 22, 2023
1 parent 11fccd1 commit 220e7dc
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 66 deletions.
4 changes: 2 additions & 2 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ repository cardano-haskell-packages
-- See CONTRIBUTING for information about these, including some Nix commands
-- you need to run if you change them
index-state:
, hackage.haskell.org 2023-05-10T10:34:57Z
, cardano-haskell-packages 2023-05-13T07:08:44Z
, hackage.haskell.org 2023-05-19T00:00:00Z
, cardano-haskell-packages 2023-05-19T00:00:00Z

packages:
cardano-cli
Expand Down
2 changes: 2 additions & 0 deletions cardano-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@

- Add `utcTimeToSlotNo` function to support UTC -> slot number conversion ([PR 5130](https://github.com/input-output-hk/cardano-node/pull/5130))

- Add `query slot-number` command line option to support UTC -> slot number conversion ([PR 5149](https://github.com/input-output-hk/cardano-node/pull/5149))

- Remove `--stake-address` option from `stake-address build`
([PR 5061](https://github.com/input-output-hk/cardano-node/pull/5061))

Expand Down
3 changes: 3 additions & 0 deletions cardano-cli/src/Cardano/CLI/Shelley/Commands.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import Prelude
import Cardano.Api.Shelley

import Data.Text (Text)
import Data.Time.Clock

import Cardano.CLI.Shelley.Key (DelegationTarget, PaymentVerifier, StakeIdentifier,
StakeVerifier, VerificationKeyOrFile, VerificationKeyOrHashOrFile,
Expand Down Expand Up @@ -367,6 +368,7 @@ data QueryCmd =
(Maybe (File () Out))
| QueryPoolState' SocketPath AnyConsensusModeParams NetworkId [Hash StakePoolKey]
| QueryTxMempool SocketPath AnyConsensusModeParams NetworkId TxMempoolQuery (Maybe (File () Out))
| QuerySlotNumber SocketPath AnyConsensusModeParams NetworkId UTCTime
deriving Show

renderQueryCmd :: QueryCmd -> Text
Expand All @@ -385,6 +387,7 @@ renderQueryCmd cmd =
QueryKesPeriodInfo {} -> "query kes-period-info"
QueryPoolState' {} -> "query pool-state"
QueryTxMempool _ _ _ query _ -> "query tx-mempool" <> renderTxMempoolQuery query
QuerySlotNumber {} -> "query slot-number"
where
renderTxMempoolQuery query =
case query of
Expand Down
24 changes: 20 additions & 4 deletions cardano-cli/src/Cardano/CLI/Shelley/Parsers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,8 @@ pQueryCmd envCli =
(Opt.info pQueryPoolState $ Opt.progDesc "Dump the pool state")
, subParser "tx-mempool"
(Opt.info pQueryTxMempool $ Opt.progDesc "Local Mempool info")
, subParser "slot-number"
(Opt.info pQuerySlotNumber $ Opt.progDesc "Query slot number for UTC timestamp")
]
where
pQueryProtocolParameters :: Parser QueryCmd
Expand Down Expand Up @@ -1092,6 +1094,20 @@ pQueryCmd envCli =
<*> pOperationalCertificateFile
<*> pMaybeOutputFile

pQuerySlotNumber :: Parser QueryCmd
pQuerySlotNumber =
QuerySlotNumber
<$> pSocketPath envCli
<*> pConsensusModeParams
<*> pNetworkId envCli
<*> pUtcTimestamp
where
pUtcTimestamp =
convertTime <$> (Opt.strArgument . mconcat)
[ Opt.metavar "TIMESTAMP"
, Opt.help "UTC timestamp in YYYY-MM-DDThh:mm:ssZ format"
]

pGovernanceCmd :: Parser GovernanceCmd
pGovernanceCmd =
asum
Expand Down Expand Up @@ -1475,10 +1491,6 @@ pGenesisCmd envCli =
<> Opt.completer (Opt.bashCompleter "file")
)

convertTime :: String -> UTCTime
convertTime =
parseTimeOrError False defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ"

pInitialSupplyNonDelegated :: Parser (Maybe Lovelace)
pInitialSupplyNonDelegated =
Opt.optional $
Expand Down Expand Up @@ -1654,6 +1666,10 @@ pTxMetadataJsonSchema =
-- Default to the no-schema conversion.
pure TxMetadataJsonNoSchema

convertTime :: String -> UTCTime
convertTime =
parseTimeOrError False defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ"

pMetadataFile :: Parser MetadataFile
pMetadataFile =
MetadataFileJSON <$>
Expand Down
13 changes: 11 additions & 2 deletions cardano-cli/src/Cardano/CLI/Shelley/Run/Query.hs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ runQueryCmd cmd =
runQueryPoolState mNodeSocketPath consensusModeParams network poolid
QueryTxMempool mNodeSocketPath consensusModeParams network op mOutFile ->
runQueryTxMempool mNodeSocketPath consensusModeParams network op mOutFile
QuerySlotNumber mNodeSocketPath consensusModeParams network utcTime ->
runQuerySlotNumber mNodeSocketPath consensusModeParams network utcTime

runQueryProtocolParameters
:: SocketPath
Expand Down Expand Up @@ -352,8 +354,6 @@ runQueryTip socketPath (AnyConsensusModeParams cModeParams) network mOutFile = d

-- | Query the UTxO, filtered by a given set of addresses, from a Shelley node
-- via the local state query protocol.
--

runQueryUTxO
:: SocketPath
-> AnyConsensusModeParams
Expand Down Expand Up @@ -698,6 +698,15 @@ runQueryTxMempool socketPath (AnyConsensusModeParams cModeParams) network query
Just (File oFp) -> handleIOExceptT (ShelleyQueryCmdWriteFileError . FileIOError oFp)
$ LBS.writeFile oFp renderedResult

runQuerySlotNumber
:: SocketPath
-> AnyConsensusModeParams
-> NetworkId
-> UTCTime
-> ExceptT ShelleyQueryCmdError IO ()
runQuerySlotNumber sockPath aCmp network utcTime = do
SlotNo slotNo <- utcTimeToSlotNo sockPath aCmp network utcTime
liftIO . putStr $ show slotNo

-- | Obtain stake snapshot information for a pool, plus information about the total active stake.
-- This information can be used for leader slot calculation, for example, and has been requested by SPOs.
Expand Down
6 changes: 5 additions & 1 deletion cardano-testnet/cardano-testnet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ library
, cardano-cli
, cardano-crypto-class
, cardano-git-rev
, cardano-ledger-core
, cardano-ledger-byron
, cardano-ledger-shelley
, cardano-node
, containers
, directory
, exceptions
, filepath
, hedgehog
, hedgehog-extras ^>= 0.4.5.1
, mtl
, optparse-applicative-fork
, ouroboros-network
, ouroboros-network-api
Expand All @@ -66,6 +69,7 @@ library

hs-source-dirs: src
exposed-modules: Cardano.Testnet
Testnet.Babbage
Testnet.Byron
Testnet.Options
Testnet.Util.Assert
Expand All @@ -82,7 +86,6 @@ library
Parsers.Shelley
Parsers.Version
Testnet
Testnet.Babbage
Testnet.Cardano
Testnet.Conf
Testnet.Commands.Genesis
Expand Down Expand Up @@ -149,6 +152,7 @@ test-suite cardano-testnet-test
Test.Cli.Babbage.LeadershipSchedule
Test.Cli.Babbage.StakeSnapshot
Test.Cli.KesPeriodInfo
Test.Cli.QuerySlotNumber
Test.FoldBlocks
Test.Misc
Test.Node.Shutdown
Expand Down
37 changes: 34 additions & 3 deletions cardano-testnet/src/Testnet/Util/Runtime.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

module Testnet.Util.Runtime
( LeadershipSlot(..)
Expand All @@ -19,29 +21,42 @@ module Testnet.Util.Runtime
, poolNodeStdout
, readNodeLoggingFormat
, startNode
, ShelleyGenesis(..)
, shelleyGenesis
, getStartTime
, fromNominalDiffTimeMicro
) where

import Prelude

import Control.Monad
import Data.Aeson (FromJSON)
import Control.Monad.Error.Class
import Control.Monad.IO.Class
import Control.Monad.Trans.Except
import Data.Aeson ((.:))
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A
import qualified Data.List as L

import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import GHC.Generics (Generic)
import GHC.Stack
import qualified Hedgehog as H
import Hedgehog.Extras.Stock.IO.Network.Sprocket (Sprocket (..))
import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as IO
import qualified Hedgehog.Extras.Stock.String as S
import qualified Hedgehog.Extras.Test.Base as H
import qualified Hedgehog.Extras.Test.File as H
import qualified Hedgehog.Extras.Test.Process as H

import System.FilePath.Posix ((</>))
import qualified System.Info as OS
import qualified System.IO as IO
import qualified System.Process as IO

import Cardano.Api
import Cardano.Ledger.Crypto (StandardCrypto)
import Cardano.Ledger.Shelley.Genesis
import qualified Testnet.Util.Process as H

data NodeLoggingFormat = NodeLoggingFormatAsJson | NodeLoggingFormatAsText deriving (Eq, Show)
Expand Down Expand Up @@ -108,6 +123,22 @@ bftSprockets = fmap nodeSprocket . bftNodes
poolSprockets :: TestnetRuntime -> [Sprocket]
poolSprockets = fmap (nodeSprocket . poolRuntime) . poolNodes

shelleyGenesis :: (H.MonadTest m, MonadIO m, HasCallStack) => TestnetRuntime -> m (ShelleyGenesis StandardCrypto)
shelleyGenesis TestnetRuntime{shelleyGenesisFile} = withFrozenCallStack $
H.evalEither =<< H.evalIO (A.eitherDecodeFileStrict' shelleyGenesisFile)

getStartTime :: (H.MonadTest m, MonadIO m, HasCallStack) => FilePath -> TestnetRuntime -> m UTCTime
getStartTime tempRootPath TestnetRuntime{configurationFile} = withFrozenCallStack $ H.evalEither <=< H.evalIO . runExceptT $ do
testnetConfig <- ExceptT $ A.eitherDecodeFileStrict' configurationFile
byronGenesisFile <- liftEither $ A.parseEither
(A.withObject ("testnet configuration file: " <> configurationFile) (.: "ByronGenesisFile"))
testnetConfig
let byronGenesisFilePath = tempRootPath </> byronGenesisFile
genesisConfig <- ExceptT $ A.eitherDecodeFileStrict' byronGenesisFilePath
fmap posixSecondsToUTCTime . liftEither $ A.parseEither
(A.withObject ("byron genesis file: " <> byronGenesisFilePath) (.: "startTime"))
genesisConfig

readNodeLoggingFormat :: String -> Either String NodeLoggingFormat
readNodeLoggingFormat = \case
"json" -> Right NodeLoggingFormatAsJson
Expand Down
140 changes: 140 additions & 0 deletions cardano-testnet/test/cardano-testnet-test/Test/Cli/QuerySlotNumber.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Redundant id" #-}

module Test.Cli.QuerySlotNumber
( hprop_querySlotNumber
) where

import Control.Monad
import Data.Either
import Data.Monoid (Last (..))
import qualified Data.Time.Clock as DT
import qualified Data.Time.Format as DT
import Hedgehog (Property)
import qualified Hedgehog as H
import qualified Hedgehog.Extras.Stock as H
import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as IO
import qualified Hedgehog.Extras.Test.Base as H
import qualified Hedgehog.Extras.Test.Process as H
import qualified Hedgehog.Internal.Property as H
import Prelude
import qualified System.Directory as IO
import System.Environment (getEnvironment)
import System.FilePath ((</>))
import qualified System.Info as SYS

import Cardano.Testnet
import qualified Testnet.Util.Base as H
import Testnet.Util.Process
import Testnet.Util.Runtime

-- | Tests @query slot-number@ cardano-cli command that it returns correct slot numbers for provided utc time
hprop_querySlotNumber :: Property
hprop_querySlotNumber = H.integrationRetryWorkspace 2 "query-slot-number" $ \tempAbsBasePath' -> do
H.note_ SYS.os
base <- H.note =<< H.noteIO . IO.canonicalizePath =<< H.getProjectBase
configurationTemplate <- H.noteShow $ base </> "configuration/defaults/byron-mainnet/configuration.yaml"
conf@Conf { tempBaseAbsPath } <- H.noteShowM $
mkConf (ProjectBase base) (Just $ YamlFilePath configurationTemplate) tempAbsBasePath' Nothing

let
testnetOptions = BabbageOnlyTestnetOptions $ babbageDefaultTestnetOptions
{ babbageNodeLoggingFormat = NodeLoggingFormatAsJson
}
tr@TestnetRuntime
{ testnetMagic
, poolNodes
} <- testnet testnetOptions conf
ShelleyGenesis{sgSlotLength, sgEpochLength} <- H.noteShowM $ shelleyGenesis tr
startTime <- H.noteShowM $ getStartTime tempAbsBasePath' tr

let slotLength = fromNominalDiffTimeMicro sgSlotLength
-- how many slots can the checked value differ from
-- we have 1s precision for UTC timestamp CLI argument, so this value tells how many slots in 1s can be
slotPrecision = round $ 1 / slotLength
epochSize = fromIntegral sgEpochLength :: Int

poolNode1 <- H.headM poolNodes
poolSprocket1 <- H.noteShow $ nodeSprocket $ poolRuntime poolNode1
env <- H.evalIO getEnvironment
execConfig <- H.noteShow H.ExecConfig
{ H.execConfigEnv = Last $ Just $
[ ("CARDANO_NODE_SOCKET_PATH", IO.sprocketArgumentName poolSprocket1)
]
-- The environment must be passed onto child process on Windows in order to
-- successfully start that process.
<> env
, H.execConfigCwd = Last $ Just tempBaseAbsPath
}

id do
H.note_ "Try to retrieve slot 5s before genesis"
testTime <- H.note . formatTime $ (-5) `DT.addUTCTime` startTime
(result, _) <- H.runTestT $ execCli' execConfig
[ "query", "slot-number"
, "--testnet-magic", show @Int testnetMagic
, testTime
]
H.assertWith result isLeft

id do
H.note_ "Retrieve slot number for the start time"
testTime <- H.note $ formatTime startTime
let expectedSlot = 0
slot <- H.readNoteM =<< execCli' execConfig
[ "query", "slot-number"
, "--testnet-magic", show @Int testnetMagic
, testTime
]
H.assertWithinTolerance slot expectedSlot slotPrecision

id do
H.note_ "Retrieve slot number for some delay"
let expectedSlot = 200
passedTime = fromIntegral expectedSlot * slotLength
testTime <- H.note . formatTime $ passedTime `DT.addUTCTime` startTime
slot <- H.readNoteM @Int =<< execCli' execConfig
[ "query", "slot-number"
, "--testnet-magic", show @Int testnetMagic
, testTime
]
H.assertWithinTolerance slot expectedSlot slotPrecision

id do
H.note_ "Retrieve slot number at the end of epoch"
-- that's the last slot we can look up
-- for detailed explanation about the horizon limit see:
-- https://github.com/input-output-hk/ouroboros-consensus/pull/62
let expectedSlot = epochSize - 1
passedTime = fromIntegral expectedSlot * slotLength
testTime <- H.note . formatTime $ passedTime `DT.addUTCTime` startTime
slot <- H.readNoteM @Int =<< execCli' execConfig
[ "query", "slot-number"
, "--testnet-magic", show @Int testnetMagic
, testTime
]
H.assertWithinTolerance slot expectedSlot slotPrecision

id do
H.note_ "Try to retrieve slot beyond the horizon"
let timeOffset = slotLength * fromIntegral epochSize * 2
testTime <- H.note . formatTime $ timeOffset `DT.addUTCTime` startTime
(result, _) <- H.runTestT $ execCli' execConfig
[ "query", "slot-number"
, "--testnet-magic", show @Int testnetMagic
, testTime
]
H.assertWith result isLeft

-- }}}

formatTime :: DT.UTCTime -> String
formatTime = DT.formatTime DT.defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ"

Loading

0 comments on commit 220e7dc

Please sign in to comment.