github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/mempool/consensus/execution_tree_test.go (about)

     1  package consensus
     2  
     3  import (
     4  	"reflect"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/suite"
     9  
    10  	"github.com/onflow/flow-go/module/mempool"
    11  
    12  	"github.com/onflow/flow-go/model/flow"
    13  
    14  	"github.com/onflow/flow-go/utils/unittest"
    15  )
    16  
    17  func TestReceiptsForest(t *testing.T) {
    18  	suite.Run(t, new(ExecutionTreeTestSuite))
    19  }
    20  
    21  type ExecutionTreeTestSuite struct {
    22  	suite.Suite
    23  	Forest *ExecutionTree
    24  }
    25  
    26  func (et *ExecutionTreeTestSuite) SetupTest() {
    27  	et.Forest = NewExecutionTree()
    28  }
    29  
    30  // addReceiptForest creates an Execution Tree for testing purposes and stores it in the ResultForest.
    31  // Nomenclature:
    32  //   - `r[A]` Execution Result for block A
    33  //     if we have multiple _different_ results for block A,
    34  //     we denote them as `r[A]_1`, `r[A]_2` ...
    35  //   - `ER[r]` Execution Receipt committing to result `r`
    36  //     if we have multiple _different_ receipts committing to the same result `r`,
    37  //     we denote them as `ER[r]_1`, `ER[r]_2` ...
    38  //   - Multiple Execution Receipts for the same result form an Equivalence Class, e.g. {`ER[r]_1`, `ER[r]_2`, ...}
    39  //     For brevity, we denote the Equivalence Class as `r{ER_1, ER_2, ...}`
    40  //
    41  // We consider the following forest of blocks where the number indicates the block height:
    42  //
    43  //	: <- A10 <- A11
    44  //	:
    45  //	: <- B10 <- B11 <- B12
    46  //	:       ^-- C11 <- C12 <- C13
    47  //	:
    48  //	:                      ?<- D13
    49  //	pruned height
    50  //
    51  // We construct the following Execution Tree:
    52  //
    53  //	:
    54  //	: <- r[A10]{ER} <- r[A11]{ER}
    55  //	:
    56  //	: <- r[B10]{ER} <- r[B11]_1 {ER_1, ER_2} <- r[B12]_1 {ER}
    57  //	:              ^-- r[B11]_2 {ER}         <- r[B12]_2 {ER}
    58  //	:             ^
    59  //	:             └-- r[C11] {ER_1, ER_2}    < . . .  ? ? ? ? . .   <- r[C13] {ER}
    60  //	:
    61  //	:                                                       ? ? ? ? <- r[C13] {ER}
    62  //	pruned height
    63  func (et *ExecutionTreeTestSuite) createExecutionTree() (map[string]*flow.Block, map[string]*flow.ExecutionResult, map[string]*flow.ExecutionReceipt) {
    64  	// Make blocks
    65  	blocks := make(map[string]*flow.Block)
    66  
    67  	blocks["A10"] = makeBlockWithHeight(10)
    68  	blocks["A11"] = makeChildBlock(blocks["A10"])
    69  
    70  	blocks["B10"] = makeBlockWithHeight(10)
    71  	blocks["B11"] = makeChildBlock(blocks["B10"])
    72  	blocks["B12"] = makeChildBlock(blocks["B11"])
    73  
    74  	blocks["C11"] = makeChildBlock(blocks["B10"])
    75  	blocks["C12"] = makeChildBlock(blocks["C11"])
    76  	blocks["C13"] = makeChildBlock(blocks["C12"])
    77  
    78  	blocks["D13"] = makeBlockWithHeight(13)
    79  
    80  	// Make Results
    81  	results := make(map[string]*flow.ExecutionResult)
    82  	results["r[A10]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["A10"]))
    83  	results["r[A11]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["A11"]), unittest.WithPreviousResult(*results["r[A10]"]))
    84  
    85  	results["r[B10]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B10"]))
    86  	results["r[B11_1]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B11"]), unittest.WithPreviousResult(*results["r[B10]"]))
    87  	results["r[B12_1]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B12"]), unittest.WithPreviousResult(*results["r[B11_1]"]))
    88  	results["r[B11_2]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B11"]), unittest.WithPreviousResult(*results["r[B10]"]))
    89  	results["r[B12_2]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B12"]), unittest.WithPreviousResult(*results["r[B11_2]"]))
    90  
    91  	results["r[C11]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["C11"]), unittest.WithPreviousResult(*results["r[B10]"]))
    92  	results["r[C12]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["C12"]), unittest.WithPreviousResult(*results["r[C11]"]))
    93  	results["r[C13]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["C13"]), unittest.WithPreviousResult(*results["r[C12]"]))
    94  
    95  	results["r[D13]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["D13"]))
    96  
    97  	// Make Receipts
    98  	receipts := make(map[string]*flow.ExecutionReceipt)
    99  
   100  	receipts["ER[r[A10]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[A10]"]))
   101  	receipts["ER[r[A11]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[A11]"]))
   102  
   103  	receipts["ER[r[B10]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B10]"]))
   104  	receipts["ER[r[B11]_1]_1"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B11_1]"]))
   105  	receipts["ER[r[B11]_1]_2"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B11_1]"]))
   106  	receipts["ER[r[B12]_1]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B12_1]"]))
   107  
   108  	receipts["ER[r[B11]_2]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B11_2]"]))
   109  	receipts["ER[r[B12]_2]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B12_2]"]))
   110  
   111  	receipts["ER[r[C11]]_1"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[C11]"]))
   112  	receipts["ER[r[C11]]_2"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[C11]"]))
   113  
   114  	receipts["ER[r[C13]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[C13]"]))
   115  	receipts["ER[r[D13]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[D13]"]))
   116  
   117  	return blocks, results, receipts
   118  }
   119  
   120  func (et *ExecutionTreeTestSuite) addReceipts2ReceiptsForest(receipts map[string]*flow.ExecutionReceipt, blocks map[string]*flow.Block) {
   121  	blockById := make(map[flow.Identifier]*flow.Block)
   122  	for _, block := range blocks {
   123  		blockById[block.ID()] = block
   124  	}
   125  	for name, rcpt := range receipts {
   126  		block := blockById[rcpt.ExecutionResult.BlockID]
   127  		_, err := et.Forest.AddReceipt(rcpt, block.Header)
   128  		if err != nil {
   129  			et.FailNow("failed to add receipt '%s'", name)
   130  		}
   131  	}
   132  }
   133  
   134  // Receipts that are already included in the fork should be skipped.
   135  func (et *ExecutionTreeTestSuite) Test_Initialization() {
   136  	assert.Equal(et.T(), uint(0), et.Forest.Size())
   137  	assert.Equal(et.T(), uint64(0), et.Forest.LowestHeight())
   138  }
   139  
   140  // Test_AddReceipt checks the Forest's AddReceipt method.
   141  // Receipts that are already included in the fork should be skipped.
   142  func (et *ExecutionTreeTestSuite) Test_AddReceipt() {
   143  	block := unittest.BlockFixture()
   144  	receipt := unittest.ReceiptForBlockFixture(&block)
   145  
   146  	// add should succeed and increase size
   147  	added, err := et.Forest.AddReceipt(receipt, block.Header)
   148  	assert.NoError(et.T(), err)
   149  	assert.True(et.T(), added)
   150  	assert.Equal(et.T(), uint(1), et.Forest.Size())
   151  
   152  	// adding different receipt for same result
   153  	receipt2 := unittest.ExecutionReceiptFixture(unittest.WithResult(&receipt.ExecutionResult))
   154  	added, err = et.Forest.AddReceipt(receipt2, block.Header)
   155  	assert.NoError(et.T(), err)
   156  	assert.True(et.T(), added)
   157  	assert.Equal(et.T(), uint(2), et.Forest.Size())
   158  
   159  	// repeated addition should be idempotent
   160  	added, err = et.Forest.AddReceipt(receipt, block.Header)
   161  	assert.NoError(et.T(), err)
   162  	assert.False(et.T(), added)
   163  	assert.Equal(et.T(), uint(2), et.Forest.Size())
   164  }
   165  
   166  // Test_AddResult_Detached verifies that vertices can be added to the Execution Tree without requiring
   167  // an Execution Receipt. Here, we add a result for a completely detached block. Starting a tree search
   168  // from this result should not yield any receipts.
   169  func (et *ExecutionTreeTestSuite) Test_AddResult_Detached() {
   170  	miscBlock := makeBlockWithHeight(101)
   171  	miscResult := unittest.ExecutionResultFixture(unittest.WithBlock(miscBlock))
   172  
   173  	err := et.Forest.AddResult(miscResult, miscBlock.Header)
   174  	assert.NoError(et.T(), err)
   175  	collectedReceipts, err := et.Forest.ReachableReceipts(miscResult.ID(), anyBlock(), anyReceipt())
   176  	assert.NoError(et.T(), err)
   177  	et.Assert().Empty(collectedReceipts)
   178  }
   179  
   180  // Test_AddResult_Bridge verifies that vertices can be added to the Execution Tree without requiring
   181  // an Execution Receipt. Here, we add the result r[C12], which closes the gap between r[C11] and r[C13].
   182  // Hence, the tree search should reach r[C13].
   183  func (et *ExecutionTreeTestSuite) Test_AddResult_Bridge() {
   184  	blocks, results, receipts := et.createExecutionTree()
   185  	et.addReceipts2ReceiptsForest(receipts, blocks)
   186  
   187  	// restrict traversal to B10 <- C11 <- C12 <- C13
   188  	blockFilter := func(h *flow.Header) bool {
   189  		for _, blockName := range []string{"B10", "C11", "C12", "C13"} {
   190  			if blocks[blockName].ID() == h.ID() {
   191  				return true
   192  			}
   193  		}
   194  		return false
   195  	}
   196  
   197  	// before we add result r[C12], tree search should not be able to reach r[C13]
   198  	collectedReceipts, err := et.Forest.ReachableReceipts(results["r[B10]"].ID(), blockFilter, anyReceipt())
   199  	assert.NoError(et.T(), err)
   200  	expected := et.toSet("ER[r[B10]]", "ER[r[C11]]_1", "ER[r[C11]]_2")
   201  	et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts)))
   202  
   203  	// after we added r[C12], tree search should reach r[C13] and hence include the corresponding receipt ER[r[C13]]
   204  	err = et.Forest.AddResult(results["r[C12]"], blocks["C12"].Header)
   205  	assert.NoError(et.T(), err)
   206  	collectedReceipts, err = et.Forest.ReachableReceipts(results["r[B10]"].ID(), blockFilter, anyReceipt())
   207  	assert.NoError(et.T(), err)
   208  	expected = et.toSet("ER[r[B10]]", "ER[r[C11]]_1", "ER[r[C11]]_2", "ER[r[C13]]")
   209  	et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts)))
   210  }
   211  
   212  // Test_FullTreeSearch verifies that Receipt Forest enumerates all receipts that are
   213  // reachable from the given result
   214  func (et *ExecutionTreeTestSuite) Test_FullTreeSearch() {
   215  	blocks, _, receipts := et.createExecutionTree()
   216  	et.addReceipts2ReceiptsForest(receipts, blocks)
   217  
   218  	// search Execution Tree starting from result `r[A10]`
   219  	collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[A10]]"].ExecutionResult.ID(), anyBlock(), anyReceipt())
   220  	assert.NoError(et.T(), err)
   221  	et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[A10]]", "ER[r[A11]]"), et.receiptSet(collectedReceipts, receipts)))
   222  
   223  	// search Execution Tree starting from result `r[B10]`
   224  	collectedReceipts, err = et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), anyBlock(), anyReceipt())
   225  	assert.NoError(et.T(), err)
   226  	expected := et.toSet(
   227  		"ER[r[B10]]", "ER[r[B11]_1]_1", "ER[r[B11]_1]_2", "ER[r[B12]_1]",
   228  		"ER[r[B11]_2]", "ER[r[B12]_2]",
   229  		"ER[r[C11]]_1", "ER[r[C11]]_2",
   230  	)
   231  	et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts)))
   232  
   233  	// search Execution Tree starting from result `r[B11]_2`
   234  	collectedReceipts, err = et.Forest.ReachableReceipts(receipts["ER[r[B11]_2]"].ExecutionResult.ID(), anyBlock(), anyReceipt())
   235  	assert.NoError(et.T(), err)
   236  	et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[B11]_2]", "ER[r[B12]_2]"), et.receiptSet(collectedReceipts, receipts)))
   237  
   238  	// search Execution Tree starting from result `r[C13]`
   239  	collectedReceipts, err = et.Forest.ReachableReceipts(receipts["ER[r[C13]]"].ExecutionResult.ID(), anyBlock(), anyReceipt())
   240  	assert.NoError(et.T(), err)
   241  	et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[C13]]"), et.receiptSet(collectedReceipts, receipts)))
   242  }
   243  
   244  // Test_ReceiptSorted verifies that receipts are ordered in a "parent first" manner
   245  func (et *ExecutionTreeTestSuite) Test_ReceiptOrdered() {
   246  	blocks, results, receipts := et.createExecutionTree()
   247  	et.addReceipts2ReceiptsForest(receipts, blocks)
   248  
   249  	// search Execution Tree starting from result `r[B10]`
   250  	collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), anyBlock(), anyReceipt())
   251  	assert.NoError(et.T(), err)
   252  
   253  	// first receipt must be for `r[B10]`
   254  	id := collectedReceipts[0].ExecutionResult.ID()
   255  	et.Assert().Equal(results["r[B10]"].ID(), id)
   256  	// for all subsequent receipts, a receipt committing to the parent result must have been listed before
   257  	knownResults := make(map[flow.Identifier]struct{})
   258  	knownResults[id] = struct{}{}
   259  	for _, rcpt := range collectedReceipts[1:] {
   260  		_, found := knownResults[rcpt.ExecutionResult.PreviousResultID]
   261  		et.Assert().True(found)
   262  		knownResults[rcpt.ExecutionResult.ID()] = struct{}{}
   263  	}
   264  }
   265  
   266  // Test_FilterReceipts checks that ExecutionTree does filter Receipts as directed by the ReceiptFilter.
   267  func (et *ExecutionTreeTestSuite) Test_FilterReceipts() {
   268  	blocks, _, receipts := et.createExecutionTree()
   269  	et.addReceipts2ReceiptsForest(receipts, blocks)
   270  
   271  	receiptFilter := func(rcpt *flow.ExecutionReceipt) bool {
   272  		for _, receiptName := range []string{"ER[r[B10]]", "ER[r[B11]_1]_2", "ER[r[B12]_2]", "ER[r[C11]]_1"} {
   273  			if receipts[receiptName].ID() == rcpt.ID() {
   274  				return false
   275  			}
   276  		}
   277  		return true
   278  	}
   279  
   280  	// search Execution Tree starting from result `r[B10]`
   281  	collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), anyBlock(), receiptFilter)
   282  	assert.NoError(et.T(), err)
   283  	expected := et.toSet("ER[r[B11]_1]_1", "ER[r[B12]_1]", "ER[r[B11]_2]", "ER[r[C11]]_2")
   284  	et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts)))
   285  }
   286  
   287  // Test_RootBlockExcluded checks that ExecutionTree does not traverses results for excluded forks.
   288  // In this specific test, we set the root results' block to be excluded. Therefore, the
   289  // tree search should stop immediately and no result should be returned.
   290  func (et *ExecutionTreeTestSuite) Test_RootBlockExcluded() {
   291  	blocks, _, receipts := et.createExecutionTree()
   292  	et.addReceipts2ReceiptsForest(receipts, blocks)
   293  
   294  	blockFilter := func(h *flow.Header) bool {
   295  		return blocks["B10"].ID() != h.ID()
   296  	}
   297  
   298  	// search Execution Tree starting from result `r[B10]`
   299  	collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), blockFilter, anyReceipt())
   300  	assert.NoError(et.T(), err)
   301  	assert.Empty(et.T(), collectedReceipts)
   302  }
   303  
   304  // Test_FilterChainForks checks that ExecutionTree does not traverses results for excluded forks
   305  func (et *ExecutionTreeTestSuite) Test_FilterChainForks() {
   306  	blocks, _, receipts := et.createExecutionTree()
   307  	et.addReceipts2ReceiptsForest(receipts, blocks)
   308  
   309  	blockFilter := func(h *flow.Header) bool {
   310  		return blocks["B11"].ID() != h.ID()
   311  	}
   312  
   313  	// search Execution Tree starting from result `r[B10]`: fork starting from B11 should be excluded
   314  	collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), blockFilter, anyReceipt())
   315  	assert.NoError(et.T(), err)
   316  	expected := et.toSet("ER[r[B10]]", "ER[r[C11]]_1", "ER[r[C11]]_2")
   317  	et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts)))
   318  }
   319  
   320  // Test_ExcludeReceiptsForSealedBlock verifies that, even though we are filtering out the
   321  // receipts for the root result, the tree search still traverses to the derived results
   322  func (et *ExecutionTreeTestSuite) Test_ExcludeReceiptsForSealedBlock() {
   323  	blocks, _, receipts := et.createExecutionTree()
   324  	et.addReceipts2ReceiptsForest(receipts, blocks)
   325  
   326  	receiptFilter := func(rcpt *flow.ExecutionReceipt) bool {
   327  		// exclude all receipts for block B11
   328  		return rcpt.ExecutionResult.BlockID != blocks["B11"].ID()
   329  	}
   330  
   331  	// search Execution Tree starting from result `r[B11]_1`
   332  	collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B11]_1]_1"].ExecutionResult.ID(), anyBlock(), receiptFilter)
   333  	assert.NoError(et.T(), err)
   334  	et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[B12]_1]"), et.receiptSet(collectedReceipts, receipts)))
   335  }
   336  
   337  // Test_UnknownResult checks the behaviour of ExecutionTree when the search is started on an unknown result
   338  func (et *ExecutionTreeTestSuite) Test_UnknownResult() {
   339  	blocks, _, receipts := et.createExecutionTree()
   340  	et.addReceipts2ReceiptsForest(receipts, blocks)
   341  
   342  	// search Execution Tree starting from result random result
   343  	_, err := et.Forest.ReachableReceipts(unittest.IdentifierFixture(), anyBlock(), anyReceipt())
   344  	assert.Error(et.T(), err)
   345  
   346  	// search Execution Tree starting from parent result of "ER[r[D13]]"; While the result is referenced,
   347  	// a receipt committing to this result was never added. Hence the search should error
   348  	_, err = et.Forest.ReachableReceipts(receipts["ER[r[D13]]"].ExecutionResult.PreviousResultID, anyBlock(), anyReceipt())
   349  	assert.Error(et.T(), err)
   350  }
   351  
   352  // Receipts that are already included in the fork should be skipped.
   353  func (et *ExecutionTreeTestSuite) Test_Prune() {
   354  	blocks, _, receipts := et.createExecutionTree()
   355  	et.addReceipts2ReceiptsForest(receipts, blocks)
   356  
   357  	assert.Equal(et.T(), uint(12), et.Forest.Size())
   358  	assert.Equal(et.T(), uint64(0), et.Forest.LowestHeight())
   359  
   360  	// prunes all receipts for blocks with height _smaller_ than 12
   361  	err := et.Forest.PruneUpToHeight(12)
   362  	assert.NoError(et.T(), err)
   363  	assert.Equal(et.T(), uint(4), et.Forest.Size())
   364  
   365  	// now, searching results from r[B11] should fail as the receipts were pruned
   366  	_, err = et.Forest.ReachableReceipts(receipts["ER[r[B11]_1]_2"].ExecutionResult.PreviousResultID, anyBlock(), anyReceipt())
   367  	assert.Error(et.T(), err)
   368  
   369  	// now, searching results from r[B12] should fail as the receipts were pruned
   370  	collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B12]_1]"].ExecutionResult.ID(), anyBlock(), anyReceipt())
   371  	assert.NoError(et.T(), err)
   372  	expected := et.toSet("ER[r[B12]_1]")
   373  	et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts)))
   374  }
   375  
   376  func anyBlock() mempool.BlockFilter {
   377  	return func(*flow.Header) bool { return true }
   378  }
   379  
   380  func anyReceipt() mempool.ReceiptFilter {
   381  	return func(*flow.ExecutionReceipt) bool { return true }
   382  }
   383  
   384  func makeBlockWithHeight(height uint64) *flow.Block {
   385  	block := unittest.BlockFixture()
   386  	block.Header.Height = height
   387  	return &block
   388  }
   389  
   390  func makeChildBlock(parent *flow.Block) *flow.Block {
   391  	return unittest.BlockWithParentFixture(parent.Header)
   392  }
   393  
   394  func (et *ExecutionTreeTestSuite) receiptSet(selected []*flow.ExecutionReceipt, receipts map[string]*flow.ExecutionReceipt) map[string]struct{} {
   395  	id2Name := make(map[flow.Identifier]string)
   396  	for name, rcpt := range receipts {
   397  		id2Name[rcpt.ID()] = name
   398  	}
   399  
   400  	names := make(map[string]struct{})
   401  	for _, r := range selected {
   402  		name, found := id2Name[r.ID()]
   403  		if !found {
   404  			et.FailNow("unknown execution receipt %x", r.ID())
   405  		}
   406  		names[name] = struct{}{}
   407  	}
   408  	return names
   409  }
   410  
   411  func (et *ExecutionTreeTestSuite) toSet(receiptNames ...string) map[string]struct{} {
   412  	set := make(map[string]struct{})
   413  	for _, name := range receiptNames {
   414  		set[name] = struct{}{}
   415  	}
   416  	if len(set) != len(receiptNames) {
   417  		et.FailNow("repeated receipts")
   418  	}
   419  	return set
   420  }