diff --git a/go.mod b/go.mod index 138128de..3e1904e4 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/mattn/go-sqlite3 v1.14.24 github.com/shopspring/decimal v1.4.0 - go.sia.tech/core v0.6.2 - go.sia.tech/coreutils v0.7.0 + go.sia.tech/core v0.7.1-0.20241203090808-c6a988d759d6 + go.sia.tech/coreutils v0.7.1-0.20241203172514-7bf95dd18f31 go.sia.tech/jape v0.12.1 go.sia.tech/web/hostd v0.51.0 go.uber.org/goleak v1.3.0 diff --git a/go.sum b/go.sum index c5e2971e..3f29fd10 100644 --- a/go.sum +++ b/go.sum @@ -40,10 +40,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.sia.tech/core v0.6.2 h1:8NEjxyD93A+EhZopsBy/LvuHH+zUSjRNKnf9rXgtIwU= -go.sia.tech/core v0.6.2/go.mod h1:4v+aT/33857tMfqa5j5OYlAoLsoIrd4d7qMlgeP+VGk= -go.sia.tech/coreutils v0.7.0 h1:YpgOUD4vrpDz0KC7FJz+UCOaKaqV5EkX3gMUUmJoz5s= -go.sia.tech/coreutils v0.7.0/go.mod h1:eMoqzqO4opKQ6n9tTTxHQmccfNlj+8RFOYuDSL/Qd4g= +go.sia.tech/core v0.7.1-0.20241203090808-c6a988d759d6 h1:52hztNcOJ+eql7dHMBl+g9VL4Lxr87cUrR9cXrYOkMs= +go.sia.tech/core v0.7.1-0.20241203090808-c6a988d759d6/go.mod h1:4v+aT/33857tMfqa5j5OYlAoLsoIrd4d7qMlgeP+VGk= +go.sia.tech/coreutils v0.7.1-0.20241203172514-7bf95dd18f31 h1:Qskaf8d6oDKG5emNvGHZsd9iZRqz2GeouVNKY5paXlE= +go.sia.tech/coreutils v0.7.1-0.20241203172514-7bf95dd18f31/go.mod h1:d6jrawloc02MCXi/EVc8FIN5h3C6XDiMs4fuFMcU0PU= go.sia.tech/jape v0.12.1 h1:xr+o9V8FO8ScRqbSaqYf9bjj1UJ2eipZuNcI1nYousU= go.sia.tech/jape v0.12.1/go.mod h1:wU+h6Wh5olDjkPXjF0tbZ1GDgoZ6VTi4naFw91yyWC4= go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= diff --git a/host/contracts/manager.go b/host/contracts/manager.go index 25a35adb..97e492c7 100644 --- a/host/contracts/manager.go +++ b/host/contracts/manager.go @@ -184,6 +184,11 @@ func (cm *Manager) ReviseV2Contract(contractID types.FileContractID, revision ty return fmt.Errorf("failed to get existing contract: %w", err) } + // note: not checking status here since that is only changed after the renewal is confirmed. + if existing.RenewedTo != (types.FileContractID{}) { + return errors.New("renewed contracts cannot be revised") + } + oldRoots := cm.getSectorRoots(contractID) // validate the contract revision fields @@ -290,13 +295,10 @@ func (cm *Manager) RenewV2Contract(renewal rhp4.TransactionSet, usage proto4.Usa if err != nil { return fmt.Errorf("failed to get existing contract: %w", err) } - finalRevision := resolution.FinalRevision fc := resolution.NewContract // sanity checks - if finalRevision.RevisionNumber != types.MaxRevisionNumber { - return errors.New("final revision must have max revision number") - } else if fc.Filesize != existing.Filesize { + if fc.Filesize != existing.Filesize { return errors.New("renewal contract must have same file size as existing contract") } else if fc.Capacity != existing.Capacity { return errors.New("renewal contract must have same capacity as existing contract") @@ -320,7 +322,7 @@ func (cm *Manager) RenewV2Contract(renewal rhp4.TransactionSet, usage proto4.Usa Usage: usage, } - if err := cm.store.RenewV2Contract(contract, renewal, existingID, finalRevision, existingRoots); err != nil { + if err := cm.store.RenewV2Contract(contract, renewal, existingID, existingRoots); err != nil { return err } cm.setSectorRoots(contract.ID, existingRoots) diff --git a/host/contracts/manager_test.go b/host/contracts/manager_test.go index a992ee75..ecaa770c 100644 --- a/host/contracts/manager_test.go +++ b/host/contracts/manager_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "strings" "sync" "testing" "time" @@ -1008,38 +1009,35 @@ func TestV2ContractLifecycle(t *testing.T) { cm := node.Chain cs := cm.TipState() - final := fc - final.RevisionNumber = types.MaxRevisionNumber - final.HostSignature = types.Signature{} - final.RenterSignature = types.Signature{} - final.RevisionNumber = types.MaxRevisionNumber additionalCollateral := types.Siacoins(2) renewal := types.V2FileContractRenewal{ - FinalRevision: final, NewContract: types.V2FileContract{ RevisionNumber: 0, Filesize: fc.Filesize, Capacity: fc.Capacity, FileMerkleRoot: fc.FileMerkleRoot, - ProofHeight: final.ProofHeight + 10, - ExpirationHeight: final.ExpirationHeight + 10, - RenterOutput: final.RenterOutput, + ProofHeight: fc.ProofHeight + 10, + ExpirationHeight: fc.ExpirationHeight + 10, + RenterOutput: fc.RenterOutput, HostOutput: types.SiacoinOutput{ - Address: final.HostOutput.Address, - Value: final.HostOutput.Value.Add(additionalCollateral), + Address: fc.HostOutput.Address, + Value: fc.HostOutput.Value.Add(additionalCollateral), }, - MissedHostValue: final.MissedHostValue.Add(additionalCollateral), - TotalCollateral: final.TotalCollateral.Add(additionalCollateral), + MissedHostValue: fc.MissedHostValue.Add(additionalCollateral), + TotalCollateral: fc.TotalCollateral.Add(additionalCollateral), RenterPublicKey: renterKey.PublicKey(), HostPublicKey: hostKey.PublicKey(), }, - HostRollover: final.HostOutput.Value, - RenterRollover: final.RenterOutput.Value, + HostRollover: fc.HostOutput.Value, + RenterRollover: fc.RenterOutput.Value, } renewalSigHash := cs.RenewalSigHash(renewal) renewal.HostSignature = hostKey.SignHash(renewalSigHash) renewal.RenterSignature = renterKey.SignHash(renewalSigHash) + contractSigHash := cs.ContractSigHash(renewal.NewContract) + renewal.NewContract.HostSignature = hostKey.SignHash(contractSigHash) + renewal.NewContract.RenterSignature = renterKey.SignHash(contractSigHash) _, fce, err := com.V2FileContractElement(contractID) if err != nil { @@ -1096,6 +1094,15 @@ func TestV2ContractLifecycle(t *testing.T) { assertContractMetrics(t, types.Siacoins(20), collateral) assertStorageMetrics(t, 1, 1) + // try to revise the original contract before the renewal is confirmed + err = node.Contracts.ReviseV2Contract(contractID, fc, roots, proto4.Usage{ + Storage: cost, + RiskedCollateral: collateral, + }) + if err == nil || !strings.Contains(err.Error(), "renewed contracts cannot be revised") { + t.Fatalf("expected renewal error, got %v", err) + } + // mine to confirm the renewal testutil.MineAndSync(t, node, types.VoidAddress, 1) // new contract pending -> active, old contract active -> renewed @@ -1114,6 +1121,15 @@ func TestV2ContractLifecycle(t *testing.T) { assertContractStatus(t, renewalID, contracts.V2ContractStatusSuccessful) assertContractMetrics(t, types.ZeroCurrency, types.ZeroCurrency) assertStorageMetrics(t, 0, 0) + + // try to revise the original contract after the renewal is confirmed + err = node.Contracts.ReviseV2Contract(contractID, fc, roots, proto4.Usage{ + Storage: cost, + RiskedCollateral: collateral, + }) + if err == nil || !strings.Contains(err.Error(), "renewed contracts cannot be revised") { + t.Fatalf("expected renewal error, got %v", err) + } }) t.Run("reject", func(t *testing.T) { diff --git a/host/contracts/persist.go b/host/contracts/persist.go index 590f602f..03636247 100644 --- a/host/contracts/persist.go +++ b/host/contracts/persist.go @@ -48,7 +48,7 @@ type ( AddV2Contract(V2Contract, rhp4.TransactionSet) error // RenewV2Contract renews a contract. It is expected that the existing // contract will be cleared. - RenewV2Contract(renewal V2Contract, renewalSet rhp4.TransactionSet, renewedID types.FileContractID, clearing types.V2FileContract, roots []types.Hash256) error + RenewV2Contract(renewal V2Contract, renewalSet rhp4.TransactionSet, renewedID types.FileContractID, roots []types.Hash256) error // ReviseV2Contract atomically updates a contract and its associated // sector roots. ReviseV2Contract(id types.FileContractID, revision types.V2FileContract, oldRoots, newRoots []types.Hash256, usage proto4.Usage) error diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index e1c32bcf..79ce43b1 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -86,12 +86,7 @@ func MineBlocks(t *testing.T, cn *ConsensusNode, addr types.Address, n int) { } if b.V2 == nil { - cn.Syncer.BroadcastHeader(gateway.BlockHeader{ - ParentID: b.ParentID, - Nonce: b.Nonce, - Timestamp: b.Timestamp, - MerkleRoot: b.MerkleRoot(), - }) + cn.Syncer.BroadcastHeader(b.Header()) } else { cn.Syncer.BroadcastV2BlockOutline(gateway.OutlineBlock(b, cn.Chain.PoolTransactions(), cn.Chain.V2PoolTransactions())) } diff --git a/persist/sqlite/contracts.go b/persist/sqlite/contracts.go index 34531207..9f9537bc 100644 --- a/persist/sqlite/contracts.go +++ b/persist/sqlite/contracts.go @@ -223,7 +223,7 @@ func (s *Store) AddV2Contract(contract contracts.V2Contract, formationSet rhp4.T // contract's renewed_from field. The old contract's sector roots are // copied to the new contract. The status of the old contract should continue // to be active until the renewal is confirmed -func (s *Store) RenewV2Contract(renewal contracts.V2Contract, renewalSet rhp4.TransactionSet, renewedID types.FileContractID, clearing types.V2FileContract, roots []types.Hash256) error { +func (s *Store) RenewV2Contract(renewal contracts.V2Contract, renewalSet rhp4.TransactionSet, renewedID types.FileContractID, roots []types.Hash256) error { return s.transaction(func(tx *txn) error { // add the new contract renewedDBID, err := insertV2Contract(tx, renewal, renewalSet) @@ -231,7 +231,7 @@ func (s *Store) RenewV2Contract(renewal contracts.V2Contract, renewalSet rhp4.Tr return fmt.Errorf("failed to insert renewed contract: %w", err) } - clearedDBID, err := updateResolvedV2Contract(tx, renewedID, clearing, renewedDBID) + clearedDBID, err := updateResolvedV2Contract(tx, renewedID, renewedDBID) if err != nil { return fmt.Errorf("failed to resolve existing contract: %w", err) } @@ -737,12 +737,10 @@ RETURNING sector_id;` } // updateResolvedV2Contract clears a contract and returns its ID -func updateResolvedV2Contract(tx *txn, contractID types.FileContractID, clearing types.V2FileContract, renewedDBID int64) (dbID int64, err error) { - const clearQuery = `UPDATE contracts_v2 SET (renewed_to, revision_number, raw_revision) = ($1, $2, $3) WHERE contract_id=$4 RETURNING id;` +func updateResolvedV2Contract(tx *txn, contractID types.FileContractID, renewedDBID int64) (dbID int64, err error) { + const clearQuery = `UPDATE contracts_v2 SET renewed_to=$1 WHERE contract_id=$2 RETURNING id;` err = tx.QueryRow(clearQuery, renewedDBID, - encode(clearing.RevisionNumber), - encode(clearing), encode(contractID), ).Scan(&dbID) return