Skip to content

Commit ba6003d

Browse files
authored
Merge pull request #755 from IntersectMBO/fix-estimate-transaction-key-witness-count
Modify `estimateTransactionKeyWitnessCount` to estimate simple scripts too
2 parents a1ba5b5 + 3305710 commit ba6003d

File tree

3 files changed

+96
-6
lines changed
  • cardano-api

3 files changed

+96
-6
lines changed

cardano-api/gen/Test/Gen/Cardano/Api/Typed.hs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ module Test.Gen.Cardano.Api.Typed
132132
, genProposals
133133
, genProposal
134134
, genVotingProcedures
135+
, genSimpleScriptWithoutEmptyAnys
135136
)
136137
where
137138

@@ -233,8 +234,23 @@ genScript SimpleScriptLanguage =
233234
genScript (PlutusScriptLanguage lang) =
234235
PlutusScript lang <$> genPlutusScript lang
235236

237+
genSimpleScriptWithoutEmptyAnys :: Gen SimpleScript
238+
genSimpleScriptWithoutEmptyAnys = genRandomSimpleScript False
239+
236240
genSimpleScript :: Gen SimpleScript
237-
genSimpleScript =
241+
genSimpleScript = genRandomSimpleScript True
242+
243+
-- | We include a @hasEmptyAnys@ parameter to control whether we allow empty
244+
-- 'RequireAnyOf' constructors. This is because an empty 'RequireAnyOf',
245+
-- same as a 'RequireMOf' with less than M elements, is not satisfiable.
246+
-- In the function @satisfyScript@ in the "Test.Cardano.Api.TxBody" module,
247+
-- we look for a set of witnesses that satisfy a script, and we can't do it
248+
-- if the script consists of an empty 'RequireAnyOf' constructor.
249+
-- Note that this is not the only way to make an unsatisfiable script,
250+
-- but this is the one that affects the @satisfyScript@ function, because
251+
-- it is only concerned with the witnesses, and not with the times.
252+
genRandomSimpleScript :: Bool -> Gen SimpleScript
253+
genRandomSimpleScript hasEmptyAnys =
238254
genTerm
239255
where
240256
genTerm = Gen.recursive Gen.choice nonRecursive recursive
@@ -249,7 +265,7 @@ genSimpleScript =
249265
-- Recursive generators
250266
recursive =
251267
[ RequireAllOf <$> Gen.list (Range.linear 0 10) genTerm
252-
, RequireAnyOf <$> Gen.list (Range.linear 0 10) genTerm
268+
, RequireAnyOf <$> Gen.list (Range.linear (if hasEmptyAnys then 0 else 1) 10) genTerm
253269
, do
254270
ts <- Gen.list (Range.linear 0 10) genTerm
255271
m <- Gen.integral (Range.constant 0 (length ts))

cardano-api/src/Cardano/Api/Internal/Fees.hs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,20 @@ import Cardano.Ledger.Val qualified as L
8686
import Ouroboros.Consensus.HardFork.History qualified as Consensus
8787

8888
import Control.Monad
89-
import Data.Bifunctor (bimap, first, second)
89+
import Data.Bifunctor
90+
( bimap
91+
, first
92+
, second
93+
)
9094
import Data.ByteString.Short (ShortByteString)
9195
import Data.Function ((&))
96+
import Data.List (sortBy)
9297
import Data.List qualified as List
9398
import Data.Map.Strict (Map)
9499
import Data.Map.Strict qualified as Map
95100
import Data.Maybe
96101
import Data.OSet.Strict qualified as OSet
102+
import Data.Ord (Down (Down), comparing)
97103
import Data.Ratio
98104
import Data.Set (Set)
99105
import Data.Set qualified as Set
@@ -464,7 +470,7 @@ estimateTransactionKeyWitnessCount
464470
, txUpdateProposal
465471
} =
466472
fromIntegral $
467-
length [() | (_txin, BuildTxWith KeyWitness{}) <- txIns]
473+
sum (map estimateTxInWitnesses txIns)
468474
+ case txInsCollateral of
469475
TxInsCollateral _ txins ->
470476
length txins
@@ -486,6 +492,27 @@ estimateTransactionKeyWitnessCount
486492
TxUpdateProposal _ (UpdateProposal updatePerGenesisKey _) ->
487493
Map.size updatePerGenesisKey
488494
_ -> 0
495+
where
496+
estimateTxInWitnesses :: (TxIn, BuildTxWith BuildTx (Witness WitCtxTxIn era)) -> Int
497+
estimateTxInWitnesses (_, BuildTxWith (KeyWitness _)) = 1
498+
estimateTxInWitnesses (_, BuildTxWith (ScriptWitness _ (SimpleScriptWitness _ (SScript simpleScript)))) = maxWitnessesInSimpleScript simpleScript
499+
estimateTxInWitnesses (_, BuildTxWith (ScriptWitness _ (SimpleScriptWitness _ (SReferenceScript _)))) = 0
500+
estimateTxInWitnesses (_, BuildTxWith (ScriptWitness _ (PlutusScriptWitness{}))) = 0
501+
502+
-- This is a rough conservative estimate of the maximum number of witnesses
503+
-- needed for a simple script to be satisfied. It is conservative because it
504+
-- assumes that each key hash only appears once, and it assumes the worst
505+
-- scenario. A more accurate estimate for the maximum could be computed by
506+
-- keeping track of the possible combinations of key hashes that have
507+
-- potentially already been counted, but that would increase complexity a lot,
508+
-- and it would still be a conservative estimate.
509+
maxWitnessesInSimpleScript :: SimpleScript -> Int
510+
maxWitnessesInSimpleScript (RequireSignature _) = 1
511+
maxWitnessesInSimpleScript (RequireTimeBefore _) = 0
512+
maxWitnessesInSimpleScript (RequireTimeAfter _) = 0
513+
maxWitnessesInSimpleScript (RequireAllOf simpleScripts) = sum $ map maxWitnessesInSimpleScript simpleScripts
514+
maxWitnessesInSimpleScript (RequireAnyOf simpleScripts) = maximum $ map maxWitnessesInSimpleScript simpleScripts
515+
maxWitnessesInSimpleScript (RequireMOf n simpleScripts) = sum $ take n $ sortBy (comparing Down) (map maxWitnessesInSimpleScript simpleScripts)
489516

490517
-- ----------------------------------------------------------------------------
491518
-- Script execution units
@@ -841,6 +868,7 @@ data TxBodyErrorAutoBalance era
841868
-- input ada.
842869
TxBodyErrorAdaBalanceTooSmall
843870
-- \^ Offending TxOut
871+
844872
TxOutInAnyEra
845873
-- ^ Minimum UTxO
846874
L.Coin
@@ -857,6 +885,7 @@ data TxBodyErrorAutoBalance era
857885
| -- | The minimum spendable UTxO threshold has not been met.
858886
TxBodyErrorMinUTxONotMet
859887
-- \^ Offending TxOut
888+
860889
TxOutInAnyEra
861890
-- ^ Minimum UTxO
862891
L.Coin

cardano-api/test/cardano-api-test/Test/Cardano/Api/TxBody.hs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
{-# LANGUAGE OverloadedLists #-}
55
{-# LANGUAGE RankNTypes #-}
66
{-# LANGUAGE ScopedTypeVariables #-}
7+
{-# LANGUAGE TupleSections #-}
78

89
{- HLINT ignore "Use camelCase" -}
910

@@ -18,15 +19,24 @@ import Cardano.Api.Ledger qualified as L
1819
import Cardano.Api.Shelley (ShelleyLedgerEra)
1920

2021
import Data.Maybe (isJust)
21-
import Data.Type.Equality (TestEquality (testEquality))
22+
import Data.Set (Set)
23+
import Data.Set qualified as Set
24+
import Data.Type.Equality
25+
( TestEquality (testEquality)
26+
)
2227
import GHC.Exts (IsList (..))
2328

2429
import Test.Gen.Cardano.Api.Typed
2530

2631
import Test.Cardano.Api.Orphans ()
2732

28-
import Hedgehog (MonadTest, Property, (===))
33+
import Hedgehog
34+
( MonadTest
35+
, Property
36+
, (===)
37+
)
2938
import Hedgehog qualified as H
39+
import Hedgehog.Gen (shuffle)
3040
import Test.Tasty (TestTree, testGroup)
3141
import Test.Tasty.Hedgehog (testProperty)
3242

@@ -108,6 +118,38 @@ prop_roundtrip_txbodycontent_conway_fields = H.property $ do
108118
getProposalProcedures TxProposalProceduresNone = Nothing
109119
getProposalProcedures (TxProposalProcedures pp) = Just $ fst <$> toList pp
110120

121+
prop_simple_script_witness_count :: Property
122+
prop_simple_script_witness_count = H.property $ do
123+
let sbe = ShelleyBasedEraConway
124+
(_, contentWithoutScript) <- H.forAll $ genValidTxBody sbe
125+
script <- H.forAll genSimpleScriptWithoutEmptyAnys
126+
newTxIn <-
127+
H.forAll $
128+
(,BuildTxWith
129+
( ScriptWitness
130+
ScriptWitnessForSpending
131+
(SimpleScriptWitness SimpleScriptInConway (SScript script))
132+
))
133+
<$> genTxIn
134+
witList <- H.forAll $ satisfyScript script
135+
let witCount = fromIntegral $ Set.size witList
136+
-- We use the inequality @<=@ instead of @==@ because 'estimateTransactionKeyWitnessCount'
137+
-- calculates an upper bound on the number of key witnesses required to validate a transaction,
138+
-- and the @witList@ contains a random subset that can potentially be used to satisfy the script.
139+
-- So we only know it must be smaller or equal to the upper bound.
140+
H.diff
141+
(estimateTransactionKeyWitnessCount contentWithoutScript + witCount)
142+
(<=)
143+
(estimateTransactionKeyWitnessCount (addTxIn newTxIn contentWithoutScript))
144+
where
145+
satisfyScript :: SimpleScript -> H.Gen (Set (Hash PaymentKey))
146+
satisfyScript (RequireSignature paymentKeyHash) = return $ Set.singleton paymentKeyHash
147+
satisfyScript (RequireTimeBefore _) = return mempty
148+
satisfyScript (RequireTimeAfter _) = return mempty
149+
satisfyScript (RequireAllOf simpleScripts) = Set.unions <$> traverse satisfyScript simpleScripts
150+
satisfyScript (RequireMOf n simpleScripts) = shuffle simpleScripts >>= satisfyScript . RequireAllOf . take n
151+
satisfyScript (RequireAnyOf simpleScripts) = satisfyScript (RequireMOf 1 simpleScripts)
152+
111153
tests :: TestTree
112154
tests =
113155
testGroup
@@ -119,4 +161,7 @@ tests =
119161
, testProperty
120162
"roundtrip txbodycontent new conway fields"
121163
prop_roundtrip_txbodycontent_conway_fields
164+
, testProperty
165+
"simple script witness count"
166+
prop_simple_script_witness_count
122167
]

0 commit comments

Comments
 (0)