diff --git a/internal/test/rhp/v3/rhp.go b/internal/test/rhp/v3/rhp.go index 292382eb..55f7a845 100644 --- a/internal/test/rhp/v3/rhp.go +++ b/internal/test/rhp/v3/rhp.go @@ -755,7 +755,6 @@ func taxAdjustedPayout(target types.Currency) types.Currency { func prepareContractRenewal(currentRevision types.FileContractRevision, renterAddress types.Address, renterKey types.PrivateKey, renterPayout, newCollateral types.Currency, hostKey types.PublicKey, hostAddr types.Address, host rhp3.HostPriceTable, endHeight uint64) (types.FileContract, types.Currency) { hostValidPayout, hostMissedPayout, voidMissedPayout, basePrice := calculateRenewalPayouts(currentRevision.FileContract, newCollateral, host, endHeight) - renterPub := renterKey.PublicKey() return types.FileContract{ Filesize: currentRevision.Filesize, FileMerkleRoot: currentRevision.FileMerkleRoot, @@ -764,9 +763,10 @@ func prepareContractRenewal(currentRevision types.FileContractRevision, renterAd Payout: taxAdjustedPayout(renterPayout.Add(hostValidPayout)), UnlockHash: types.Hash256(types.UnlockConditions{ PublicKeys: []types.UnlockKey{ - {Algorithm: types.SpecifierEd25519, Key: renterPub[:]}, - {Algorithm: types.SpecifierEd25519, Key: hostKey[:]}, + renterKey.PublicKey().UnlockKey(), + hostKey.UnlockKey(), }, + SignaturesRequired: 2, }.UnlockHash()), RevisionNumber: 0, ValidProofOutputs: []types.SiacoinOutput{ diff --git a/rhp/v2/contracts.go b/rhp/v2/contracts.go index bd6e7a4a..fc7dd025 100644 --- a/rhp/v2/contracts.go +++ b/rhp/v2/contracts.go @@ -99,6 +99,8 @@ func validateContractRenewal(existing types.FileContractRevision, renewal types. return types.ZeroCurrency, types.ZeroCurrency, types.ZeroCurrency, errors.New("wrong address for missed host output") case renewal.MissedProofOutputs[2].Address != types.VoidAddress: return types.ZeroCurrency, types.ZeroCurrency, types.ZeroCurrency, errors.New("wrong address for void output") + case renewal.UnlockHash != types.Hash256(contractUnlockConditions(hostKey, renterKey).UnlockHash()): + return types.ZeroCurrency, types.ZeroCurrency, types.ZeroCurrency, errors.New("incorrect unlock hash") } expectedBurn := baseHostRevenue.Add(baseRiskedCollateral) diff --git a/rhp/v2/contracts_test.go b/rhp/v2/contracts_test.go new file mode 100644 index 00000000..fa1d722b --- /dev/null +++ b/rhp/v2/contracts_test.go @@ -0,0 +1,99 @@ +package rhp + +import ( + "math" + "testing" + + rhp2 "go.sia.tech/core/rhp/v2" + "go.sia.tech/core/types" + "lukechampine.com/frand" +) + +func TestValidateContractRenewal(t *testing.T) { + hostKey, renterKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey(), types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey() + hostAddress, renterAddress := types.StandardUnlockHash(hostKey), types.StandardUnlockHash(renterKey) + hostCollateral := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) + renterAllowance := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) + + settings := rhp2.HostSettings{ + MaxDuration: math.MaxUint64, + MaxCollateral: types.NewCurrency(math.MaxUint64, math.MaxUint64), + Address: hostAddress, + } + + existing := types.FileContractRevision{ + ParentID: types.FileContractID{1}, + UnlockConditions: types.UnlockConditions{ + PublicKeys: []types.UnlockKey{renterKey.UnlockKey(), hostKey.UnlockKey()}, + SignaturesRequired: 2, + }, + FileContract: types.FileContract{ + RevisionNumber: frand.Uint64n(math.MaxUint64), + Filesize: frand.Uint64n(math.MaxUint64), + FileMerkleRoot: frand.Entropy256(), + WindowStart: 100, + WindowEnd: 300, + Payout: types.ZeroCurrency, // not validated here + UnlockHash: types.Hash256(types.UnlockConditions{ + PublicKeys: []types.UnlockKey{renterKey.UnlockKey(), hostKey.UnlockKey()}, + SignaturesRequired: 2, + }.UnlockHash()), + ValidProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + }, + MissedProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + {Address: types.VoidAddress, Value: types.ZeroCurrency}, + }, + }, + } + + renewal := types.FileContract{ + Filesize: existing.Filesize, + FileMerkleRoot: existing.FileMerkleRoot, + WindowStart: existing.WindowStart + 100, + WindowEnd: existing.WindowEnd + 100, + ValidProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + }, + MissedProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + {Address: types.VoidAddress, Value: types.ZeroCurrency}, + }, + } + + // bad renter key + badRenterKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey().UnlockKey() + renewal.UnlockHash = types.Hash256(contractUnlockConditions(hostKey.UnlockKey(), badRenterKey).UnlockHash()) + _, _, _, err := validateContractRenewal(existing, renewal, hostKey.UnlockKey(), renterKey.UnlockKey(), types.ZeroCurrency, types.ZeroCurrency, 0, settings) + if err == nil || err.Error() != "incorrect unlock hash" { + t.Fatalf("expected unlock hash error, got %v", err) + } + + // bad host key + badHostKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey().UnlockKey() + renewal.UnlockHash = types.Hash256(contractUnlockConditions(badHostKey, renterKey.UnlockKey()).UnlockHash()) + _, _, _, err = validateContractRenewal(existing, renewal, hostKey.UnlockKey(), renterKey.UnlockKey(), types.ZeroCurrency, types.ZeroCurrency, 0, settings) + if err == nil || err.Error() != "incorrect unlock hash" { + t.Fatalf("expected unlock hash error, got %v", err) + } + + // original keys + renewal.UnlockHash = types.Hash256(contractUnlockConditions(hostKey.UnlockKey(), renterKey.UnlockKey()).UnlockHash()) + _, _, _, err = validateContractRenewal(existing, renewal, hostKey.UnlockKey(), renterKey.UnlockKey(), types.ZeroCurrency, types.ZeroCurrency, 0, settings) + if err != nil { + t.Fatal(err) + } + + // different renter key, same host key + newRenterKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey().UnlockKey() + renewal.UnlockHash = types.Hash256(contractUnlockConditions(hostKey.UnlockKey(), newRenterKey).UnlockHash()) + _, _, _, err = validateContractRenewal(existing, renewal, hostKey.UnlockKey(), newRenterKey, types.ZeroCurrency, types.ZeroCurrency, 0, settings) + if err != nil { + t.Fatal(err) + } +} diff --git a/rhp/v3/contracts.go b/rhp/v3/contracts.go index 02951e38..fce1ce67 100644 --- a/rhp/v3/contracts.go +++ b/rhp/v3/contracts.go @@ -16,6 +16,13 @@ func hashFinalRevision(clearing types.FileContractRevision, renewal types.FileCo return h.Sum() } +func contractUnlockConditions(hostKey, renterKey types.UnlockKey) types.UnlockConditions { + return types.UnlockConditions{ + PublicKeys: []types.UnlockKey{renterKey, hostKey}, + SignaturesRequired: 2, + } +} + // validateContractRenewal verifies that the renewed contract is valid given the // old contract. A renewal is valid if the contract fields match and the // revision number is 0. @@ -45,6 +52,8 @@ func validateContractRenewal(existing types.FileContractRevision, renewal types. return types.ZeroCurrency, types.ZeroCurrency, errors.New("wrong address for missed host output") case renewal.MissedProofOutputs[2].Address != types.VoidAddress: return types.ZeroCurrency, types.ZeroCurrency, errors.New("wrong address for void output") + case renewal.UnlockHash != types.Hash256(contractUnlockConditions(hostKey, renterKey).UnlockHash()): + return types.ZeroCurrency, types.ZeroCurrency, errors.New("incorrect unlock hash") } expectedBurn := baseStorageRevenue.Add(baseRiskedCollateral) diff --git a/rhp/v3/contracts_test.go b/rhp/v3/contracts_test.go new file mode 100644 index 00000000..47e8ae78 --- /dev/null +++ b/rhp/v3/contracts_test.go @@ -0,0 +1,98 @@ +package rhp + +import ( + "math" + "testing" + + rhp3 "go.sia.tech/core/rhp/v3" + "go.sia.tech/core/types" + "lukechampine.com/frand" +) + +func TestValidateContractRenewal(t *testing.T) { + hostKey, renterKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey(), types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey() + hostAddress, renterAddress := types.StandardUnlockHash(hostKey), types.StandardUnlockHash(renterKey) + hostCollateral := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) + renterAllowance := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) + + pt := rhp3.HostPriceTable{ + MaxDuration: math.MaxUint64, + MaxCollateral: types.NewCurrency(math.MaxUint64, math.MaxUint64), + } + + existing := types.FileContractRevision{ + ParentID: types.FileContractID{1}, + UnlockConditions: types.UnlockConditions{ + PublicKeys: []types.UnlockKey{renterKey.UnlockKey(), hostKey.UnlockKey()}, + SignaturesRequired: 2, + }, + FileContract: types.FileContract{ + RevisionNumber: frand.Uint64n(math.MaxUint64), + Filesize: frand.Uint64n(math.MaxUint64), + FileMerkleRoot: frand.Entropy256(), + WindowStart: 100, + WindowEnd: 300, + Payout: types.ZeroCurrency, // not validated here + UnlockHash: types.Hash256(types.UnlockConditions{ + PublicKeys: []types.UnlockKey{renterKey.UnlockKey(), hostKey.UnlockKey()}, + SignaturesRequired: 2, + }.UnlockHash()), + ValidProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + }, + MissedProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + {Address: types.VoidAddress, Value: types.ZeroCurrency}, + }, + }, + } + + renewal := types.FileContract{ + Filesize: existing.Filesize, + FileMerkleRoot: existing.FileMerkleRoot, + WindowStart: existing.WindowStart + 100, + WindowEnd: existing.WindowEnd + 100, + ValidProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + }, + MissedProofOutputs: []types.SiacoinOutput{ + {Address: renterAddress, Value: renterAllowance}, + {Address: hostAddress, Value: hostCollateral}, + {Address: types.VoidAddress, Value: types.ZeroCurrency}, + }, + } + + // bad renter key + badRenterKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey().UnlockKey() + renewal.UnlockHash = types.Hash256(contractUnlockConditions(hostKey.UnlockKey(), badRenterKey).UnlockHash()) + _, _, err := validateContractRenewal(existing, renewal, hostKey.UnlockKey(), renterKey.UnlockKey(), hostAddress, types.ZeroCurrency, types.ZeroCurrency, pt) + if err == nil || err.Error() != "incorrect unlock hash" { + t.Fatalf("expected unlock hash error, got %v", err) + } + + // bad host key + badHostKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey().UnlockKey() + renewal.UnlockHash = types.Hash256(contractUnlockConditions(badHostKey, renterKey.UnlockKey()).UnlockHash()) + _, _, err = validateContractRenewal(existing, renewal, hostKey.UnlockKey(), renterKey.UnlockKey(), hostAddress, types.ZeroCurrency, types.ZeroCurrency, pt) + if err == nil || err.Error() != "incorrect unlock hash" { + t.Fatalf("expected unlock hash error, got %v", err) + } + + // original keys + renewal.UnlockHash = types.Hash256(contractUnlockConditions(hostKey.UnlockKey(), renterKey.UnlockKey()).UnlockHash()) + _, _, err = validateContractRenewal(existing, renewal, hostKey.UnlockKey(), renterKey.UnlockKey(), hostAddress, types.ZeroCurrency, types.ZeroCurrency, pt) + if err != nil { + t.Fatal(err) + } + + // different renter key, same host key + newRenterKey := types.NewPrivateKeyFromSeed(frand.Bytes(32)).PublicKey().UnlockKey() + renewal.UnlockHash = types.Hash256(contractUnlockConditions(hostKey.UnlockKey(), newRenterKey).UnlockHash()) + _, _, err = validateContractRenewal(existing, renewal, hostKey.UnlockKey(), newRenterKey, hostAddress, types.ZeroCurrency, types.ZeroCurrency, pt) + if err != nil { + t.Fatal(err) + } +}