-
Notifications
You must be signed in to change notification settings - Fork 210
/
BalanceSpec.hs
3732 lines (3362 loc) · 134 KB
/
BalanceSpec.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{- HLINT ignore "Use camelCase" -}
module Cardano.Wallet.Primitive.CoinSelection.BalanceSpec
( spec
) where
import Prelude
import Algebra.PartialOrd
( PartialOrd (..) )
import Cardano.Numeric.Util
( inAscendingPartialOrder )
import Cardano.Wallet.Primitive.CoinSelection.Balance
( AssetCount (..)
, BalanceInsufficientError (..)
, InsufficientMinCoinValueError (..)
, MakeChangeCriteria (..)
, RunSelectionParams (..)
, SelectionCriteria (..)
, SelectionDelta (..)
, SelectionError (..)
, SelectionInsufficientError (..)
, SelectionLens (..)
, SelectionLimit
, SelectionLimitOf (..)
, SelectionResult (..)
, SelectionSkeleton (..)
, SelectionState (..)
, UnableToConstructChangeError (..)
, addMintValueToChangeMaps
, addMintValuesToChangeMaps
, assetSelectionLens
, assignCoinsToChangeMaps
, balanceMissing
, coinSelectionLens
, collateNonUserSpecifiedAssetQuantities
, computeUTxOBalanceAvailable
, computeUTxOBalanceRequired
, computeUTxOBalanceSufficiencyInfo
, groupByKey
, isUTxOBalanceSufficient
, makeChange
, makeChangeForCoin
, makeChangeForNonUserSpecifiedAsset
, makeChangeForNonUserSpecifiedAssets
, makeChangeForUserSpecifiedAsset
, mapMaybe
, performSelection
, prepareOutputsWith
, reduceTokenQuantities
, removeBurnValueFromChangeMaps
, removeBurnValuesFromChangeMaps
, runRoundRobin
, runSelection
, runSelectionNonEmptyWith
, runSelectionStep
, selectionDeltaAllAssets
, selectionHasValidSurplus
, splitBundleIfAssetCountExcessive
, splitBundlesWithExcessiveAssetCounts
, splitBundlesWithExcessiveTokenQuantities
, ungroupByKey
)
import Cardano.Wallet.Primitive.CoinSelection.Gen
( genSelectionLimit
, genSelectionState
, shrinkSelectionLimit
, shrinkSelectionState
)
import Cardano.Wallet.Primitive.Types.Address
( Address (..) )
import Cardano.Wallet.Primitive.Types.Coin
( Coin (..), addCoin )
import Cardano.Wallet.Primitive.Types.Coin.Gen
( genCoin, genCoinPositive, shrinkCoinPositive )
import Cardano.Wallet.Primitive.Types.Hash
( Hash (..) )
import Cardano.Wallet.Primitive.Types.TokenBundle
( Flat (..), TokenBundle (..) )
import Cardano.Wallet.Primitive.Types.TokenBundle.Gen
( genTokenBundleSmallRangePositive, shrinkTokenBundleSmallRangePositive )
import Cardano.Wallet.Primitive.Types.TokenMap
( AssetId (..), TokenMap )
import Cardano.Wallet.Primitive.Types.TokenMap.Gen
( genAssetId
, genAssetIdLargeRange
, genTokenMapSmallRange
, shrinkAssetId
, shrinkTokenMap
)
import Cardano.Wallet.Primitive.Types.TokenPolicy
( TokenName (..), TokenPolicyId (..) )
import Cardano.Wallet.Primitive.Types.TokenPolicy.Gen
( genTokenName )
import Cardano.Wallet.Primitive.Types.TokenQuantity
( TokenQuantity (..) )
import Cardano.Wallet.Primitive.Types.TokenQuantity.Gen
( genTokenQuantityPositive, shrinkTokenQuantityPositive )
import Cardano.Wallet.Primitive.Types.Tx
( TokenBundleSizeAssessment (..)
, TokenBundleSizeAssessor (..)
, TxIn (..)
, TxOut (..)
, txOutCoin
, txOutMaxTokenQuantity
)
import Cardano.Wallet.Primitive.Types.Tx.Gen
( genTxOut, shrinkTxOut )
import Cardano.Wallet.Primitive.Types.UTxO
( UTxO (..) )
import Cardano.Wallet.Primitive.Types.UTxOIndex
( SelectionFilter (..), UTxOIndex )
import Cardano.Wallet.Primitive.Types.UTxOIndex.Gen
( genUTxOIndex, genUTxOIndexLarge, genUTxOIndexLargeN, shrinkUTxOIndex )
import Control.Monad
( forM_, replicateM )
import Data.Bifunctor
( bimap, second )
import Data.ByteString
( ByteString )
import Data.Function
( (&) )
import Data.Functor.Identity
( Identity (..) )
import Data.Generics.Internal.VL.Lens
( over, set, view )
import Data.Generics.Labels
()
import Data.List.NonEmpty
( NonEmpty (..) )
import Data.Map.Strict
( Map )
import Data.Maybe
( fromMaybe, isJust, isNothing )
import Data.Semigroup
( mtimesDefault )
import Data.Set
( Set )
import Data.Tuple
( swap )
import Data.Word
( Word64, Word8 )
import Fmt
( blockListF, pretty )
import GHC.Generics
( Generic )
import Numeric.Natural
( Natural )
import Safe
( tailMay )
import Test.Hspec
( Expectation, Spec, SpecWith, describe, it, shouldBe )
import Test.Hspec.Core.QuickCheck
( modifyMaxSuccess )
import Test.Hspec.Extra
( parallel )
import Test.QuickCheck
( Arbitrary (..)
, Blind (..)
, Fun
, Gen
, Positive (..)
, Property
, applyFun
, arbitraryBoundedEnum
, checkCoverage
, choose
, conjoin
, counterexample
, cover
, disjoin
, forAll
, frequency
, generate
, genericShrink
, ioProperty
, label
, oneof
, property
, shrinkList
, sublistOf
, suchThat
, tabulate
, withMaxSuccess
, (.&&.)
, (===)
, (==>)
)
import Test.QuickCheck.Classes
( eqLaws, ordLaws )
import Test.QuickCheck.Monadic
( PropertyM (..), assert, monadicIO, monitor, run )
import Test.Utils.Laws
( testLawsMany )
import qualified Cardano.Wallet.Primitive.Types.TokenBundle as TokenBundle
import qualified Cardano.Wallet.Primitive.Types.TokenMap as TokenMap
import qualified Cardano.Wallet.Primitive.Types.TokenQuantity as TokenQuantity
import qualified Cardano.Wallet.Primitive.Types.UTxOIndex as UTxOIndex
import qualified Data.ByteString.Char8 as B8
import qualified Data.Foldable as F
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
spec :: Spec
spec = describe "Cardano.Wallet.Primitive.CoinSelection.BalanceSpec" $
modifyMaxSuccess (const 1000) $ do
parallel $ describe "Coverage" $ do
it "prop_Small_UTxOIndex_coverage" $
property prop_Small_UTxOIndex_coverage
it "prop_Large_UTxOIndex_coverage" $
property prop_Large_UTxOIndex_coverage
parallel $ describe "Class instances respect laws" $ do
testLawsMany @(AssetCount TokenMap)
[ eqLaws
, ordLaws
]
testLawsMany @SelectionLimit
[ eqLaws
, ordLaws
]
parallel $ describe "Ordering of token maps" $ do
it "prop_AssetCount_TokenMap_placesEmptyMapsFirst" $
property prop_AssetCount_TokenMap_placesEmptyMapsFirst
parallel $ describe "Preparing outputs" $ do
it "prop_prepareOutputWith_twice" $
property prop_prepareOutputsWith_twice
it "prop_prepareOutputsWith_length" $
property prop_prepareOutputsWith_length
it "prop_prepareOutputsWith_assetsUnchanged" $
property prop_prepareOutputsWith_assetsUnchanged
it "prop_prepareOutputsWith_preparedOrExistedBefore" $
property prop_prepareOutputsWith_preparedOrExistedBefore
parallel $ describe "Performing a selection" $ do
it "prop_performSelection_small" $
property prop_performSelection_small
it "prop_performSelection_large" $
property prop_performSelection_large
it "prop_performSelection_huge" $ ioProperty $ do
-- The UTxO index is generated outside of the property here to avoid
-- the cost of re-generating it on every pass. This would still
-- generate interesting cases since the selection within that large
-- index is random. Plus, other selection criteria still vary.
utxoAvailable <- generate (genUTxOIndexLargeN 50000)
pure $ property $ \minCoin costFor (Large criteria) ->
let
criteria' = Blind $
set #utxoAvailable utxoAvailable criteria
in
prop_performSelection minCoin costFor criteria' (const id)
& withMaxSuccess 5
parallel $ describe "Selection states" $ do
it "prop_genSelectionState_coverage" $
property prop_genSelectionState_coverage
it "prop_genSelectionState_valid" $
property prop_genSelectionState_valid
it "prop_shrinkSelectionState_valid" $
property prop_shrinkSelectionState_valid
parallel $ describe "Running a selection (without making change)" $ do
it "prop_runSelection_UTxO_empty" $
property prop_runSelection_UTxO_empty
it "prop_runSelection_UTxO_notEnough" $
property prop_runSelection_UTxO_notEnough
it "prop_runSelection_UTxO_exactlyEnough" $
property prop_runSelection_UTxO_exactlyEnough
it "prop_runSelection_UTxO_moreThanEnough" $
property prop_runSelection_UTxO_moreThanEnough
it "prop_runSelection_UTxO_muchMoreThanEnough" $
property prop_runSelection_UTxO_muchMoreThanEnough
parallel $ describe "Running a selection (non-empty)" $ do
it "prop_runSelectionNonEmpty" $
property prop_runSelectionNonEmpty
parallel $ describe "Running a selection step" $ do
it "prop_runSelectionStep_supplyExhausted" $
property prop_runSelectionStep_supplyExhausted
it "prop_runSelectionStep_notYetEnoughToSatisfyMinimum" $
property prop_runSelectionStep_notYetEnoughToSatisfyMinimum
it "prop_runSelectionStep_getsCloserToTargetButDoesNotExceedIt" $
property prop_runSelectionStep_getsCloserToTargetButDoesNotExceedIt
it "prop_runSelectionStep_getsCloserToTargetAndExceedsIt" $
property prop_runSelectionStep_getsCloserToTargetAndExceedsIt
it "prop_runSelectionStep_exceedsTargetAndGetsFurtherAway" $
property prop_runSelectionStep_exceedsTargetAndGetsFurtherAway
parallel $ describe "Behaviour of selection lenses" $ do
it "prop_assetSelectonLens_givesPriorityToSingletonAssets" $
property prop_assetSelectionLens_givesPriorityToSingletonAssets
it "prop_coinSelectonLens_givesPriorityToCoins" $
property prop_coinSelectionLens_givesPriorityToCoins
parallel $ describe "Boundary tests" $ do
unit_testBoundaries "Large token quantities"
boundaryTestMatrix_largeTokenQuantities
unit_testBoundaries "Large asset counts"
boundaryTestMatrix_largeAssetCounts
parallel $ describe "Making change" $ do
it "prop_makeChange_identity" $
property prop_makeChange_identity
it "prop_makeChange_length" $
property prop_makeChange_length
it "prop_makeChange" $
property prop_makeChange
unitTests "makeChange"
unit_makeChange
parallel $ describe "Collating non-user specified asset quantities" $ do
it "prop_collateNonUserSpecifiedAssetQuantities" $
property prop_collateNonUserSpecifiedAssetQuantities
describe "unit_collateNonUserSpecifiedAssetQuantities"
unit_collateNonUserSpecifiedAssetQuantities
parallel $ describe "assignCoinsToChangeMaps" $ do
unitTests "assignCoinsToChangeMaps"
unit_assignCoinsToChangeMaps
parallel $ describe "Making change for coins" $ do
it "prop_makeChangeForCoin_sum" $
property prop_makeChangeForCoin_sum
it "prop_makeChangeForCoin_length" $
property prop_makeChangeForCoin_length
unitTests "makeChangeForCoin"
unit_makeChangeForCoin
parallel $ describe "Making change for one non-user-specified asset" $ do
it "prop_makeChangeForNonUserSpecifiedAsset_sum" $
property prop_makeChangeForNonUserSpecifiedAsset_sum
it "prop_makeChangeForNonUserSpecifiedAsset_order" $
property prop_makeChangeForNonUserSpecifiedAsset_order
it "prop_makeChangeForNonUserSpecifiedAsset_length" $
property prop_makeChangeForNonUserSpecifiedAsset_length
unitTests "makeChangeForNonUserSpecifiedAsset"
unit_makeChangeForNonUserSpecifiedAsset
parallel $ describe "Making change for many non-user-specified assets" $ do
it "prop_makeChangeForNonUserSpecifiedAssets_length" $
property prop_makeChangeForNonUserSpecifiedAssets_length
it "prop_makeChangeForNonUserSpecifiedAssets_order" $
property prop_makeChangeForNonUserSpecifiedAssets_order
it "prop_makeChangeForNonUserSpecifiedAssets_sum" $
property prop_makeChangeForNonUserSpecifiedAssets_sum
describe "unit_makeChangeForNonUserSpecifiedAssets"
unit_makeChangeForNonUserSpecifiedAssets
parallel $ describe "Making change for user-specified assets" $ do
it "prop_makeChangeForUserSpecifiedAsset_sum" $
property prop_makeChangeForUserSpecifiedAsset_sum
it "prop_makeChangeForUserSpecifiedAsset_length" $
property prop_makeChangeForUserSpecifiedAsset_length
unitTests "makeChangeForUserSpecifiedAsset"
unit_makeChangeForUserSpecifiedAsset
parallel $ describe "Splitting bundles with excessive asset counts" $ do
it "prop_splitBundleIfAssetCountExcessive_length" $
property prop_splitBundleIfAssetCountExcessive_length
it "prop_splitBundleIfAssetCountExcessive_maximalSplitting" $
property prop_splitBundleIfAssetCountExcessive_maximalSplitting
it "prop_splitBundleIfAssetCountExcessive_postCondition" $
property prop_splitBundleIfAssetCountExcessive_postCondition
it "prop_splitBundleIfAssetCountExcessive_sum" $
property prop_splitBundleIfAssetCountExcessive_sum
it "prop_splitBundlesWithExcessiveAssetCounts_length" $
property prop_splitBundlesWithExcessiveAssetCounts_length
it "prop_splitBundlesWithExcessiveAssetCounts_sum" $
property prop_splitBundlesWithExcessiveAssetCounts_sum
parallel $ describe "Splitting bundles with excessive token quantities" $ do
it "prop_splitBundlesWithExcessiveTokenQuantities_length" $
property prop_splitBundlesWithExcessiveTokenQuantities_length
it "prop_splitBundlesWithExcessiveTokenQuantities_sum" $
property prop_splitBundlesWithExcessiveTokenQuantities_sum
parallel $ describe "Grouping and ungrouping" $ do
it "prop_groupByKey_ungroupByKey" $
property $ prop_groupByKey_ungroupByKey @Int @Int
it "prop_ungroupByKey_groupByKey" $
property $ prop_ungroupByKey_groupByKey @Int @Int
parallel $ describe "Round-robin processing" $ do
it "prop_runRoundRobin_identity" $
property $ prop_runRoundRobin_identity @Int
it "prop_runRoundRobin_iterationCount" $
property $ prop_runRoundRobin_iterationCount @TokenName @Word8
it "prop_runRoundRobin_iterationOrder" $
property $ prop_runRoundRobin_iterationOrder @TokenName @Word8
it "prop_runRoundRobin_generationCount" $
property $ prop_runRoundRobin_generationCount @TokenName @Word8
it "prop_runRoundRobin_generationOrder" $
property $ prop_runRoundRobin_generationOrder @TokenName @Word8
parallel $ describe "Utility functions" $ do
it "prop_mapMaybe_oracle" $
property prop_mapMaybe_oracle
parallel $ describe "Minting and burning values from the change maps" $ do
it "prop_addMintValueToChangeMaps_value" $
property prop_addMintValueToChangeMaps_value
it "prop_addMintValueToChangeMaps_length" $
property prop_addMintValueToChangeMaps_length
it "prop_addMintValueToChangeMaps_order" $
property prop_addMintValueToChangeMaps_order
it "prop_addMintValuesToChangeMaps" $
property prop_addMintValuesToChangeMaps
it "prop_removeBurnValueFromChangeMaps_value" $
property prop_removeBurnValueFromChangeMaps_value
it "prop_removeBurnValueFromChangeMaps_length" $
property prop_removeBurnValueFromChangeMaps_length
it "prop_removeBurnValueFromChangeMaps_order" $
property prop_removeBurnValueFromChangeMaps_order
it "prop_removeBurnValuesFromChangeMaps" $
property prop_removeBurnValuesFromChangeMaps
it "prop_reduceTokenQuantities_value" $
property prop_reduceTokenQuantities_value
it "prop_reduceTokenQuantities_length" $
property prop_reduceTokenQuantities_length
it "prop_reduceTokenQuantities_order" $
property prop_reduceTokenQuantities_order
--------------------------------------------------------------------------------
-- Coverage
--------------------------------------------------------------------------------
prop_Small_UTxOIndex_coverage :: Small UTxOIndex -> Property
prop_Small_UTxOIndex_coverage (Small index) =
checkCoverage $ property
-- Asset counts:
$ cover 1 (assetCount == 0)
"asset count = 0"
$ cover 80 (assetCount > 0)
"asset count > 0"
$ cover 40 (assetCount > 8)
"asset count > 8"
-- Entry counts:
$ cover 1 (entryCount == 0)
"UTxO set size = 0 entries"
$ cover 40 (entryCount > 16)
"UTxO set size > 16 entries"
$ cover 10 (entryCount > 32)
"UTxO set size > 32 entries"
True
where
assetCount = Set.size $ UTxOIndex.assets index
entryCount = UTxOIndex.size index
prop_Large_UTxOIndex_coverage :: Large UTxOIndex -> Property
prop_Large_UTxOIndex_coverage (Large index) =
-- Generation of large UTxO sets takes longer, so limit the number of runs:
withMaxSuccess 100 $ checkCoverage $ property
-- Asset counts:
$ cover 80 (assetCount > 8)
"asset count > 8"
-- Entry counts:
$ cover 80 (entryCount >= 1024)
"UTxO set size >= 1024 entries"
$ cover 20 (entryCount >= 2048)
"UTxO set size >= 2048 entries"
$ cover 10 (entryCount >= 3072)
"UTxO set size >= 3072 entries"
True
where
assetCount = Set.size $ UTxOIndex.assets index
entryCount = UTxOIndex.size index
--------------------------------------------------------------------------------
-- Ordering of token maps
--------------------------------------------------------------------------------
prop_AssetCount_TokenMap_placesEmptyMapsFirst
:: NonEmpty TokenMap
-> Property
prop_AssetCount_TokenMap_placesEmptyMapsFirst maps =
checkCoverage
-- Check counts of empty maps and non-empty maps:
$ cover 80 (emptyMapCount >= 1 && nonEmptyMapCount >= 1)
"empty map count >= 1 && non-empty map count >= 1"
$ cover 60 (emptyMapCount >= 2 && nonEmptyMapCount >= 2)
"empty map count >= 2 && non-empty map count >= 2"
$ cover 40 (emptyMapCount >= 4 && nonEmptyMapCount >= 4)
"empty map count >= 4 && non-empty map count >= 4"
$ cover 20 (emptyMapCount >= 8 && nonEmptyMapCount >= 8)
"empty map count >= 8 && non-empty map count >= 8"
-- Check head and last element of list:
$ cover 20 (isEmptyMap $ NE.head maps)
"head element is empty map"
$ cover 40 (not $ isEmptyMap $ NE.head maps)
"head element is non-empty map"
$ cover 20 (isEmptyMap $ NE.last maps)
"last element is empty map"
$ cover 40 (not $ isEmptyMap $ NE.last maps)
"last element is non-empty map"
prop
where
prop = (===)
( NE.span isEmptyMap $ NE.sortWith AssetCount maps )
( L.sortOn AssetCount emptyMaps
, L.sortOn AssetCount nonEmptyMaps
)
isEmptyMap = TokenMap.isEmpty
(emptyMaps, nonEmptyMaps) = NE.partition isEmptyMap maps
(emptyMapCount, nonEmptyMapCount) = (length emptyMaps, length nonEmptyMaps)
--------------------------------------------------------------------------------
-- Preparing outputs
--------------------------------------------------------------------------------
prop_prepareOutputsWith_twice
:: MinCoinValueFor
-> NonEmpty TxOut
-> Property
prop_prepareOutputsWith_twice minCoinValueDef outs =
once === twice
where
minCoinValueFor = mkMinCoinValueFor minCoinValueDef
(_:once:twice:_) = iterate (prepareOutputsWith minCoinValueFor) outs
prop_prepareOutputsWith_length
:: MinCoinValueFor
-> NonEmpty TxOut
-> Property
prop_prepareOutputsWith_length minCoinValueDef outs =
F.length (prepareOutputsWith minCoinValueFor outs) === F.length outs
where
minCoinValueFor = mkMinCoinValueFor minCoinValueDef
prop_prepareOutputsWith_assetsUnchanged
:: MinCoinValueFor
-> NonEmpty TxOut
-> Property
prop_prepareOutputsWith_assetsUnchanged minCoinValueDef outs =
(txOutAssets <$> (prepareOutputsWith minCoinValueFor outs))
===
(txOutAssets <$> outs)
where
minCoinValueFor = mkMinCoinValueFor minCoinValueDef
txOutAssets = TokenBundle.getAssets . view #tokens
prop_prepareOutputsWith_preparedOrExistedBefore
:: MinCoinValueFor
-> NonEmpty TxOut
-> Property
prop_prepareOutputsWith_preparedOrExistedBefore minCoinValueDef outs =
property $ F.all isPreparedOrExistedBefore (NE.zip outs outs')
where
minCoinValueFor = mkMinCoinValueFor minCoinValueDef
outs' = prepareOutputsWith minCoinValueFor outs
isPreparedOrExistedBefore :: (TxOut, TxOut) -> Bool
isPreparedOrExistedBefore (before, after)
| txOutCoin before /= Coin 0 =
txOutCoin after == txOutCoin before
| otherwise =
txOutCoin after == minCoinValueFor (view (#tokens . #tokens) before)
--------------------------------------------------------------------------------
-- Performing a selection
--------------------------------------------------------------------------------
-- | The result of calling 'performSelection'.
--
-- We define this type alias to shorten type signatures.
--
type PerformSelectionResult =
Either SelectionError (SelectionResult TokenBundle)
genSelectionCriteria :: Gen UTxOIndex -> Gen SelectionCriteria
genSelectionCriteria genUTxOIndex' = do
utxoAvailable <- genUTxOIndex'
outputCount <- max 1 <$>
choose (1, UTxOIndex.size utxoAvailable `div` 8)
outputsToCover <- NE.fromList <$>
replicateM outputCount genTxOut
selectionLimit <- frequency
[ (5, pure NoLimit)
, (1, pure $ MaximumInputLimit 0)
, (1, pure $ MaximumInputLimit (UTxOIndex.size utxoAvailable))
, (4, MaximumInputLimit <$> choose
(1, UTxOIndex.size utxoAvailable `div` 8)
)
]
extraCoinSource <- oneof [ pure Nothing, Just <$> genCoin ]
(assetsToMint, assetsToBurn) <-
genAssetsToMintAndBurn utxoAvailable outputsToCover
pure $ SelectionCriteria
{ outputsToCover
, utxoAvailable
, extraCoinSource
, selectionLimit
, assetsToMint
, assetsToBurn
}
where
genAssetsToMintAndBurn
:: UTxOIndex
-> NonEmpty TxOut
-> Gen (TokenMap, TokenMap)
genAssetsToMintAndBurn utxoAvailable outputsToCover =
frequency
[ (95, genForSuccess)
, ( 5, genForFailureWhereSomeMintedAssetsNotSpentOrBurned)
]
where
assetsProvidedByUTxO =
view #tokens $ UTxOIndex.balance utxoAvailable
assetsSpentByUserSpecifiedOutputs =
F.foldMap (view (#tokens . #tokens)) outputsToCover
-- To make a successful coin selection, we must satisfy the following
-- inequalities:
--
-- (assetsInUTxO ∪ assetsToMint) ⊇ (assetsInOutputs ∪ assetsToBurn)
-- assetsToMint ⊆ (assetsInOutputs ∪ assetsToBurn)
--
genForSuccess :: Gen (TokenMap, TokenMap)
genForSuccess = do
let assetsAvailableToBurn = TokenMap.difference
assetsProvidedByUTxO
assetsSpentByUserSpecifiedOutputs
assetsToBurn <- TokenMap.fromFlatList <$>
sublistOf (TokenMap.toFlatList assetsAvailableToBurn)
let assetsAvailableToMint = TokenMap.add
assetsToBurn
assetsSpentByUserSpecifiedOutputs
assetsToMint <- TokenMap.fromFlatList <$>
sublistOf (TokenMap.toFlatList assetsAvailableToMint)
pure (assetsToMint, assetsToBurn)
-- For this generator, we purposefully violate the following condition:
--
-- assetsToMint ⊆ (assetsInOutputs ∪ assetsToBurn)
--
-- This allows us to provoke the 'OutputsInsufficient' error.
--
genForFailureWhereSomeMintedAssetsNotSpentOrBurned
:: Gen (TokenMap, TokenMap)
genForFailureWhereSomeMintedAssetsNotSpentOrBurned = do
let assetsAvailableToBurn = TokenMap.difference
assetsProvidedByUTxO
assetsSpentByUserSpecifiedOutputs
assetsToBurn <- TokenMap.fromFlatList <$>
sublistOf (TokenMap.toFlatList assetsAvailableToBurn)
let assetsAvailableToMint = TokenMap.add
assetsToBurn
assetsSpentByUserSpecifiedOutputs
-- Here we deliberately mint more than we have spent and burned:
let assetsToMint = mtimesDefault (2 :: Int) assetsAvailableToMint
pure (assetsToMint, assetsToBurn)
prop_performSelection_small
:: MinCoinValueFor
-> CostFor
-> Blind (Small SelectionCriteria)
-> Property
prop_performSelection_small minCoinValueFor costFor (Blind (Small criteria)) =
checkCoverage $
-- Inspect the balance:
cover 30 (isUTxOBalanceSufficient criteria)
"balance sufficient" $
cover 25 (not $ isUTxOBalanceSufficient criteria)
"balance insufficient" $
-- Inspect the UTxO and user-specified outputs:
cover 5 (utxoHasAtLeastOneAsset)
"UTxO has at least one asset" $
cover 5 (not outputsHaveAtLeastOneAsset)
"No assets to cover" $
cover 2 (outputsHaveAtLeastOneAsset && not utxoHasAtLeastOneAsset)
"Assets to cover, but no assets in UTxO" $
-- Inspect the sets of minted and burned assets:
cover 20 (view #assetsToMint criteria /= TokenMap.empty)
"Have some assets to mint" $
cover 20 (view #assetsToBurn criteria /= TokenMap.empty)
"Have some assets to burn" $
cover 2 (view #assetsToMint criteria == TokenMap.empty)
"Have no assets to mint" $
cover 2 (view #assetsToBurn criteria == TokenMap.empty)
"Have no assets to burn" $
-- Inspect the intersection between minted assets and burned assets:
cover 2 (someAssetsAreBothMintedAndBurned)
"Some assets are both minted and burned" $
cover 2 (noAssetsAreBothMintedAndBurned)
"No assets are both minted and burned" $
-- Inspect the intersection between minted assets and spent assets:
cover 2 (someAssetsAreBothMintedAndSpent)
"Some assets are both minted and spent" $
cover 2 (noAssetsAreBothMintedAndSpent)
"No assets are both minted and spent" $
-- Inspect the intersection between spent assets and burned assets:
cover 2 (someAssetsAreBothSpentAndBurned)
"Some assets are both spent and burned" $
cover 2 (noAssetsAreBothSpentAndBurned)
"No assets are both spent and burned" $
-- Inspect the relationship between minted, burned, and spent assets:
cover 2 (allMintedAssetsEitherBurnedOrSpent)
"All minted assets were either spent or burned" $
cover 2 (not allMintedAssetsEitherBurnedOrSpent)
"Some minted assets were neither spent nor burned" $
prop_performSelection minCoinValueFor costFor (Blind criteria) $ \result ->
cover 10 (selectionUnlimited && selectionSufficient result)
"selection unlimited and sufficient"
. cover 4 (selectionLimited && selectionSufficient result)
"selection limited but sufficient"
. cover 10 (selectionLimited && selectionInsufficient result)
"selection limited and insufficient"
where
utxoHasAtLeastOneAsset = not
. Set.null
. UTxOIndex.assets
$ view #utxoAvailable criteria
outputsHaveAtLeastOneAsset =
not . Set.null $ TokenBundle.getAssets outputTokens
where
outputTokens = mconcat
. F.toList
. fmap (view #tokens)
$ outputsToCover criteria
selectionLimited :: Bool
selectionLimited = case view #selectionLimit criteria of
MaximumInputLimit _ -> True
NoLimit -> False
selectionUnlimited :: Bool
selectionUnlimited = not selectionLimited
selectionSufficient :: PerformSelectionResult -> Bool
selectionSufficient = \case
Right _ -> True
_ -> False
selectionInsufficient :: PerformSelectionResult -> Bool
selectionInsufficient = \case
Left (SelectionInsufficient _) -> True
_ -> False
assetsSpentByUserSpecifiedOutputs :: TokenMap
assetsSpentByUserSpecifiedOutputs =
F.foldMap (view (#tokens . #tokens)) (outputsToCover criteria)
allMintedAssetsEitherBurnedOrSpent :: Bool
allMintedAssetsEitherBurnedOrSpent =
view #assetsToMint criteria `leq` TokenMap.add
(view #assetsToBurn criteria)
(assetsSpentByUserSpecifiedOutputs)
someAssetsAreBothMintedAndBurned :: Bool
someAssetsAreBothMintedAndBurned
= TokenMap.isNotEmpty
$ TokenMap.intersection
(view #assetsToMint criteria)
(view #assetsToBurn criteria)
someAssetsAreBothMintedAndSpent :: Bool
someAssetsAreBothMintedAndSpent
= TokenMap.isNotEmpty
$ TokenMap.intersection
(view #assetsToMint criteria)
(assetsSpentByUserSpecifiedOutputs)
someAssetsAreBothSpentAndBurned :: Bool
someAssetsAreBothSpentAndBurned
= TokenMap.isNotEmpty
$ TokenMap.intersection
(assetsSpentByUserSpecifiedOutputs)
(view #assetsToBurn criteria)
noAssetsAreBothMintedAndBurned :: Bool
noAssetsAreBothMintedAndBurned = not someAssetsAreBothMintedAndBurned
noAssetsAreBothMintedAndSpent :: Bool
noAssetsAreBothMintedAndSpent = not someAssetsAreBothMintedAndSpent
noAssetsAreBothSpentAndBurned :: Bool
noAssetsAreBothSpentAndBurned = not someAssetsAreBothSpentAndBurned
prop_performSelection_large
:: MinCoinValueFor
-> CostFor
-> Blind (Large SelectionCriteria)
-> Property
prop_performSelection_large minCoinValueFor costFor (Blind (Large criteria)) =
-- Generation of large UTxO sets takes longer, so limit the number of runs:
withMaxSuccess 100 $
checkCoverage $
cover 50 (isUTxOBalanceSufficient criteria)
"UTxO balance sufficient" $
prop_performSelection minCoinValueFor costFor (Blind criteria) (const id)
prop_performSelection
:: MinCoinValueFor
-> CostFor
-> Blind SelectionCriteria
-> (PerformSelectionResult -> Property -> Property)
-> Property
prop_performSelection minCoinValueFor costFor (Blind criteria) coverage =
monadicIO $ do
monitor $ counterexample $ unlines
[ "extraCoinSource:"
, show extraCoinSource
, "selectionLimit:"
, show selectionLimit
, "assetsToMint:"
, pretty (Flat assetsToMint)
, "assetsToBurn:"
, pretty (Flat assetsToBurn)
]
result <- run $ performSelection
(mkMinCoinValueFor minCoinValueFor)
(mkCostFor costFor)
(mkBundleSizeAssessor NoBundleSizeLimit)
(criteria)
monitor (coverage result)
either onFailure onSuccess result
where
SelectionCriteria
{ outputsToCover
, utxoAvailable
, extraCoinSource
, selectionLimit
, assetsToMint
, assetsToBurn
} = criteria
onSuccess result = do
monitor $ counterexample $ unlines
[ "available UTXO balance:"
, pretty (Flat utxoBalanceAvailable)
, "required UTXO balance:"
, pretty (Flat utxoBalanceRequired)
, "change balance:"
, pretty (Flat balanceChange)
, "actual delta:"
, pretty (Flat <$> delta)
, "minimum expected coin surplus:"
, pretty minExpectedCoinSurplus
, "maximum expected coin surplus:"
, pretty maxExpectedCoinSurplus
, "absolute minimum coin quantity:"
, pretty absoluteMinCoinValue
, "number of outputs:"
, pretty (length outputsCovered)
, "number of change outputs:"
, pretty (length changeGenerated)
]
assertOnSuccess
"isUTxOBalanceSufficient criteria"
(isUTxOBalanceSufficient criteria)
assertOnSuccess
"selectionHasValidSurplus result"
(selectionHasValidSurplus result)
assertOnSuccess
"view #tokens surplus == TokenMap.empty"
(view #tokens surplus == TokenMap.empty)
assertOnSuccess
"TokenBundle.getCoin surplus >= minExpectedCoinSurplus"
(TokenBundle.getCoin surplus >= minExpectedCoinSurplus)
assertOnSuccess
"TokenBundle.getCoin surplus <= maxExpectedCoinSurplus"
(TokenBundle.getCoin surplus <= maxExpectedCoinSurplus)
assertOnSuccess
"utxoAvailable == UTxOIndex.insertMany inputsSelected utxoRemaining"
(utxoAvailable == UTxOIndex.insertMany inputsSelected utxoRemaining)
assertOnSuccess
"utxoRemaining == UTxOIndex.deleteMany txInsSelected utxoAvailable"
(utxoRemaining == UTxOIndex.deleteMany txInsSelected utxoAvailable)
assertOnSuccess
"outputsCovered == NE.toList outputsToCover"
(outputsCovered == NE.toList outputsToCover)
case selectionLimit of
MaximumInputLimit limit ->
assertOnSuccess
"NE.length inputsSelected <= limit"
(NE.length inputsSelected <= limit)
NoLimit ->
assert True
where
assertOnSuccess = assertWith . (<>) "onSuccess: "
absoluteMinCoinValue = mkMinCoinValueFor minCoinValueFor TokenMap.empty
delta :: SelectionDelta TokenBundle
delta = selectionDeltaAllAssets result
surplus :: TokenBundle
surplus = case delta of
SelectionSurplus s -> s
SelectionDeficit d -> error $ unwords
["Unexpected deficit:", show d]
minExpectedCoinSurplus :: Coin
minExpectedCoinSurplus = mkCostFor costFor skeleton
maxExpectedCoinSurplus :: Coin
maxExpectedCoinSurplus = minExpectedCoinSurplus `addCoin` toAdd
where
toAdd = absoluteMinCoinValue `multiplyCoin`
(length outputsCovered - length changeGenerated)
multiplyCoin :: Coin -> Int -> Coin
multiplyCoin (Coin c) i = Coin $ c * fromIntegral i
SelectionResult
{ inputsSelected
, changeGenerated
, outputsCovered
, utxoRemaining
} = result
skeleton = SelectionSkeleton
{ skeletonInputCount =
length inputsSelected
, skeletonOutputs =
NE.toList outputsToCover
, skeletonChange =
fmap (TokenMap.getAssets . view #tokens) changeGenerated
}
txInsSelected :: NonEmpty TxIn
txInsSelected = fst <$> inputsSelected
balanceChange =
F.fold changeGenerated
onFailure = \case
BalanceInsufficient e ->
onBalanceInsufficient e
SelectionInsufficient e ->
onSelectionInsufficient e
InsufficientMinCoinValues es ->
onInsufficientMinCoinValues es
UnableToConstructChange e ->
onUnableToConstructChange e
EmptyUTxO ->
onEmptyUTxO
onBalanceInsufficient e = do
monitor $ counterexample $ unlines
[ "available balance:"
, pretty (Flat utxoBalanceAvailable)
, "required balance:"
, pretty (Flat utxoBalanceRequired)
, "missing balance:"
, pretty (Flat $ balanceMissing e)
]
assertOnBalanceInsufficient
"not $ isUTxOBalanceSufficient criteria"
(not $ isUTxOBalanceSufficient criteria)
assertOnBalanceInsufficient
"utxoBalanceAvailable == errorBalanceAvailable"
(utxoBalanceAvailable == errorBalanceAvailable)
assertOnBalanceInsufficient
"utxoBalanceRequired == errorBalanceRequired"