Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify V2FileContractRenewal #238

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading