Skip to content

Commit

Permalink
types,consensus: Simplify V2FileContractRenewal
Browse files Browse the repository at this point in the history
Instead of a full revision, renewals now specify just the final outputs
and rollover amounts. This sidesteps existing inconsistencies around
whether the "final revision" should be inserted into the accumulator or
not, as well as the issue of the revision being valid in a standalone
transaction.
  • Loading branch information
lukechampine committed Dec 2, 2024
1 parent b4d62da commit 6a67c9a
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 143 deletions.
1 change: 0 additions & 1 deletion consensus/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,6 @@ func (s State) ContractSigHash(fc types.V2FileContract) types.Hash256 {
func (s State) RenewalSigHash(fcr types.V2FileContractRenewal) types.Hash256 {
nilSigs(
&fcr.NewContract.RenterSignature, &fcr.NewContract.HostSignature,
&fcr.FinalRevision.RenterSignature, &fcr.FinalRevision.HostSignature,
&fcr.RenterSignature, &fcr.HostSignature,
)
return hashAll("sig/filecontractrenewal", s.v2ReplayPrefix(), fcr)
Expand Down
4 changes: 1 addition & 3 deletions consensus/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,7 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) {
var renter, host types.SiacoinOutput
switch r := fcr.Resolution.(type) {
case *types.V2FileContractRenewal:
renter, host = r.FinalRevision.RenterOutput, r.FinalRevision.HostOutput
renter.Value = renter.Value.Sub(r.RenterRollover)
host.Value = host.Value.Sub(r.HostRollover)
renter, host = r.FinalRenterOutput, r.FinalHostOutput
ms.addV2FileContractElement(fce.ID.V2RenewalID(), r.NewContract)
case *types.V2StorageProof:
renter, host = fc.RenterOutput, fc.HostOutput
Expand Down
63 changes: 20 additions & 43 deletions consensus/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,32 +690,17 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error {
return nil
}

validateSignatures := func(fc types.V2FileContract, renter, host types.PublicKey, renewal bool) error {
if renewal {
// The sub-contracts of a renewal must have empty signatures;
// otherwise they would be independently valid, i.e. the atomicity
// of the renewal could be violated. Consider a host who has lost or
// deleted their contract data; all they have to do is wait for a
// renter to initiate a renewal, then broadcast just the
// finalization of the old contract, allowing them to successfully
// resolve the contract without a storage proof.
if fc.RenterSignature != (types.Signature{}) {
return errors.New("has non-empty renter signature")
} else if fc.HostSignature != (types.Signature{}) {
return errors.New("has non-empty host signature")
}
} else {
contractHash := ms.base.ContractSigHash(fc)
if !renter.VerifyHash(contractHash, fc.RenterSignature) {
return errors.New("has invalid renter signature")
} else if !host.VerifyHash(contractHash, fc.HostSignature) {
return errors.New("has invalid host signature")
}
validateSignatures := func(fc types.V2FileContract, renter, host types.PublicKey) error {
contractHash := ms.base.ContractSigHash(fc)
if !renter.VerifyHash(contractHash, fc.RenterSignature) {
return errors.New("has invalid renter signature")
} else if !host.VerifyHash(contractHash, fc.HostSignature) {
return errors.New("has invalid host signature")
}
return nil
}

validateContract := func(fc types.V2FileContract, renewal bool) error {
validateContract := func(fc types.V2FileContract) error {
switch {
case fc.Filesize > fc.Capacity:
return fmt.Errorf("has filesize (%v) exceeding capacity (%v)", fc.Filesize, fc.Capacity)
Expand All @@ -730,10 +715,10 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error {
case fc.TotalCollateral.Cmp(fc.HostOutput.Value) > 0:
return fmt.Errorf("has total collateral (%d H) exceeding valid host value (%d H)", fc.TotalCollateral, fc.HostOutput.Value)
}
return validateSignatures(fc, fc.RenterPublicKey, fc.HostPublicKey, renewal)
return validateSignatures(fc, fc.RenterPublicKey, fc.HostPublicKey)
}

validateRevision := func(fce types.V2FileContractElement, rev types.V2FileContract, renewal bool) error {
validateRevision := func(fce types.V2FileContractElement, rev types.V2FileContract) error {
cur := fce.V2FileContract
if priorRev, ok := ms.v2revs[fce.ID]; ok {
cur = priorRev.V2FileContract
Expand Down Expand Up @@ -761,11 +746,11 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error {
return fmt.Errorf("leaves no time between proof height (%v) and expiration height (%v)", rev.ProofHeight, rev.ExpirationHeight)
}
// NOTE: very important that we verify with the *current* keys!
return validateSignatures(rev, cur.RenterPublicKey, cur.HostPublicKey, renewal)
return validateSignatures(rev, cur.RenterPublicKey, cur.HostPublicKey)
}

for i, fc := range txn.FileContracts {
if err := validateContract(fc, false); err != nil {
if err := validateContract(fc); err != nil {
return fmt.Errorf("file contract %v %s", i, err)
}
}
Expand All @@ -780,7 +765,7 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error {
// NOTE: disallowing this means that resolutions always take
// precedence over revisions
return fmt.Errorf("file contract revision %v resolves contract", i)
} else if err := validateRevision(fcr.Parent, rev, false); err != nil {
} else if err := validateRevision(fcr.Parent, rev); err != nil {
return fmt.Errorf("file contract revision %v %s", i, err)
}
revised[fcr.Parent.ID] = i
Expand All @@ -794,25 +779,17 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error {
switch r := fcr.Resolution.(type) {
case *types.V2FileContractRenewal:
renewal := *r
old, renewed := renewal.FinalRevision, renewal.NewContract
if old.RevisionNumber != types.MaxRevisionNumber {
return fmt.Errorf("file contract renewal %v does not finalize old contract", i)
} else if err := validateRevision(fcr.Parent, old, true); err != nil {
return fmt.Errorf("file contract renewal %v final revision %s", i, err)
} else if err := validateContract(renewed, false); err != nil {
return fmt.Errorf("file contract renewal %v initial revision %s", i, err)
}

rollover := renewal.RenterRollover.Add(renewal.HostRollover)
newContractCost := renewed.RenterOutput.Value.Add(renewed.HostOutput.Value).Add(ms.base.V2FileContractTax(renewed))
if renewal.RenterRollover.Cmp(old.RenterOutput.Value) > 0 {
return fmt.Errorf("file contract renewal %v has renter rollover (%d H) exceeding old output (%d H)", i, renewal.RenterRollover, old.RenterOutput.Value)
} else if renewal.HostRollover.Cmp(old.HostOutput.Value) > 0 {
return fmt.Errorf("file contract renewal %v has host rollover (%d H) exceeding old output (%d H)", i, renewal.HostRollover, old.HostOutput.Value)
} else if rollover.Cmp(newContractCost) > 0 {
newContractCost := renewal.NewContract.RenterOutput.Value.Add(renewal.NewContract.HostOutput.Value).Add(ms.base.V2FileContractTax(renewal.NewContract))
if totalRenter := renewal.FinalRenterOutput.Value.Add(renewal.RenterRollover); totalRenter != fc.RenterOutput.Value {
return fmt.Errorf("file contract renewal %v renter payout plus rollover (%d H) does not match old contract payout (%d H)", i, totalRenter, fc.RenterOutput.Value)
} else if totalHost := renewal.FinalHostOutput.Value.Add(renewal.HostRollover); totalHost != fc.HostOutput.Value {
return fmt.Errorf("file contract renewal %v host payout plus rollover (%d H) does not match old contract payout (%d H)", i, totalHost, fc.HostOutput.Value)
} else if rollover := renewal.RenterRollover.Add(renewal.HostRollover); rollover.Cmp(newContractCost) > 0 {
return fmt.Errorf("file contract renewal %v has rollover (%d H) exceeding new contract cost (%d H)", i, rollover, newContractCost)
} else if err := validateContract(renewal.NewContract); err != nil {
return fmt.Errorf("file contract renewal %v initial revision %s", i, err)
}

renewalHash := ms.base.RenewalSigHash(renewal)
if !fc.RenterPublicKey.VerifyHash(renewalHash, renewal.RenterSignature) {
return fmt.Errorf("file contract renewal %v has invalid renter signature", i)
Expand Down
129 changes: 57 additions & 72 deletions consensus/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1453,26 +1453,6 @@ func TestValidateV2Block(t *testing.T) {
}}
},
},
{
"file contract renewal that does not finalize old contract",
func(b *types.Block) {
txn := &b.V2.Transactions[0]
txn.SiacoinInputs = []types.V2SiacoinInput{{
Parent: sces[1],
SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy},
}}

rev := testFces[0].V2FileContract
resolution := types.V2FileContractRenewal{
FinalRevision: rev,
NewContract: testFces[0].V2FileContract,
}
txn.FileContractResolutions = []types.V2FileContractResolution{{
Parent: testFces[0],
Resolution: &resolution,
}}
},
},
{
"file contract renewal with invalid final revision",
func(b *types.Block) {
Expand All @@ -1482,12 +1462,9 @@ func TestValidateV2Block(t *testing.T) {
SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy},
}}

rev := testFces[0].V2FileContract
rev.RevisionNumber = types.MaxRevisionNumber
rev.TotalCollateral = types.ZeroCurrency
resolution := types.V2FileContractRenewal{
FinalRevision: rev,
NewContract: testFces[0].V2FileContract,
FinalRenterOutput: types.SiacoinOutput{Value: types.Siacoins(1e6)},
NewContract: testFces[0].V2FileContract,
}
txn.FileContractResolutions = []types.V2FileContractResolution{{
Parent: testFces[0],
Expand All @@ -1506,11 +1483,10 @@ func TestValidateV2Block(t *testing.T) {

rev := testFces[0].V2FileContract
rev.ExpirationHeight = rev.ProofHeight
finalRev := testFces[0].V2FileContract
finalRev.RevisionNumber = types.MaxRevisionNumber
resolution := types.V2FileContractRenewal{
FinalRevision: finalRev,
NewContract: rev,
FinalRenterOutput: rev.RenterOutput,
FinalHostOutput: rev.HostOutput,
NewContract: rev,
}
txn.FileContractResolutions = []types.V2FileContractResolution{{
Parent: testFces[0],
Expand Down Expand Up @@ -1885,7 +1861,6 @@ func TestV2RenewalResolution(t *testing.T) {
tests := []struct {
desc string
renewFn func(*types.V2Transaction)
errors bool
errString string
}{
{
Expand All @@ -1896,6 +1871,7 @@ func TestV2RenewalResolution(t *testing.T) {
desc: "valid renewal - no renter rollover",
renewFn: func(txn *types.V2Transaction) {
renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal)
renewal.FinalRenterOutput.Value = renewal.RenterRollover
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))
Expand All @@ -1905,6 +1881,7 @@ func TestV2RenewalResolution(t *testing.T) {
desc: "valid renewal - no host rollover",
renewFn: func(txn *types.V2Transaction) {
renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal)
renewal.FinalHostOutput.Value = renewal.HostRollover
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))
Expand All @@ -1914,19 +1891,39 @@ func TestV2RenewalResolution(t *testing.T) {
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)
partial := renewal.NewContract.MissedHostValue.Div64(2)
renewal.FinalHostOutput.Value = partial
renewal.HostRollover = renewal.HostRollover.Sub(partial)
// 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))
txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(partial).Sub(cs.V2FileContractTax(renewal.NewContract))
},
},
{
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)
partial := renewal.NewContract.RenterOutput.Value.Div64(2)
renewal.FinalRenterOutput.Value = partial
renewal.RenterRollover = renewal.RenterRollover.Sub(partial)
// 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))
txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(partial).Sub(cs.V2FileContractTax(renewal.NewContract))
},
},
{
desc: "invalid renewal - bad new contract renter signature",
renewFn: func(txn *types.V2Transaction) {
renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal)
renewal.NewContract.RenterSignature[0] ^= 1
},
errString: "invalid renter signature",
},
{
desc: "invalid renewal - bad new contract host signature",
renewFn: func(txn *types.V2Transaction) {
renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal)
renewal.NewContract.HostSignature[0] ^= 1
},
errString: "invalid host signature",
},
{
desc: "invalid renewal - not enough host funds",
Expand All @@ -1935,7 +1932,6 @@ func TestV2RenewalResolution(t *testing.T) {
renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2)
// do not adjust the change output
},
errors: true,
errString: "do not equal outputs",
},
{
Expand All @@ -1945,7 +1941,6 @@ func TestV2RenewalResolution(t *testing.T) {
renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2)
// do not adjust the change output
},
errors: true,
errString: "do not equal outputs",
},
{
Expand All @@ -1963,7 +1958,6 @@ func TestV2RenewalResolution(t *testing.T) {
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",
},
{
Expand All @@ -1980,18 +1974,12 @@ func TestV2RenewalResolution(t *testing.T) {
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{
newContract := types.V2FileContract{
ProofHeight: 100,
ExpirationHeight: 150,
RenterPublicKey: pk.PublicKey(),
Expand All @@ -2004,30 +1992,30 @@ func TestV2RenewalResolution(t *testing.T) {
},
MissedHostValue: types.Siacoins(10),
}
tax := cs.V2FileContractTax(fc)
newContract.RenterSignature = pk.SignHash(cs.ContractSigHash(newContract))
newContract.HostSignature = pk.SignHash(cs.ContractSigHash(newContract))

renewTxn := types.V2Transaction{
FileContractResolutions: []types.V2FileContractResolution{
{
Parent: fces[contractID],
Resolution: &types.V2FileContractRenewal{
FinalRevision: finalRevision,
NewContract: fc,
RenterRollover: types.Siacoins(10),
HostRollover: types.Siacoins(10),
},
FileContractResolutions: []types.V2FileContractResolution{{
Parent: fces[contractID],
Resolution: &types.V2FileContractRenewal{
FinalRenterOutput: types.SiacoinOutput{Address: fc.RenterOutput.Address, Value: types.ZeroCurrency},
FinalHostOutput: types.SiacoinOutput{Address: fc.HostOutput.Address, Value: types.ZeroCurrency},
NewContract: newContract,
RenterRollover: types.Siacoins(10),
HostRollover: types.Siacoins(10),
},
},
SiacoinInputs: []types.V2SiacoinInput{
{
Parent: genesisOutput,
SatisfiedPolicy: types.SatisfiedPolicy{
Policy: types.AnyoneCanSpend(),
},
}},
SiacoinInputs: []types.V2SiacoinInput{{
Parent: genesisOutput,
SatisfiedPolicy: types.SatisfiedPolicy{
Policy: types.AnyoneCanSpend(),
},
},
SiacoinOutputs: []types.SiacoinOutput{
{Address: addr, Value: genesisOutput.SiacoinOutput.Value.Sub(tax)},
},
}},
SiacoinOutputs: []types.SiacoinOutput{{
Address: addr,
Value: genesisOutput.SiacoinOutput.Value.Sub(cs.V2FileContractTax(newContract)),
}},
}
resolution, ok := renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal)
if !ok {
Expand All @@ -2038,23 +2026,20 @@ func TestV2RenewalResolution(t *testing.T) {
test.renewFn(&renewTxn)

// sign the renewal
newContract := &renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal).NewContract
newContract.RenterSignature = pk.SignHash(cs.ContractSigHash(*newContract))
newContract.HostSignature = pk.SignHash(cs.ContractSigHash(*newContract))
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:
case test.errString != "" && err == nil:
t.Fatal("expected error")
case test.errors && test.errString == "":
case test.errString != "" && test.errString == "":
t.Fatalf("received error %q, missing error string to compare", err)
case test.errors && !strings.Contains(err.Error(), test.errString):
case test.errString != "" && !strings.Contains(err.Error(), test.errString):
t.Fatalf("expected error %q to contain %q", err, test.errString)
case !test.errors && err != nil:
case test.errString == "" && err != nil:
t.Fatalf("unexpected error: %q", err)
}
})
Expand Down
Loading

0 comments on commit 6a67c9a

Please sign in to comment.