diff --git a/consensus/validation_test.go b/consensus/validation_test.go index 0081ed7d..a8f03427 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -1816,7 +1816,7 @@ func TestV2RevisionApply(t *testing.T) { checkRevision(t, 100) } -func TestV2ResolutionRollover(t *testing.T) { +func TestV2RenewalResolution(t *testing.T) { n, genesisBlock := testnet() n.HardforkOak.Height = 0 @@ -1837,18 +1837,18 @@ func TestV2ResolutionRollover(t *testing.T) { Address: addr, Value: types.Siacoins(10), }, RenterOutput: types.SiacoinOutput{ - Address: addr, Value: types.ZeroCurrency, + Address: addr, Value: types.Siacoins(10), }, + MissedHostValue: types.Siacoins(10), } cs := n.GenesisState() sigHash := cs.ContractSigHash(fc) fc.HostSignature = pk.SignHash(sigHash) fc.RenterSignature = pk.SignHash(sigHash) - contractCost := cs.V2FileContractTax(fc).Add(fc.HostOutput.Value) genesisTxn := types.V2Transaction{ SiacoinOutputs: []types.SiacoinOutput{ - {Address: addr, Value: contractCost}, + {Address: addr, Value: types.Siacoins(1000)}, }, FileContracts: []types.V2FileContract{fc}, } @@ -1857,7 +1857,8 @@ func TestV2ResolutionRollover(t *testing.T) { } contractID := genesisTxn.V2FileContractID(genesisTxn.ID(), 0) fces := make(map[types.Hash256]types.V2FileContractElement) - applyContractChanges := func(au ApplyUpdate) { + genesisOutput := genesisTxn.EphemeralSiacoinOutput(0) + applyChanges := func(au ApplyUpdate) { au.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { switch { case res != nil: @@ -1869,55 +1870,196 @@ func TestV2ResolutionRollover(t *testing.T) { } }) + au.ForEachSiacoinElement(func(sce types.SiacoinElement, created, spent bool) { + if sce.ID == genesisOutput.ID { + genesisOutput = sce + } + }) + // update proofs + au.UpdateElementProof(&genesisOutput.StateElement) for key, fce := range fces { au.UpdateElementProof(&fce.StateElement) fces[key] = fce } } + // confirm the contract cs, au := ApplyBlock(cs, genesisBlock, V1BlockSupplement{}, time.Time{}) - applyContractChanges(au) - - finalRevision := fc - finalRevision.RevisionNumber = types.MaxRevisionNumber - finalRevision.RenterSignature = types.Signature{} - finalRevision.HostSignature = types.Signature{} - - renewal := types.V2FileContractRenewal{ - FinalRevision: finalRevision, - NewContract: types.V2FileContract{ - ProofHeight: 100, - ExpirationHeight: 150, - RenterPublicKey: pk.PublicKey(), - HostPublicKey: pk.PublicKey(), - HostOutput: types.SiacoinOutput{ - Address: addr, Value: types.Siacoins(1), + applyChanges(au) + + tests := []struct { + desc string + renewFn func(*types.V2Transaction) + errors bool + errString string + }{ + { + desc: "valid renewal", + renewFn: func(vt *types.V2Transaction) {}, // no changes should be a valid renewal + }, + { + desc: "valid renewal - no renter rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = types.ZeroCurrency + // subtract the renter cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) }, - RenterOutput: types.SiacoinOutput{ - Address: addr, Value: types.ZeroCurrency, + }, + { + desc: "valid renewal - no host rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.HostRollover = types.ZeroCurrency + // subtract the host cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.HostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) }, }, - HostRollover: types.Siacoins(10), - } - newContractCost := cs.V2FileContractTax(renewal.NewContract) - escapeAmount := renewal.HostRollover.Sub(renewal.NewContract.HostOutput.Value).Sub(newContractCost) - renewTxn := types.V2Transaction{ - FileContractResolutions: []types.V2FileContractResolution{ - { - Parent: fces[types.Hash256(contractID)], - Resolution: &renewal, + { + desc: "valid renewal - partial host rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2) + // subtract the host cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.HostOutput.Value.Div64(2)).Sub(cs.V2FileContractTax(renewal.NewContract)) }, }, - SiacoinOutputs: []types.SiacoinOutput{ - {Value: escapeAmount, Address: types.VoidAddress}, + { + desc: "valid renewal - partial renter rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2) + // subtract the host cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value.Div64(2)).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "invalid renewal - not enough host funds", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2) + // do not adjust the change output + }, + errors: true, + errString: "do not equal outputs", + }, + { + desc: "invalid renewal - not enough renter funds", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2) + // do not adjust the change output + }, + errors: true, + errString: "do not equal outputs", + }, + { + desc: "invalid renewal - host rollover escape", + renewFn: func(txn *types.V2Transaction) { + // tests that the file contract renewal rollover cannot be used + // outside of the new file contract. i.e. a siacoin output should + // not be able to be created using the funds from a rollover. This + // ensures that the maturity delay is enforced for renewals. + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.HostOutput.Value = types.Siacoins(1) + renewal.NewContract.MissedHostValue = types.Siacoins(1) + // adjust the file contract tax + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(cs.V2FileContractTax(renewal.NewContract)) + escapeAmount := renewal.HostRollover.Sub(renewal.NewContract.HostOutput.Value) + txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{Value: escapeAmount, Address: types.VoidAddress}) + }, + errors: true, + errString: "exceeding new contract cost", + }, + { + desc: "invalid renewal - renter rollover escape", + renewFn: func(txn *types.V2Transaction) { + // tests that the file contract renewal rollover cannot be used + // outside of the new file contract. i.e. a siacoin output should + // not be able to be created using the funds from a rollover. This + // ensures that the maturity delay is enforced for renewals. + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.RenterOutput.Value = types.Siacoins(1) + // adjust the file contract tax + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(cs.V2FileContractTax(renewal.NewContract)) + escapeAmount := renewal.RenterRollover.Sub(renewal.NewContract.RenterOutput.Value) + txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{Value: escapeAmount, Address: types.VoidAddress}) + }, + errors: true, + errString: "exceeding new contract cost", }, } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + finalRevision := fc + finalRevision.RevisionNumber = types.MaxRevisionNumber + finalRevision.RenterSignature = types.Signature{} + finalRevision.HostSignature = types.Signature{} + + fc := types.V2FileContract{ + ProofHeight: 100, + ExpirationHeight: 150, + RenterPublicKey: pk.PublicKey(), + HostPublicKey: pk.PublicKey(), + HostOutput: types.SiacoinOutput{ + Address: addr, Value: types.Siacoins(10), + }, + RenterOutput: types.SiacoinOutput{ + Address: addr, Value: types.Siacoins(10), + }, + MissedHostValue: types.Siacoins(10), + } + tax := cs.V2FileContractTax(fc) + renewTxn := types.V2Transaction{ + FileContractResolutions: []types.V2FileContractResolution{ + { + Parent: fces[types.Hash256(contractID)], + Resolution: &types.V2FileContractRenewal{ + FinalRevision: finalRevision, + NewContract: fc, + RenterRollover: types.Siacoins(10), + HostRollover: types.Siacoins(10), + }, + }, + }, + SiacoinInputs: []types.V2SiacoinInput{ + { + Parent: genesisOutput, + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.AnyoneCanSpend(), + }, + }, + }, + SiacoinOutputs: []types.SiacoinOutput{ + {Address: addr, Value: genesisOutput.SiacoinOutput.Value.Sub(tax)}, + }, + } + resolution, ok := renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + if !ok { + t.Fatal("expected renewal resolution") + } - ms := NewMidState(cs) - if err := ValidateV2Transaction(ms, renewTxn); err == nil { - t.Fatalf("expected error") - } else if !strings.Contains(err.Error(), "exceeding new contract cost") { - t.Fatal(err) + // modify the renewal + test.renewFn(&renewTxn) + + // sign the renewal + sigHash := cs.RenewalSigHash(*resolution) + resolution.RenterSignature = pk.SignHash(sigHash) + resolution.HostSignature = pk.SignHash(sigHash) + // apply the renewal + ms := NewMidState(cs) + err := ValidateV2Transaction(ms, renewTxn) + switch { + case test.errors && err == nil: + t.Fatal("expected error") + case test.errors && test.errString == "": + t.Fatalf("received error %q, missing error string to compare", err) + case test.errors && !strings.Contains(err.Error(), test.errString): + t.Fatalf("expected error %q to contain %q", err, test.errString) + case !test.errors && err != nil: + t.Fatalf("unexpected error: %q", err) + } + }) } }