Merge pull request #2624 from input-output-hk/jc/pool-ranking-new-module
moved pool ranking functions to a new module
moved pool ranking functions to a new module
Cardano.Ledger.Shelley.LedgerState
Cardano.Ledger.Shelley.Metadata
Cardano.Ledger.Shelley.Orphans
Cardano.Ledger.Shelley.PoolRank
Cardano.Ledger.Shelley.PParams
Cardano.Ledger.Shelley.Rewards
Cardano.Ledger.Shelley.RewardProvenance
ProposedPPUpdates (..),
Update (..),
)
import Cardano.Ledger.Shelley.Rewards as X
import Cardano.Ledger.Shelley.PoolRank as X
( NonMyopic,
)
import Cardano.Ledger.Shelley.Rules.Deleg as X (DELEG, DelegEnv (..))
rewards,
)
import Cardano.Ledger.Shelley.PParams (PParams' (..))
import Cardano.Ledger.Shelley.RewardProvenance (RewardProvenance)
import Cardano.Ledger.Shelley.Rewards
import Cardano.Ledger.Shelley.PoolRank
( NonMyopic (..),
PerformanceEstimate (..),
StakeShare (..),
getTopRankedPoolsVMap,
nonMyopicMemberRew,
percentile',
)
import Cardano.Ledger.Shelley.RewardProvenance (RewardProvenance)
import Cardano.Ledger.Shelley.Rewards (StakeShare (..))
import Cardano.Ledger.Shelley.Rules.NewEpoch (calculatePoolDistr)
import Cardano.Ledger.Shelley.Tx (Tx (..), WitnessSet, WitnessSetHKD (..))
import Cardano.Ledger.Shelley.TxBody (DCert, PoolParams (..), WitVKey (..))
Update (..),
emptyPPPUpdates,
)
import Cardano.Ledger.Shelley.PoolRank
( Likelihood (..),
NonMyopic (..),
applyDecay,
leaderProbability,
likelihood,
)
import Cardano.Ledger.Shelley.RewardProvenance (RewardProvenance (..))
import qualified Cardano.Ledger.Shelley.RewardProvenance as RP
import Cardano.Ledger.Shelley.RewardUpdate
pulseOther,
)
import Cardano.Ledger.Shelley.Rewards
( Likelihood (..),
NonMyopic (..),
PoolRewardInfo (..),
( PoolRewardInfo (..),
Reward,
StakeShare (..),
aggregateRewards,
applyDecay,
filterRewards,
leaderProbability,
leaderRewardToGeneral,
likelihood,
mkPoolRewardInfo,
sumRewards,
)
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Cardano.Ledger.Shelley.PoolRank
( desirability,
PerformanceEstimate (..),
NonMyopic (..),
getTopRankedPools,
getTopRankedPoolsVMap,
nonMyopicStake,
nonMyopicMemberRew,
percentile',
Histogram (..),
LogWeight (..),
likelihood,
applyDecay,
Likelihood (..),
leaderProbability,
)
where
import Cardano.Binary
( FromCBOR (..),
ToCBOR (..),
decodeDouble,
encodeDouble,
encodeListLen,
)
import Cardano.Ledger.BaseTypes
( ActiveSlotCoeff,
BoundedRational (..),
NonNegativeInterval,
UnitInterval,
activeSlotVal,
)
import Cardano.Ledger.Coin (Coin (..), coinToRational)
import qualified Cardano.Ledger.Crypto as CC (Crypto)
import Cardano.Ledger.Keys (KeyHash, KeyRole (..))
import Cardano.Ledger.Serialization
( decodeRecordNamedT,
decodeSeq,
encodeFoldable,
)
import Cardano.Ledger.Shelley.EpochBoundary (maxPool)
import Cardano.Ledger.Shelley.Rewards (StakeShare (..), memberRew)
import Cardano.Ledger.Shelley.TxBody (PoolParams (..))
import Cardano.Slotting.Slot (EpochSize (..))
import Control.DeepSeq (NFData)
import Control.Monad.Trans
import qualified Data.Compact.VMap as VMap
import Data.Default.Class (Default, def)
import Data.Foldable (find)
import Data.Function (on)
import Data.List (sortBy)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Ratio ((%))
import qualified Data.Sequence as Seq
import Data.Sequence.Strict (StrictSeq)
import qualified Data.Sequence.Strict as StrictSeq
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Sharing
import GHC.Generics (Generic)
import GHC.Records (HasField (getField))
import Lens.Micro (_1)
import NoThunks.Class (NoThunks (..))
import Numeric.Natural (Natural)
import Quiet
newtype LogWeight = LogWeight {unLogWeight :: Float}
deriving (Eq, Generic, Ord, Num, NFData, NoThunks, ToCBOR, FromCBOR)
deriving (Show) via Quiet LogWeight
toLogWeight :: Double -> LogWeight
toLogWeight d = LogWeight (realToFrac $ log d)
fromLogWeight :: LogWeight -> Double
fromLogWeight (LogWeight l) = exp (realToFrac l)
newtype Histogram = Histogram {unHistogram :: StrictSeq LogWeight}
deriving (Eq, Show, Generic)
newtype Likelihood = Likelihood {unLikelihood :: StrictSeq LogWeight}
-- TODO: replace with small data structure
deriving (Show, Ord, Generic, NFData)
instance NoThunks Likelihood
instance Eq Likelihood where
(==) = (==) `on` unLikelihood . normalizeLikelihood
instance Semigroup Likelihood where
(Likelihood x) <> (Likelihood y) =
normalizeLikelihood $ Likelihood (StrictSeq.zipWith (+) x y)
instance Monoid Likelihood where
mempty = Likelihood $ StrictSeq.forceToStrict $ Seq.replicate (length samplePositions) (LogWeight 0)
normalizeLikelihood :: Likelihood -> Likelihood
normalizeLikelihood (Likelihood xs) = Likelihood $ (\x -> x - m) <$> xs
where
m = minimum xs
instance ToCBOR Likelihood where
toCBOR (Likelihood logweights) = encodeFoldable logweights
instance FromCBOR Likelihood where
fromCBOR = Likelihood . StrictSeq.forceToStrict <$> decodeSeq fromCBOR
leaderProbability :: ActiveSlotCoeff -> Rational -> UnitInterval -> Double
leaderProbability activeSlotCoeff relativeStake decentralizationParameter =
(1 - (1 - asc) ** s) * (1 - d')
where
d' = realToFrac . unboundRational $ decentralizationParameter
asc = realToFrac . unboundRational . activeSlotVal $ activeSlotCoeff
s = realToFrac relativeStake
samplePositions :: StrictSeq Double
samplePositions = (\x -> (x + 0.5) / 100.0) <$> StrictSeq.fromList [0.0 .. 99.0]
likelihood ::
Natural -> -- number of blocks produced this epoch
Double -> -- chance we're allowed to produce a block in this slot
EpochSize ->
Likelihood
likelihood blocks t slotsPerEpoch =
Likelihood $
sample <$> samplePositions
where
-- The likelihood function L(x) is the probability of observing the data we got
-- under the assumption that the underlying pool performance is equal to x.
-- L(x) = C(n,m) * (tx)^n * (1-tx)^m
-- where
-- t is the chance we're allowed to produce a block
-- n is the number of slots in which a block was produced
-- m is the number of slots in which a block was not produced
-- (slots per epoch minus n)
-- C(n,m) is a coefficient that will be irrelevant
-- Since the likelihood function only matters up to a scalar multiple, we will
-- will divide out C(n,m) t^n and use the following instead:
-- L(x) = x^n * (1-tx)^m
-- We represent this function using 100 sample points, but to avoid very
-- large exponents, we store the log of the value instead of the value itself.
-- log(L(x)) = log [ x^n * (1-tx)^m ]
-- = n * log(x) + m * log(1 - tx)
-- TODO: worry more about loss of floating point precision
--
-- example:
-- a pool has relative stake of 1 / 1,000,000 (~ 30k ada of 35b ada)
-- f = active slot coefficient = 1/20
-- t = 1 - (1-f)^(1/1,000,000)
n = fromIntegral blocks
m = fromIntegral $ slotsPerEpoch - fromIntegral blocks
l :: Double -> Double
l x = n * log x + m * log (1 - t * x)
sample position = LogWeight (realToFrac $ l position)
-- | Decay previous likelihood
applyDecay :: Float -> Likelihood -> Likelihood
applyDecay decay (Likelihood logWeights) = Likelihood $ mul decay <$> logWeights
where
mul x (LogWeight f) = LogWeight (x * f)
posteriorDistribution :: Histogram -> Likelihood -> Histogram
posteriorDistribution (Histogram points) (Likelihood likelihoods) =
normalize $
Histogram $ StrictSeq.zipWith (+) points likelihoods
-- | Normalize the histogram so that the total area is 1
normalize :: Histogram -> Histogram
normalize (Histogram values) = Histogram $ (\x -> x - logArea) <$> values'
where
m = maximum values
values' = (\x -> x - m) <$> values
logArea = toLogWeight area
area = reimannSum 0.01 (fromLogWeight <$> values')
-- | Calculate the k percentile for this distribution.
-- k is a value between 0 and 1. The 0 percentile is 0 and the 1 percentile is 1
percentile :: Double -> Histogram -> Likelihood -> PerformanceEstimate
percentile p prior likelihoods =
PerformanceEstimate . fst $
fromMaybe (1, 1) $
find (\(_x, fx) -> fx > p) cdf
where
(Histogram values) = posteriorDistribution prior likelihoods
cdf =
import qualified Cardano.Ledger.Crypto as CC (Crypto)
import Cardano.Ledger.Keys (KeyHash, KeyRole (..))
import Cardano.Ledger.Serialization (decodeRecordNamed)
import Cardano.Ledger.Shelley.PoolRank (Likelihood, NonMyopic)
import Cardano.Ledger.Shelley.RewardProvenance (RewardProvenancePool (..))
import qualified Cardano.Ledger.Shelley.RewardProvenance as RP
import Cardano.Ledger.Shelley.Rewards
( Likelihood,
NonMyopic,
PoolRewardInfo (..),
( PoolRewardInfo (..),
Reward (..),
RewardType (..),
rewardOnePoolMember,
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Cardano.Ledger.Shelley.Rewards
( desirability,
PerformanceEstimate (..),
NonMyopic (..),
getTopRankedPools,
getTopRankedPoolsVMap,
StakeShare (..),
( StakeShare (..),
PoolRewardInfo (..),
mkApparentPerformance,
RewardType (..),
LeaderOnlyReward (..),
leaderRewardToGeneral,
Reward (..),
nonMyopicStake,
nonMyopicMemberRew,
percentile',
Histogram (..),
LogWeight (..),
likelihood,
applyDecay,
Likelihood (..),
leaderProbability,
leaderRew,
memberRew,
aggregateRewards,
import Cardano.Binary
( FromCBOR (..),
ToCBOR (..),
decodeDouble,
decodeWord,
encodeDouble,
encodeListLen,
encodeWord,
)
import Cardano.Ledger.BaseTypes
( ActiveSlotCoeff,
BlocksMade (..),
( BlocksMade (..),
BoundedRational (..),
NonNegativeInterval,
ProtVer,
UnitInterval,
activeSlotVal,
invalidKey,
)
import Cardano.Ledger.Coin
import qualified Cardano.Ledger.Crypto as CC (Crypto)
import Cardano.Ledger.Era (Crypto)
import Cardano.Ledger.Keys (KeyHash, KeyRole (..))
import Cardano.Ledger.Serialization
( decodeRecordNamedT,
decodeSeq,
encodeFoldable,
)
import Cardano.Ledger.Shelley.Delegation.PoolParams (poolSpec)
import Cardano.Ledger.Shelley.EpochBoundary
( Stake (..),
maxPool,
maxPool',
)
import Cardano.Ledger.Shelley.EpochBoundary (Stake (..), maxPool')
import qualified Cardano.Ledger.Shelley.HardForks as HardForks
import Cardano.Ledger.Shelley.TxBody (PoolParams (..))
import Cardano.Ledger.Val ((<->))
import Cardano.Slotting.Slot (EpochSize (..))
import Control.DeepSeq (NFData)
import Control.Monad (guard)
import Control.Monad.Trans
import Data.Coders (Decode (..), Encode (..), decode, encode, (!>), (<!))
import qualified Data.Compact.VMap as VMap
import Data.Default.Class (Default, def)
import Data.Foldable (find, fold, foldMap')
import Data.Function (on)
import Data.List (sortBy)
import Data.Foldable (fold, foldMap')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Ratio ((%))
import qualified Data.Sequence as Seq
import Data.Sequence.Strict (StrictSeq)
import qualified Data.Sequence.Strict as StrictSeq
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Sharing
import GHC.Generics (Generic)
import GHC.Records (HasField (getField))
import Lens.Micro (_1)
import NoThunks.Class (NoThunks (..))
import Numeric.Natural (Natural)
import Quiet
newtype LogWeight = LogWeight {unLogWeight :: Float}
deriving (Eq, Generic, Ord, Num, NFData, NoThunks, ToCBOR, FromCBOR)
deriving (Show) via Quiet LogWeight
toLogWeight :: Double -> LogWeight
toLogWeight d = LogWeight (realToFrac $ log d)
fromLogWeight :: LogWeight -> Double
fromLogWeight (LogWeight l) = exp (realToFrac l)
newtype Histogram = Histogram {unHistogram :: StrictSeq LogWeight}
deriving (Eq, Show, Generic)
newtype Likelihood = Likelihood {unLikelihood :: StrictSeq LogWeight}
-- TODO: replace with small data structure
deriving (Show, Ord, Generic, NFData)
instance NoThunks Likelihood
instance Eq Likelihood where
(==) = (==) `on` unLikelihood . normalizeLikelihood
instance Semigroup Likelihood where
(Likelihood x) <> (Likelihood y) =
normalizeLikelihood $ Likelihood (StrictSeq.zipWith (+) x y)
instance Monoid Likelihood where
mempty = Likelihood $ StrictSeq.forceToStrict $ Seq.replicate (length samplePositions) (LogWeight 0)
normalizeLikelihood :: Likelihood -> Likelihood
normalizeLikelihood (Likelihood xs) = Likelihood $ (\x -> x - m) <$> xs
where
m = minimum xs
instance ToCBOR Likelihood where
toCBOR (Likelihood logweights) = encodeFoldable logweights
instance FromCBOR Likelihood where
fromCBOR = Likelihood . StrictSeq.forceToStrict <$> decodeSeq fromCBOR
leaderProbability :: ActiveSlotCoeff -> Rational -> UnitInterval -> Double
leaderProbability activeSlotCoeff relativeStake decentralizationParameter =
(1 - (1 - asc) ** s) * (1 - d')
where
d' = realToFrac . unboundRational $ decentralizationParameter
asc = realToFrac . unboundRational . activeSlotVal $ activeSlotCoeff
s = realToFrac relativeStake
samplePositions :: StrictSeq Double
samplePositions = (\x -> (x + 0.5) / 100.0) <$> StrictSeq.fromList [0.0 .. 99.0]
likelihood ::
Natural -> -- number of blocks produced this epoch
Double -> -- chance we're allowed to produce a block in this slot
EpochSize ->
Likelihood
likelihood blocks t slotsPerEpoch =
Likelihood $
sample <$> samplePositions
where
-- The likelihood function L(x) is the probability of observing the data we got
-- under the assumption that the underlying pool performance is equal to x.
-- L(x) = C(n,m) * (tx)^n * (1-tx)^m
-- where
-- t is the chance we're allowed to produce a block
-- n is the number of slots in which a block was produced
-- m is the number of slots in which a block was not produced
-- (slots per epoch minus n)
-- C(n,m) is a coefficient that will be irrelevant
-- Since the likelihood function only matters up to a scalar multiple, we will
-- will divide out C(n,m) t^n and use the following instead:
-- L(x) = x^n * (1-tx)^m
-- We represent this function using 100 sample points, but to avoid very
-- large exponents, we store the log of the value instead of the value itself.
-- log(L(x)) = log [ x^n * (1-tx)^m ]
-- = n * log(x) + m * log(1 - tx)
-- TODO: worry more about loss of floating point precision
--
-- example:
-- a pool has relative stake of 1 / 1,000,000 (~ 30k ada of 35b ada)
-- f = active slot coefficient = 1/20
-- t = 1 - (1-f)^(1/1,000,000)
n = fromIntegral blocks
m = fromIntegral $ slotsPerEpoch - fromIntegral blocks
l :: Double -> Double
l x = n * log x + m * log (1 - t * x)
sample position = LogWeight (realToFrac $ l position)
-- | Decay previous likelihood
updateStakeDistribution,
)
import Cardano.Ledger.Shelley.PParams (PParams' (..))
import Cardano.Ledger.Shelley.Rewards (likelihood)
import Cardano.Ledger.Shelley.PoolRank (likelihood)
import Cardano.Ledger.Shelley.UTxO (UTxO)
import Cardano.Protocol.TPraos.API (PraosCrypto)
import Cardano.Slotting.Slot (EpochSize (..))
)
import Cardano.Ledger.Shelley.LedgerState (FutureGenDeleg)
import qualified Cardano.Ledger.Shelley.Metadata as MD
import Cardano.Ledger.Shelley.PoolRank
( Likelihood (..),
LogWeight (..),
PerformanceEstimate (..),
)
import Cardano.Ledger.Shelley.RewardProvenance
( Desirability (..),
RewardProvenance (..),
)
import Cardano.Ledger.Shelley.Rewards
( LeaderOnlyReward (..),
Likelihood (..),
LogWeight (..),
PerformanceEstimate (..),
PoolRewardInfo (..),
Reward (..),
RewardType (..),
startStep,
)
import Cardano.Ledger.Shelley.PParams (PParams' (..))
import qualified Cardano.Ledger.Shelley.RewardProvenance as RP
import Cardano.Ledger.Shelley.Rewards
import Cardano.Ledger.Shelley.PoolRank
( Likelihood (..),
NonMyopic (..),
Reward (..),
RewardType (..),
applyDecay,
leaderProbability,
likelihood,
)
import qualified Cardano.Ledger.Shelley.RewardProvenance as RP
import Cardano.Ledger.Shelley.Rewards (Reward (..), RewardType (..))
import Cardano.Ledger.Shelley.Tx
( Tx (..),
WitnessSetHKD (..),
( PParams,
PParams' (..),
)
import Cardano.Ledger.Shelley.Rewards
import Cardano.Ledger.Shelley.PoolRank
( Likelihood (..),
NonMyopic (..),
Reward (..),
leaderProbability,
likelihood,
)
import Cardano.Ledger.Shelley.Rewards
( Reward (..),
RewardType (..),
StakeShare (..),
aggregateRewards,
leaderProbability,
leaderRew,
likelihood,
memberRew,
mkApparentPerformance,
sumRewards,
PParams' (..),
emptyPParams,
)
import Cardano.Ledger.Shelley.PoolRank
( Likelihood,
NonMyopic,
leaderProbability,
likelihood,
)
import Cardano.Ledger.Shelley.RewardUpdate
( FreeVars (..),
KeyHashPoolProvenance,
RewardPulser (RSLP),
)
import Cardano.Ledger.Shelley.Rewards
( Likelihood,
NonMyopic,
Reward (rewardAmount),
( Reward (rewardAmount),
StakeShare (..),
aggregateRewards,
leaderProbability,
leaderRew,
likelihood,
memberRew,
mkApparentPerformance,
mkPoolRewardInfo,
ProposedPPUpdates (..),
Update (..),
)
import Cardano.Ledger.Shelley.PoolRank
( Histogram (..),
Likelihood (..),
LogWeight (..),
NonMyopic (..),
PerformanceEstimate (..),
)
import Cardano.Ledger.Shelley.RewardUpdate
( FreeVars (..),
Pulser,
RewardUpdate (..),
)
import Cardano.Ledger.Shelley.Rewards
( Histogram (..),
LeaderOnlyReward (..),
Likelihood (..),
LogWeight (..),
NonMyopic (..),
PerformanceEstimate (..),
( LeaderOnlyReward (..),
PoolRewardInfo (..),
Reward (..),
RewardType (..),
import Cardano.Ledger.Keys
import Cardano.Ledger.SafeHash
import Cardano.Ledger.Shelley.LedgerState
import Cardano.Ledger.Shelley.Rewards
import Cardano.Ledger.Shelley.PoolRank
import Cardano.Ledger.Shelley.TxBody (PoolParams (..))
import Cardano.Ledger.State.UTxO
import Cardano.Ledger.TxIn
import qualified Cardano.Ledger.Credential as Credential
import qualified Cardano.Ledger.Keys as Keys
import qualified Cardano.Ledger.Shelley.LedgerState as Shelley
import qualified Cardano.Ledger.Shelley.Rewards as Shelley
import qualified Cardano.Ledger.Shelley.PoolRank as Shelley
import qualified Cardano.Ledger.Shelley.TxBody as Shelley
import Cardano.Ledger.State.Orphans (Enc, SnapShotType (..))
import Cardano.Ledger.State.UTxO
import Cardano.Ledger.PoolDistr (individualPoolStakeVrf)
import Cardano.Ledger.Shelley.API
import Cardano.Ledger.Shelley.LedgerState
import Cardano.Ledger.Shelley.Rewards
import Cardano.Ledger.Shelley.PoolRank
import Conduit
import Control.Exception (throwIO)
import Control.Foldl (Fold (..))
3852: cardano-tracer: init RTView r=deepfire a=denisshevchenko This is pre-MVP for RTView. 3880: Old peers tracing was erroneously called in new tracing r=deepfire a=jutaro /nix/store/qaplqccmisqy8n7ai65nssafzkxyyc7p-cabal-install-exe-cabal-3.6.2.0/bin/cabal --project-file=/home/deepfire/cardano-node/.nix-shell-cabal.project run exe:cardano-node -- +RTS -sghc-rts-report.txt -RTS run --config config.json --database-path run/current/node-0/db-testnet --topology topology.json --host-addr 127.0.0.1 --port 30000 --socket-path node.socket +RTS -N2 -I0 -A16m -qg -qb --disable-delayed-os-memory-return -RTS cardano-node: ExceptionInLinkedThread (ThreadId 11) The name ""peersFromNodeKernel"" is already taken by a metric. CallStack (from HasCallStack): error, called at ./System/Metrics.hs:214:5 in ekg-core-0.1.1.7-FjoslY1tzknIAl90c73kOZ:System.Metrics 3882: Fix datum in tx and ref scripts r=Jimbo4350 a=ch1bo (Couldn't reopen https://github.com/input-output-hk/cardano-node/pull/3881, so created this one) :snowflake: Add a roundtrip property `TxBodyContent -> TxBody -> TxBodyContent` This helped in fixing the :bug: and uncover the two additional gaps in the code. I'm not 100% happy with the current implementation of the property though! I needed to accept two exceptions to the general `===`: 1. `SimpleScriptV1` reference scripts may become `SimpleScriptV2` 2. A `TxOutDatumHash` + a matching `ScriptData` may become a `TxOutDatumTx` :snowflake: Resolve datum hash + matching datum in transaction to `TxOutDatumInTx`, fixes #3866 :snowflake: Add missing script languages to `scriptLanguageSupportedInEra` for `BabbageEra` :snowflake: Allow scripts in any language as refeference scripts Co-authored-by: Denis Shevchenko <[email protected]iohk.io> Co-authored-by: Kosyrev Serge <[email protected]> Co-authored-by: Yupanqui <[email protected]> Co-authored-by: Sebastian Nagel <[email protected]>
- you're no exception, patroni!