gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/workerjobhassector_test.go (about)

     1  package renter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"testing"
     8  	"time"
     9  
    10  	"gitlab.com/NebulousLabs/fastrand"
    11  	"gitlab.com/SkynetLabs/skyd/build"
    12  	"gitlab.com/SkynetLabs/skyd/siatest/dependencies"
    13  	"gitlab.com/SkynetLabs/skyd/skymodules"
    14  	"go.sia.tech/siad/crypto"
    15  	"go.sia.tech/siad/modules"
    16  	"go.sia.tech/siad/types"
    17  )
    18  
    19  // TestHasSectorJobBatchCallNext makes sure that multiple has sector jobs are
    20  // batched together correctly.
    21  func TestHasSectorJobBatchCallNext(t *testing.T) {
    22  	t.Parallel()
    23  
    24  	// Create queue and job.
    25  	queue := jobHasSectorQueue{
    26  		availabilityMetrics: newAvailabilityMetrics(availabilityMetricsDefaultHalfLife),
    27  		jobGenericQueue:     newJobGenericQueue(&worker{}),
    28  	}
    29  	jhs := &jobHasSector{
    30  		jobGeneric: jobGeneric{
    31  			staticQueue: queue,
    32  			staticCtx:   context.Background(),
    33  		},
    34  		staticSpan: testSpan(),
    35  	}
    36  
    37  	// add jobs
    38  	for i := 0; i < int(hasSectorBatchSize)+1; i++ {
    39  		if !queue.callAdd(jhs) {
    40  			t.Fatal("job wasn't added")
    41  		}
    42  	}
    43  
    44  	// call callNext 3 times.
    45  	next1 := queue.callNext()
    46  	next2 := queue.callNext()
    47  	next3 := queue.callNext()
    48  
    49  	// the first should contain hasSectorBatchSize jobs, the second one 1 job
    50  	// and the third one should be nil.
    51  	if l := len(next1.(*jobHasSectorBatch).staticJobs); l != int(hasSectorBatchSize) {
    52  		t.Fatal("wrong size", l, hasSectorBatchSize)
    53  	}
    54  	if len(next2.(*jobHasSectorBatch).staticJobs) != 1 {
    55  		t.Fatal("wrong size")
    56  	}
    57  	if next3 != nil {
    58  		t.Fatal("should be nil")
    59  	}
    60  }
    61  
    62  // TestHasSectorJobQueueUpdateAvailabilityMetrics is a unit that verifies the HS
    63  // job queue correctly updates the availability metrics
    64  func TestHasSectorJobQueueUpdateAvailabilityMetrics(t *testing.T) {
    65  	t.Parallel()
    66  
    67  	// mock a worker
    68  	w := mockWorker(0)
    69  	hsq := w.staticJobHasSectorQueue
    70  
    71  	// assert basic case
    72  	hsq.callUpdateAvailabilityMetrics(1, 2, 1)
    73  	ar := hsq.callAvailabilityRate(1)
    74  	if ar != .5 {
    75  		t.Fatal("bad")
    76  	}
    77  
    78  	// assert updates take effect
    79  	hsq.callUpdateAvailabilityMetrics(1, 1, 0)
    80  	ar = hsq.callAvailabilityRate(1)
    81  	if ar != float64(1)/float64(3) {
    82  		t.Fatal("bad")
    83  	}
    84  
    85  	// assert min rate is returned of 0 metrics
    86  	ar = hsq.callAvailabilityRate(2)
    87  	if ar != jobHasSectorQueueMinAvailabilityRate {
    88  		t.Fatal("bad")
    89  	}
    90  
    91  	// assert min rate is returned if 0 available
    92  	hsq.callUpdateAvailabilityMetrics(2, 1, 0)
    93  	ar = hsq.callAvailabilityRate(2)
    94  	if ar != jobHasSectorQueueMinAvailabilityRate {
    95  		t.Fatal("bad")
    96  	}
    97  
    98  	// assert correct rate is returned if available
    99  	hsq.callUpdateAvailabilityMetrics(2, 1, 1)
   100  	ar = hsq.callAvailabilityRate(2)
   101  	if ar != .5 {
   102  		t.Fatal("bad")
   103  	}
   104  }
   105  
   106  // TestHasSectorJobQueueAvailabilityRate is a unit that verifies the HS job
   107  // queue correctly returns the availability rate
   108  func TestHasSectorJobQueueAvailabilityRate(t *testing.T) {
   109  	if testing.Short() {
   110  		t.SkipNow()
   111  	}
   112  	t.Parallel()
   113  
   114  	// create a new worker tester
   115  	wt, err := newWorkerTester(t.Name())
   116  	if err != nil {
   117  		t.Fatal(err)
   118  	}
   119  	defer func() {
   120  		err := wt.Close()
   121  		if err != nil {
   122  			t.Fatal(err)
   123  		}
   124  	}()
   125  	w := wt.worker
   126  
   127  	// assert the min availability rate on a new queue
   128  	randomNumPieces := fastrand.Intn(64) + 1
   129  	if w.staticJobHasSectorQueue.callAvailabilityRate(randomNumPieces) != jobHasSectorQueueMinAvailabilityRate {
   130  		t.Fatal("unexpected")
   131  	}
   132  
   133  	// create a two roots, add one to the host
   134  	randomData := fastrand.Bytes(int(modules.SectorSize))
   135  	randomRoot := crypto.MerkleRoot(randomData)
   136  	sectorData := fastrand.Bytes(int(modules.SectorSize))
   137  	sectorRoot := crypto.MerkleRoot(sectorData)
   138  	err = wt.host.AddSector(sectorRoot, sectorData)
   139  	if err != nil {
   140  		t.Fatal(err)
   141  	}
   142  
   143  	// add a job where the host is supposed to find one root out of two
   144  	roots := []crypto.Hash{sectorRoot, randomRoot}
   145  	responseChan := make(chan *jobHasSectorResponse, 1)
   146  	jhs := w.newJobHasSector(context.Background(), responseChan, randomNumPieces, roots...)
   147  	added := w.staticJobHasSectorQueue.callAdd(jhs)
   148  	if !added {
   149  		t.Fatal("unexpected")
   150  	}
   151  
   152  	// check whether the availability rate is correct
   153  	if err := build.Retry(10, 10*time.Millisecond, func() error {
   154  		availabilityRate := w.staticJobHasSectorQueue.callAvailabilityRate(randomNumPieces)
   155  		if availabilityRate != .5 {
   156  			return fmt.Errorf("unexpected availability rate %v != .5", availabilityRate)
   157  		}
   158  		return nil
   159  	}); err != nil {
   160  		t.Fatal(err)
   161  	}
   162  
   163  	// add a job where the host won't find any root
   164  	roots = []crypto.Hash{randomRoot, randomRoot, randomRoot}
   165  	responseChan = make(chan *jobHasSectorResponse, 1)
   166  	jhs = w.newJobHasSector(context.Background(), responseChan, randomNumPieces, roots...)
   167  	added = w.staticJobHasSectorQueue.callAdd(jhs)
   168  	if !added {
   169  		t.Fatal("unexpected")
   170  	}
   171  
   172  	// check whether the availability rate is correct
   173  	if err := build.Retry(10, 10*time.Millisecond, func() error {
   174  		availabilityRate := w.staticJobHasSectorQueue.callAvailabilityRate(randomNumPieces)
   175  		if availabilityRate != .2 {
   176  			return fmt.Errorf("unexpected availability rate %v != .2", availabilityRate)
   177  		}
   178  		return nil
   179  	}); err != nil {
   180  		t.Fatal(err)
   181  	}
   182  }
   183  
   184  // TestHasSectorJobExpectedBandwidth is a unit test that verifies our HS job
   185  // bandwidth estimates are given in a way we never execute a program and run out
   186  // of budget.
   187  func TestHasSectorJobExpectedBandwidth(t *testing.T) {
   188  	if testing.Short() {
   189  		t.SkipNow()
   190  	}
   191  	t.Parallel()
   192  
   193  	// create a new worker tester
   194  	wt, err := newWorkerTester(t.Name())
   195  	if err != nil {
   196  		t.Fatal(err)
   197  	}
   198  	defer func() {
   199  		err := wt.Close()
   200  		if err != nil {
   201  			t.Fatal(err)
   202  		}
   203  	}()
   204  	w := wt.worker
   205  	pt := wt.staticPriceTable().staticPriceTable
   206  
   207  	// numPacketsRequiredForSectors is a helper function that executes a HS
   208  	// program with the given amount of sectors and returns the amount of
   209  	// packets needed to cover both the upload and download bandwidth of the
   210  	// program.
   211  	numPacketsRequiredForSectors := func(numSectors int) (uint64, uint64) {
   212  		// build sectors
   213  		sectors := make([]crypto.Hash, numSectors)
   214  		for i := 0; i < numSectors; i++ {
   215  			sectors[i] = crypto.Hash{1, 2, 3}
   216  		}
   217  
   218  		// build program
   219  		pb := modules.NewProgramBuilder(&pt, 0)
   220  		for _, sector := range sectors {
   221  			pb.AddHasSectorInstruction(sector)
   222  		}
   223  		p, data := pb.Program()
   224  		cost, _, _ := pb.Cost(true)
   225  
   226  		// build job
   227  		jhs := new(jobHasSector)
   228  		jhs.staticSectors = sectors
   229  		jhs.staticNumPieces = skymodules.RenterDefaultNumPieces
   230  
   231  		// build a batch from the job for comparison
   232  		jhsb := *&jobHasSectorBatch{
   233  			staticJobs: []*jobHasSector{
   234  				jhs,
   235  			},
   236  		}
   237  
   238  		// calculate cost
   239  		ulBandwidth, dlBandwidth := jhs.callExpectedBandwidth()
   240  		bandwidthCost, bandwidthRefund := mdmBandwidthCost(pt, ulBandwidth, dlBandwidth)
   241  		cost = cost.Add(bandwidthCost)
   242  
   243  		// cost of batch should match.
   244  		ulb, dlb := jhsb.callExpectedBandwidth()
   245  		if ulb != ulBandwidth || dlb != dlBandwidth {
   246  			t.Fatal("batch bandwidth doesn't match job bandwidth")
   247  		}
   248  
   249  		// execute the program
   250  		_, limit, err := w.managedExecuteProgram(p, data, types.FileContractID{}, categoryDownload, cost, bandwidthRefund)
   251  		if err != nil {
   252  			t.Fatal(err)
   253  		}
   254  
   255  		return limit.Downloaded() / 1460, limit.Uploaded() / 1460
   256  	}
   257  
   258  	// expect 1 root to only require a single packet on both up and download
   259  	dl, ul := numPacketsRequiredForSectors(1)
   260  	if dl != 1 || ul != 1 {
   261  		t.Fatal("unexpected")
   262  	}
   263  
   264  	// expect 12 roots to not exceed the threshold (which is at 13) on download
   265  	dl, ul = numPacketsRequiredForSectors(12)
   266  	if dl != 1 || ul != 1 {
   267  		t.Fatal("unexpected")
   268  	}
   269  
   270  	// expect 13 roots to push us over the threshold, and require an extra
   271  	// packet on download
   272  	dl, ul = numPacketsRequiredForSectors(13)
   273  	if dl != 2 || ul != 1 {
   274  		t.Fatal("unexpected")
   275  	}
   276  
   277  	// expect 16 roots to not exceed the threshold (which is at 17) on upload
   278  	dl, ul = numPacketsRequiredForSectors(16)
   279  	if dl != 2 || ul != 1 {
   280  		t.Fatal("unexpected")
   281  	}
   282  
   283  	// expect 17 roots to push us over the threshold, and require an extra
   284  	// packet on upload
   285  	dl, ul = numPacketsRequiredForSectors(17)
   286  	if dl != 2 || ul != 2 {
   287  		t.Fatal("unexpected")
   288  	}
   289  }
   290  
   291  // TestAvailabilityMetrics is a unit test for AvailabilityMetrics
   292  func TestAvailabilityMetrics(t *testing.T) {
   293  	t.Parallel()
   294  
   295  	// verify we have the expected amount of buckets
   296  	metrics := newAvailabilityMetrics(100 * time.Second)
   297  	if len(metrics.buckets) != availabilityMetricsNumBuckets {
   298  		t.Fatal("bad")
   299  	}
   300  
   301  	// verify the piecesToBuckets slice against this hardcoded slice
   302  	expected := []int{-1, 0, 1, 2, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}
   303  	if len(metrics.piecesToBuckets) != len(expected) {
   304  		t.Fatal("bad")
   305  	}
   306  	for i := range expected {
   307  		if metrics.piecesToBuckets[i] != expected[i] {
   308  			t.Fatal("bad")
   309  		}
   310  	}
   311  
   312  	// manually verify some bucket indices
   313  	bucketIndex := metrics.piecesToBuckets[1]
   314  	if bucketIndex != 0 {
   315  		t.Fatal("bad", bucketIndex)
   316  	}
   317  	bucketIndex = metrics.piecesToBuckets[10]
   318  	if bucketIndex != 5 {
   319  		t.Fatal("bad")
   320  	}
   321  	bucketIndex = metrics.piecesToBuckets[30]
   322  	if bucketIndex != 10 {
   323  		t.Fatal("bad")
   324  	}
   325  	bucketIndex = metrics.piecesToBuckets[96]
   326  	if bucketIndex != 15 {
   327  		t.Fatal("bad")
   328  	}
   329  
   330  	// assert we're returning the last bucket if the num pieces is larger than
   331  	// what we support, which is 116 num pieces with the current defaults
   332  	if metrics.bucket(999) != metrics.buckets[bucketIndex] {
   333  		t.Fatal("bad")
   334  	}
   335  
   336  	// assert the bucket has no datapoints yet
   337  	bucket := metrics.bucket(10)
   338  	if bucket.totalAvailable != 0 || bucket.totalLookups != 0 {
   339  		t.Fatal("bad")
   340  	}
   341  
   342  	// update metrics and assert the correct bucket got updated
   343  	metrics.updateMetrics(10, 3, 2)
   344  	bucket = metrics.bucket(10)
   345  	if bucket.totalAvailable != 2 || bucket.totalLookups != 3 {
   346  		t.Fatal("bad")
   347  	}
   348  
   349  	// assert all other buckets have not been updated
   350  	for b := 0; b < availabilityMetricsNumBuckets; b++ {
   351  		bucketIndex = metrics.piecesToBuckets[10]
   352  		if b == bucketIndex {
   353  			continue
   354  		}
   355  		bucket := metrics.buckets[b]
   356  		if bucket.totalAvailable != 0 || bucket.totalLookups != 0 {
   357  			t.Fatal("bad")
   358  		}
   359  	}
   360  }
   361  
   362  // TestHasSectorJobWithdrawal verifies that executing a has-sector job withdraws
   363  // the right amount of tokens from the local EA balance.
   364  func TestHasSectorJobWithdrawal(t *testing.T) {
   365  	if testing.Short() {
   366  		t.SkipNow()
   367  	}
   368  	t.Parallel()
   369  
   370  	wt, err := newWorkerTesterCustomDependency(t.Name(), &dependencies.DependencyDisableWorker{}, skymodules.SkydProdDependencies)
   371  	if err != nil {
   372  		t.Fatal(err)
   373  	}
   374  	defer func() {
   375  		if err := wt.Close(); err != nil {
   376  			t.Fatal(err)
   377  		}
   378  	}()
   379  
   380  	// Get a price table and refill the account manually.
   381  	wt.staticUpdatePriceTable()
   382  	wt.managedRefillAccount()
   383  
   384  	// Loop that performs upload jobs.
   385  	go func() {
   386  		for range time.NewTicker(100 * time.Millisecond).C {
   387  			if wt.managedHasUploadJob() {
   388  				wt.externLaunchSerialJob(wt.managedPerformUploadChunkJob)
   389  			}
   390  		}
   391  	}()
   392  
   393  	// Upload some data.
   394  	r := wt.rt.renter
   395  	sup := skymodules.SkyfileUploadParameters{
   396  		BaseChunkRedundancy: 2,
   397  		Filename:            "test",
   398  		SiaPath:             skymodules.RandomSkynetFilePath(),
   399  	}
   400  	data := bytes.NewReader(fastrand.Bytes(10))
   401  	sl, err := r.UploadSkyfile(context.Background(), sup, skymodules.NewSkyfileReader(data, sup))
   402  	if err != nil {
   403  		t.Fatal(err)
   404  	}
   405  
   406  	// Get the pricetable.
   407  	pt := wt.staticPriceTable()
   408  
   409  	// Get the balance.
   410  	wt.staticAccount.mu.Lock()
   411  	balanceBefore := wt.staticAccount.balance
   412  	wt.staticAccount.mu.Unlock()
   413  
   414  	// Read the entry
   415  	respChan := make(chan *jobHasSectorResponse)
   416  	jhs := wt.newJobHasSector(context.Background(), respChan, 1, sl.MerkleRoot())
   417  	if !wt.externLaunchAsyncJob(jhs) {
   418  		t.Fatal("job wasn't launched")
   419  	}
   420  	resp := <-respChan
   421  	if resp.staticErr != nil {
   422  		t.Fatal(resp.staticErr)
   423  	}
   424  
   425  	// Get the balance after.
   426  	wt.staticAccount.mu.Lock()
   427  	balanceAfter := wt.staticAccount.balance
   428  	wt.staticAccount.mu.Unlock()
   429  
   430  	// Compute the expected cost.
   431  	pb := modules.NewProgramBuilder(&pt.staticPriceTable, 0)
   432  	pb.AddHasSectorInstruction(sl.MerkleRoot())
   433  	cost, _, _ := pb.Cost(true)
   434  
   435  	// Add the expected bandwidth cost.
   436  	// NOTE: We use 1460 here because we know that that's the actual
   437  	// bandwidth we are using. readRegistryJobExpectedBandwidth will
   438  	// slightly overestimate the bandwidth by using 1500 and then issue a
   439  	// refund for 40 bytes.
   440  	bandwidthCost := modules.MDMBandwidthCost(pt.staticPriceTable, 1460, 1460)
   441  	cost = cost.Add(bandwidthCost)
   442  
   443  	// Make sure the delta of the account matches the cost.
   444  	balanceDelta := balanceBefore.Sub(balanceAfter)
   445  	if !balanceDelta.Equals(cost) {
   446  		t.Fatal("delta doesn't match cost", balanceDelta, cost)
   447  	}
   448  
   449  	// Same test again but for non-existent root.
   450  	respChan = make(chan *jobHasSectorResponse)
   451  	jhs = wt.newJobHasSector(context.Background(), respChan, 1, crypto.Hash{})
   452  	if !wt.externLaunchAsyncJob(jhs) {
   453  		t.Fatal("job wasn't launched")
   454  	}
   455  	resp = <-respChan
   456  	if resp.staticErr != nil {
   457  		t.Fatal(resp.staticErr)
   458  	}
   459  
   460  	// Get the balance after.
   461  	wt.staticAccount.mu.Lock()
   462  	balanceBefore = balanceAfter
   463  	balanceAfter = wt.staticAccount.balance
   464  	wt.staticAccount.mu.Unlock()
   465  
   466  	// Compute the expected cost.
   467  	pb = modules.NewProgramBuilder(&pt.staticPriceTable, 0)
   468  	pb.AddHasSectorInstruction(crypto.Hash{})
   469  	cost, _, _ = pb.Cost(true)
   470  
   471  	// Add the expected bandwidth cost.
   472  	// NOTE: We use 1460 here because we know that that's the actual
   473  	// bandwidth we are using. callExpectedBandwidth will slightly
   474  	// overestimate the bandwidth by using 1500 and then issue a refund for
   475  	// 40 bytes.
   476  	bandwidthCost = modules.MDMBandwidthCost(pt.staticPriceTable, 1460, 1460)
   477  	cost = cost.Add(bandwidthCost)
   478  
   479  	// Make sure the delta of the account matches the cost.
   480  	balanceDelta = balanceBefore.Sub(balanceAfter)
   481  	if !balanceDelta.Equals(cost) {
   482  		t.Fatal("delta doesn't match cost", balanceDelta, cost)
   483  	}
   484  }
   485  
   486  // TestHasSectorJobWithdrawal verifies that executing a batched has-sector job
   487  // withdraws the right amount of tokens from the local EA balance.
   488  func TestHasSectorBatchJobWithdrawal(t *testing.T) {
   489  	if testing.Short() {
   490  		t.SkipNow()
   491  	}
   492  	t.Parallel()
   493  
   494  	wt, err := newWorkerTesterCustomDependency(t.Name(), &dependencies.DependencyDisableWorker{}, skymodules.SkydProdDependencies)
   495  	if err != nil {
   496  		t.Fatal(err)
   497  	}
   498  	defer func() {
   499  		if err := wt.Close(); err != nil {
   500  			t.Fatal(err)
   501  		}
   502  	}()
   503  
   504  	// Get a price table and refill the account manually.
   505  	wt.staticUpdatePriceTable()
   506  	wt.managedRefillAccount()
   507  
   508  	// Loop that performs upload jobs.
   509  	go func() {
   510  		for range time.NewTicker(100 * time.Millisecond).C {
   511  			if wt.managedHasUploadJob() {
   512  				wt.externLaunchSerialJob(wt.managedPerformUploadChunkJob)
   513  			}
   514  		}
   515  	}()
   516  
   517  	// Upload some data.
   518  	r := wt.rt.renter
   519  	sup := skymodules.SkyfileUploadParameters{
   520  		BaseChunkRedundancy: 2,
   521  		Filename:            "test",
   522  		SiaPath:             skymodules.RandomSkynetFilePath(),
   523  	}
   524  	data := bytes.NewReader(fastrand.Bytes(10))
   525  	sl, err := r.UploadSkyfile(context.Background(), sup, skymodules.NewSkyfileReader(data, sup))
   526  	if err != nil {
   527  		t.Fatal(err)
   528  	}
   529  
   530  	// Get the pricetable.
   531  	pt := wt.staticPriceTable()
   532  
   533  	// Get the balance.
   534  	wt.staticAccount.mu.Lock()
   535  	balanceBefore := wt.staticAccount.balance
   536  	wt.staticAccount.mu.Unlock()
   537  
   538  	// Read the entry
   539  	respChan := make(chan *jobHasSectorResponse)
   540  	jhs := wt.newJobHasSector(context.Background(), respChan, 1, sl.MerkleRoot())
   541  	var jhsb jobHasSectorBatch
   542  	for i := 0; i < hasSectorBatchSize; i++ {
   543  		jhsb.staticJobs = append(jhsb.staticJobs, jhs)
   544  	}
   545  	if !wt.externLaunchAsyncJob(jhsb) {
   546  		t.Fatal("job wasn't launched")
   547  	}
   548  	resp := <-respChan
   549  	if resp.staticErr != nil {
   550  		t.Fatal(resp.staticErr)
   551  	}
   552  
   553  	// Get the balance after.
   554  	wt.staticAccount.mu.Lock()
   555  	balanceAfter := wt.staticAccount.balance
   556  	wt.staticAccount.mu.Unlock()
   557  
   558  	// Compute the expected cost.
   559  	pb := modules.NewProgramBuilder(&pt.staticPriceTable, 0)
   560  	for range jhsb.staticJobs {
   561  		pb.AddHasSectorInstruction(sl.MerkleRoot())
   562  	}
   563  	cost, _, _ := pb.Cost(true)
   564  
   565  	// Add the expected bandwidth cost.
   566  	// NOTE: We use 1460 and 2920 here because we know that that's the
   567  	// actual bandwidth we are using. callExpectedBandwidth will slightly
   568  	// overestimate the bandwidth by using 1500 and 3000 and then issue a
   569  	// refund for the difference.
   570  	bandwidthCost := modules.MDMBandwidthCost(pt.staticPriceTable, 1460, 2920)
   571  	cost = cost.Add(bandwidthCost)
   572  
   573  	// Make sure the delta of the account matches the cost.
   574  	balanceDelta := balanceBefore.Sub(balanceAfter)
   575  	if !balanceDelta.Equals(cost) {
   576  		t.Fatal("delta doesn't match cost", balanceDelta, cost)
   577  	}
   578  }