Skip to content

Commit

Permalink
Merge pull request #238 from SiaFoundation/renewal-revision
Browse files Browse the repository at this point in the history
Simplify V2FileContractRenewal
  • Loading branch information
n8maninger authored Dec 3, 2024
2 parents b5ca54b + 5440cc4 commit 2e629a0
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 155 deletions.
1 change: 0 additions & 1 deletion consensus/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,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, true); 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
126 changes: 57 additions & 69 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 @@ -2045,13 +2033,13 @@ func TestV2RenewalResolution(t *testing.T) {
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 2e629a0

Please sign in to comment.