diff --git a/go.mod b/go.mod index 66291cef..c50df739 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.4.8-0.20240926222149-2c8b541119dc - go.sia.tech/coreutils v0.4.1 + go.sia.tech/core v0.6.1 + go.sia.tech/coreutils v0.6.0 go.sia.tech/jape v0.12.1 go.sia.tech/web/hostd v0.49.0 go.uber.org/goleak v1.3.0 diff --git a/go.sum b/go.sum index 817b3533..f35bb48e 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.4.8-0.20240926222149-2c8b541119dc h1:+hCcYky+23HtiAnirXsq0U/NaCt1WuIu308lmfTtJNM= -go.sia.tech/core v0.4.8-0.20240926222149-2c8b541119dc/go.mod h1:j2Ke8ihV8or7d2VDrFZWcCkwSVHO0DNMQJAGs9Qop2M= -go.sia.tech/coreutils v0.4.1 h1:ExQ9g6EtnFe70ptNBG+OtZyFU3aBoEzE/06rtbN6f4c= -go.sia.tech/coreutils v0.4.1/go.mod h1:v60kPqZERsb1ZS0PVe4S8hr2ArNEwTdp7XTzErXnV2U= +go.sia.tech/core v0.6.1 h1:eaExM2E2eNr43su2XDkY5J24E3F54YGS7hcC3WtVjVk= +go.sia.tech/core v0.6.1/go.mod h1:P3C1BWa/7J4XgdzWuaYHBvLo2RzZ0UBaJM4TG1GWB2g= +go.sia.tech/coreutils v0.6.0 h1:r0IZt+aVdGG2uIHl7OtaWRYdVx4NQ7ezRoSGa0Ej8GY= +go.sia.tech/coreutils v0.6.0/go.mod h1:XlsnogeYU/Tdjzp/HUNAj5T7tZCdmeBHIBjymbPC+uQ= 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/integrity_test.go b/host/contracts/integrity_test.go index 341af351..7efb2a89 100644 --- a/host/contracts/integrity_test.go +++ b/host/contracts/integrity_test.go @@ -81,7 +81,7 @@ func TestCheckIntegrity(t *testing.T) { } // mine enough for the wallet to have some funds - mineAndSync(t, cm, db, wm.Address(), 150) + mineAndSync(t, cm, db, wm.Address(), cm.Tip().Network.MaturityDelay+5) rev, err := formContract(renterKey, hostKey, 50, 60, types.Siacoins(500), types.Siacoins(1000), c, node, node.ChainManager(), node.TPool()) if err != nil { diff --git a/host/contracts/manager.go b/host/contracts/manager.go index cedb2747..1053ff90 100644 --- a/host/contracts/manager.go +++ b/host/contracts/manager.go @@ -355,14 +355,12 @@ func (cm *Manager) RenewV2Contract(renewal V2FormationTransactionSet, usage V2Us fc := resolution.NewContract // sanity checks - if finalRevision.FileMerkleRoot != (types.Hash256{}) { - return errors.New("existing contract must be cleared") - } else if finalRevision.Filesize != 0 { - return errors.New("existing contract must be cleared") - } else if finalRevision.RevisionNumber != types.MaxRevisionNumber { + if finalRevision.RevisionNumber != types.MaxRevisionNumber { return errors.New("existing contract must be cleared") } else 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") } else if fc.FileMerkleRoot != existing.FileMerkleRoot { return errors.New("renewal root does not match existing roots") } diff --git a/host/contracts/manager_test.go b/host/contracts/manager_test.go index 47c6c65c..9f91ebc3 100644 --- a/host/contracts/manager_test.go +++ b/host/contracts/manager_test.go @@ -10,6 +10,7 @@ import ( "time" rhp2 "go.sia.tech/core/rhp/v2" + rhp4 "go.sia.tech/core/rhp/v4" "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" "go.sia.tech/coreutils/syncer" @@ -35,6 +36,7 @@ func formV2Contract(t *testing.T, cm *chain.Manager, c *contracts.Manager, w *wa fc := types.V2FileContract{ RevisionNumber: 0, Filesize: 0, + Capacity: 0, FileMerkleRoot: types.Hash256{}, ProofHeight: cs.Index.Height + duration, ExpirationHeight: cs.Index.Height + duration + 10, @@ -227,7 +229,7 @@ func TestContractLifecycle(t *testing.T) { network, genesis := testutil.V1Network() node := testutil.NewHostNode(t, hostKey, network, genesis, log) - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) cm := node.Chain c := node.Contracts @@ -287,7 +289,7 @@ func TestContractLifecycle(t *testing.T) { network, genesis := testutil.V1Network() node := testutil.NewHostNode(t, hostKey, network, genesis, log) - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) rev := formContract(t, node.Chain, node.Contracts, node.Wallet, node.Syncer, renterKey, hostKey, types.Siacoins(10), types.Siacoins(20), 10, false) assertContractStatus(t, node.Contracts, rev.Revision.ParentID, contracts.ContractStatusPending) @@ -325,7 +327,7 @@ func TestContractLifecycle(t *testing.T) { t.Fatal(err) } - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) renterFunds := types.Siacoins(500) hostCollateral := types.Siacoins(1000) @@ -437,7 +439,7 @@ func TestContractLifecycle(t *testing.T) { t.Fatal(err) } - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) renterFunds := types.Siacoins(500) hostCollateral := types.Siacoins(1000) @@ -531,7 +533,7 @@ func TestContractLifecycle(t *testing.T) { t.Fatal(err) } - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) renterFunds := types.Siacoins(500) hostCollateral := types.Siacoins(1000) @@ -610,7 +612,7 @@ func TestContractLifecycle(t *testing.T) { t.Fatal(err) } - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) renterFunds := types.Siacoins(500) hostCollateral := types.Siacoins(1000) @@ -723,7 +725,7 @@ func TestV2ContractLifecycle(t *testing.T) { } // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) assertContractStatus := func(t *testing.T, contractID types.FileContractID, status contracts.V2ContractStatus) { t.Helper() @@ -851,8 +853,9 @@ func TestV2ContractLifecycle(t *testing.T) { } defer release() - fc.Filesize = rhp2.SectorSize - fc.FileMerkleRoot = rhp2.MetaRoot(roots) + fc.Filesize = rhp4.SectorSize + fc.Capacity = rhp4.SectorSize + fc.FileMerkleRoot = rhp4.MetaRoot(roots) fc.RevisionNumber++ // transfer some funds from the renter to the host cost, collateral := types.Siacoins(1), types.Siacoins(2) @@ -910,8 +913,9 @@ func TestV2ContractLifecycle(t *testing.T) { } defer release() - fc.Filesize = rhp2.SectorSize - fc.FileMerkleRoot = rhp2.MetaRoot(roots) + fc.Filesize = rhp4.SectorSize + fc.Capacity = rhp4.SectorSize + fc.FileMerkleRoot = rhp4.MetaRoot(roots) fc.RevisionNumber++ // transfer some funds from the renter to the host cost, collateral := types.Siacoins(1), types.Siacoins(2) @@ -969,8 +973,9 @@ func TestV2ContractLifecycle(t *testing.T) { } defer release() - fc.Filesize = rhp2.SectorSize - fc.FileMerkleRoot = rhp2.MetaRoot(roots) + fc.Filesize = rhp4.SectorSize + fc.Capacity = rhp4.SectorSize + fc.FileMerkleRoot = rhp4.MetaRoot(roots) fc.RevisionNumber++ // transfer some funds from the renter to the host cost, collateral := types.Siacoins(1), types.Siacoins(2) @@ -1006,8 +1011,6 @@ func TestV2ContractLifecycle(t *testing.T) { cs := cm.TipState() final := fc final.RevisionNumber = types.MaxRevisionNumber - final.FileMerkleRoot = types.Hash256{} - final.Filesize = 0 final.HostSignature = types.Signature{} final.RenterSignature = types.Signature{} final.RevisionNumber = types.MaxRevisionNumber @@ -1018,6 +1021,7 @@ func TestV2ContractLifecycle(t *testing.T) { NewContract: types.V2FileContract{ RevisionNumber: 0, Filesize: fc.Filesize, + Capacity: fc.Capacity, FileMerkleRoot: fc.FileMerkleRoot, ProofHeight: final.ProofHeight + 10, ExpirationHeight: final.ExpirationHeight + 10, @@ -1124,6 +1128,7 @@ func TestV2ContractLifecycle(t *testing.T) { fc := types.V2FileContract{ RevisionNumber: 0, Filesize: 0, + Capacity: 0, FileMerkleRoot: types.Hash256{}, ProofHeight: cs.Index.Height + duration, ExpirationHeight: cs.Index.Height + duration + 10, @@ -1198,7 +1203,7 @@ func TestSectorRoots(t *testing.T) { t.Fatal(err) } - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) // create a fake volume so disk space is not used id, err := node.Store.AddVolume("test", false) diff --git a/host/contracts/update.go b/host/contracts/update.go index 49f5d9ba..53246eb0 100644 --- a/host/contracts/update.go +++ b/host/contracts/update.go @@ -7,6 +7,7 @@ import ( rhp2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" + "go.sia.tech/coreutils/wallet" "go.uber.org/zap" ) @@ -58,12 +59,9 @@ type ( // An UpdateStateTx atomically updates the state of contracts in the contract // store. UpdateStateTx interface { - // ContractStateElements returns all state elements from the contract - // store - ContractStateElements() ([]types.StateElement, error) - // UpdateContractStateElements updates the state elements in the host + // UpdateContractElementProofs updates the state elements in the host // contract store - UpdateContractStateElements([]types.StateElement) error + UpdateContractElementProofs(wallet.ProofUpdater) error // ContractRelevant returns whether the contract with the provided id is // relevant to the host ContractRelevant(id types.FileContractID) (bool, error) @@ -81,18 +79,18 @@ type ( // been confirmed to rejected RejectContracts(height uint64) (v1, v2 []types.FileContractID, err error) - // ContractChainIndexElements returns all chain index elements from the - // contract store - ContractChainIndexElements() (elements []types.ChainIndexElement, err error) - // ApplyContractChainIndexElements adds or updates the merkle proof of + // AddContractChainIndexElement adds or updates the merkle proof of // chain index state elements - ApplyContractChainIndexElements(elements []types.ChainIndexElement) error + AddContractChainIndexElement(elements types.ChainIndexElement) error // RevertContractChainIndexElements removes chain index state elements // that were reverted RevertContractChainIndexElement(types.ChainIndex) error + // UpdateChainIndexElementProofs returns all chain index elements from the + // contract store + UpdateChainIndexElementProofs(wallet.ProofUpdater) error // DeleteExpiredContractChainIndexElements deletes chain index state // elements that are no long necessary - DeleteExpiredContractChainIndexElements(height uint64) error + DeleteExpiredChainIndexElements(height uint64) error } ) @@ -533,20 +531,6 @@ func buildContractState(tx UpdateStateTx, u stateUpdater, revert bool, log *zap. func (cm *Manager) UpdateChainState(tx UpdateStateTx, reverted []chain.RevertUpdate, applied []chain.ApplyUpdate) error { log := cm.log.Named("updateChainState") - chainElements, err := tx.ContractChainIndexElements() - if err != nil { - return fmt.Errorf("failed to get chain index state elements: %w", err) - } - - v2ContractStateElements, err := tx.ContractStateElements() - if err != nil { - return fmt.Errorf("failed to get contract state elements: %w", err) - } - v2ContractElementMap := make(map[types.Hash256]*types.StateElement, len(v2ContractStateElements)) - for _, ele := range v2ContractStateElements { - v2ContractElementMap[ele.ID] = &ele - } - for _, cru := range reverted { revertedIndex := types.ChainIndex{ ID: cru.Block.ID(), @@ -557,32 +541,12 @@ func (cm *Manager) UpdateChainState(tx UpdateStateTx, reverted []chain.RevertUpd state := buildContractState(tx, cru, true, log.Named("revert").With(zap.Stringer("index", revertedIndex))) if err := tx.RevertContracts(revertedIndex, state); err != nil { return fmt.Errorf("failed to revert contracts: %w", err) - } - - // delete reverted contract state elements from the map - for _, reverted := range state.ConfirmedV2 { - delete(v2ContractElementMap, reverted.ID) - } - // update remaining contract state elements - for key := range v2ContractElementMap { - cru.UpdateElementProof(v2ContractElementMap[key]) - } - - // revert contract chain index element - if err := tx.RevertContractChainIndexElement(revertedIndex); err != nil { + } else if err := tx.RevertContractChainIndexElement(revertedIndex); err != nil { return fmt.Errorf("failed to revert chain index state element: %w", err) - } - - // update chain state elements - if len(chainElements) > 0 { - last := chainElements[len(chainElements)-1] - if last.ChainIndex != revertedIndex { - panic(fmt.Errorf("unexpected chain index: %v != %v", last.ChainIndex, revertedIndex)) // developer error - } - chainElements = chainElements[:len(chainElements)-1] - for i := range chainElements { - cru.UpdateElementProof(&chainElements[i].StateElement) - } + } else if err := tx.UpdateChainIndexElementProofs(cru); err != nil { + return fmt.Errorf("failed to update chain index elements: %w", err) + } else if err := tx.UpdateContractElementProofs(cru); err != nil { + return fmt.Errorf("failed to update contract element proofs: %w", err) } } @@ -593,23 +557,12 @@ func (cm *Manager) UpdateChainState(tx UpdateStateTx, reverted []chain.RevertUpd return fmt.Errorf("failed to revert contracts: %w", err) } - // update existing contract state elements - for id := range v2ContractElementMap { - cau.UpdateElementProof(v2ContractElementMap[id]) - } - // add new contract state elements - for _, applied := range state.ConfirmedV2 { - v2ContractElementMap[applied.ID] = &applied.StateElement - } - - // update existing chain index elements proofs - for i := range chainElements { - cau.UpdateElementProof(&chainElements[i].StateElement) - } - // add new chain index element - chainElements = append(chainElements, cau.ChainIndexElement()) - if len(chainElements) > chainIndexBuffer { - chainElements = chainElements[len(chainElements)-chainIndexBuffer:] + if err := tx.UpdateChainIndexElementProofs(cau); err != nil { + return fmt.Errorf("failed to update chain index elements: %w", err) + } else if err := tx.UpdateContractElementProofs(cau); err != nil { + return fmt.Errorf("failed to update contract element proofs: %w", err) + } else if err := tx.AddContractChainIndexElement(cau.ChainIndexElement()); err != nil { + return fmt.Errorf("failed to add chain index state element: %w", err) } // reject any contracts that have not been confirmed after the reject buffer @@ -632,28 +585,10 @@ func (cm *Manager) UpdateChainState(tx UpdateStateTx, reverted []chain.RevertUpd // delete any chain index elements outside of the proof window buffer if cau.State.Index.Height > chainIndexBuffer { minHeight := cau.State.Index.Height - chainIndexBuffer - if err := tx.DeleteExpiredContractChainIndexElements(minHeight); err != nil { + if err := tx.DeleteExpiredChainIndexElements(minHeight); err != nil { return fmt.Errorf("failed to delete expired chain index elements: %w", err) } } } - - // update chain index state elements - if len(chainElements) > 0 { - if err := tx.ApplyContractChainIndexElements(chainElements); err != nil { - return fmt.Errorf("failed to update chain index state elements: %w", err) - } - } - - // update contract state elements - if len(v2ContractElementMap) > 0 { - contractStateElements := make([]types.StateElement, 0, len(v2ContractElementMap)) - for _, ele := range v2ContractElementMap { - contractStateElements = append(contractStateElements, *ele) - } - if err := tx.UpdateContractStateElements(contractStateElements); err != nil { - return fmt.Errorf("failed to update contract state elements: %w", err) - } - } return nil } diff --git a/host/settings/announce.go b/host/settings/announce.go index f3196e8a..5d1a7cfb 100644 --- a/host/settings/announce.go +++ b/host/settings/announce.go @@ -8,6 +8,7 @@ import ( "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" + rhp4 "go.sia.tech/coreutils/rhp/v4" "go.uber.org/zap" ) @@ -32,17 +33,15 @@ func (m *ConfigManager) Announce() error { minerFee := m.chain.RecommendedFee().Mul64(announcementTxnSize) - ha := chain.HostAnnouncement{ - PublicKey: m.hostKey.PublicKey(), - NetAddress: settings.NetAddress, - } - cs := m.chain.TipState() if cs.Index.Height < cs.Network.HardforkV2.AllowHeight { // create a transaction with an announcement txn := types.Transaction{ ArbitraryData: [][]byte{ - ha.ToArbitraryData(m.hostKey), + chain.HostAnnouncement{ + PublicKey: m.hostKey.PublicKey(), + NetAddress: settings.NetAddress, + }.ToArbitraryData(m.hostKey), }, MinerFees: []types.Currency{minerFee}, } @@ -64,7 +63,9 @@ func (m *ConfigManager) Announce() error { // create a v2 transaction with an announcement txn := types.V2Transaction{ Attestations: []types.Attestation{ - ha.ToAttestation(cs, m.hostKey), + chain.V2HostAnnouncement{ + {Protocol: rhp4.ProtocolTCPSiaMux, Address: settings.NetAddress}, // TODO: this isn't correct + }.ToAttestation(cs, m.hostKey), }, MinerFee: minerFee, } diff --git a/host/settings/announce_test.go b/host/settings/announce_test.go index 6727964a..e6545997 100644 --- a/host/settings/announce_test.go +++ b/host/settings/announce_test.go @@ -4,6 +4,8 @@ import ( "testing" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" + rhp4 "go.sia.tech/coreutils/rhp/v4" "go.sia.tech/coreutils/wallet" "go.sia.tech/hostd/host/contracts" "go.sia.tech/hostd/host/settings" @@ -40,7 +42,13 @@ func TestAutoAnnounce(t *testing.T) { } defer contracts.Close() - sm, err := settings.NewConfigManager(hostKey, node.Store, node.Chain, node.Syncer, wm, settings.WithLog(log.Named("settings")), settings.WithAnnounceInterval(50), settings.WithValidateNetAddress(false)) + storage, err := storage.NewVolumeManager(node.Store) + if err != nil { + t.Fatal("failed to create storage manager:", err) + } + defer storage.Close() + + sm, err := settings.NewConfigManager(hostKey, node.Store, node.Chain, node.Syncer, wm, settings.WithLog(log.Named("settings")), settings.WithAnnounceInterval(50)) if err != nil { t.Fatal(err) } @@ -52,10 +60,6 @@ func TestAutoAnnounce(t *testing.T) { } defer idx.Close() - // fund the wallet - testutil.MineBlocks(t, node, wm.Address(), 150) - testutil.WaitForSync(t, node.Chain, idx) - settings := settings.DefaultSettings settings.NetAddress = "foo.bar:1234" sm.UpdateSettings(settings) @@ -65,7 +69,7 @@ func TestAutoAnnounce(t *testing.T) { index, ok := node.Chain.BestIndex(height) if !ok { - t.Fatal("failed to get index") + t.Fatalf("failed to get index at height %v (%s)", height, node.Chain.Tip()) } ann, err := sm.LastAnnouncement() @@ -78,44 +82,73 @@ func TestAutoAnnounce(t *testing.T) { } } + assertV2Announcement := func(t *testing.T, expectedAddr string, height uint64) { + t.Helper() + + index, ok := node.Chain.BestIndex(height) + if !ok { + t.Fatal("failed to get index") + } + + hash, announceIndex, err := node.Store.LastV2AnnouncementHash() + if err != nil { + t.Fatal(err) + } + + h := types.NewHasher() + types.EncodeSlice(h.E, chain.V2HostAnnouncement{{Protocol: rhp4.ProtocolTCPSiaMux, Address: expectedAddr}}) + if err := h.E.Flush(); err != nil { + t.Fatal(err) + } + expectedHash := h.Sum() + + if hash != expectedHash { + t.Fatalf("expected hash %v, got %v", expectedHash, hash) + } else if announceIndex != index { + t.Fatalf("expected index %v, got %v", index, announceIndex) + } + } + // helper that mines blocks and waits for them to be processed before mining // the next one. This is necessary because test blocks can be extremely fast // and the host may not have time to process the broadcast before the next // block is mined. - mineAndSync := func(t *testing.T, numBlocks int) { + mineAndSync := func(t *testing.T, numBlocks uint64) { t.Helper() // waits for each block to be processed before mining the next one - for i := 0; i < numBlocks; i++ { + for i := uint64(0); i < numBlocks; i++ { testutil.MineBlocks(t, node, wm.Address(), 1) testutil.WaitForSync(t, node.Chain, idx) } } - // trigger an auto-announce - mineAndSync(t, 2) - assertAnnouncement(t, "foo.bar:1234", 152) + // fund the wallet and trigger the first auto-announce + mineAndSync(t, network.MaturityDelay+1+1) + assertAnnouncement(t, "foo.bar:1234", network.MaturityDelay+1+1) // first maturity height + funds available + confirmation // mine until the next announcement and confirm it + lastHeight := node.Chain.Tip().Height mineAndSync(t, 51) - assertAnnouncement(t, "foo.bar:1234", 203) // 152 (first confirm) + 50 (interval) + 1 (confirmation) + assertAnnouncement(t, "foo.bar:1234", lastHeight+50+1) // first confirm + interval + confirmation // change the address settings.NetAddress = "baz.qux:5678" sm.UpdateSettings(settings) // trigger and confirm the new announcement + lastHeight = node.Chain.Tip().Height mineAndSync(t, 2) - assertAnnouncement(t, "baz.qux:5678", 205) + assertAnnouncement(t, "baz.qux:5678", lastHeight+2) // mine until the v2 hardfork activates. The host should re-announce with a // v2 attestation. n := node.Chain.TipState().Network - mineAndSync(t, int(n.HardforkV2.AllowHeight-node.Chain.Tip().Height)+1) - assertAnnouncement(t, "baz.qux:5678", n.HardforkV2.AllowHeight+1) + mineAndSync(t, n.HardforkV2.AllowHeight-node.Chain.Tip().Height+1) + assertV2Announcement(t, "baz.qux:5678", n.HardforkV2.AllowHeight+1) // mine a few more blocks to ensure the host doesn't re-announce mineAndSync(t, 10) - assertAnnouncement(t, "baz.qux:5678", n.HardforkV2.AllowHeight+1) + assertV2Announcement(t, "baz.qux:5678", n.HardforkV2.AllowHeight+1) } func TestAutoAnnounceV2(t *testing.T) { @@ -147,6 +180,12 @@ func TestAutoAnnounceV2(t *testing.T) { } defer contracts.Close() + storage, err := storage.NewVolumeManager(node.Store) + if err != nil { + t.Fatal("failed to create storage manager:", err) + } + defer storage.Close() + sm, err := settings.NewConfigManager(hostKey, node.Store, node.Chain, node.Syncer, wm, settings.WithLog(log.Named("settings")), settings.WithAnnounceInterval(50)) if err != nil { t.Fatal(err) @@ -159,15 +198,21 @@ func TestAutoAnnounceV2(t *testing.T) { } defer idx.Close() - // fund the wallet - testutil.MineBlocks(t, node, wm.Address(), 150) - testutil.WaitForSync(t, node.Chain, idx) + // helper that mines blocks and waits for them to be processed before mining + // the next one. This is necessary because test blocks can be extremely fast + // and the host may not have time to process the broadcast before the next + // block is mined. + mineAndSync := func(t *testing.T, numBlocks uint64) { + t.Helper() - settings := settings.DefaultSettings - settings.NetAddress = "foo.bar:1234" - sm.UpdateSettings(settings) + // waits for each block to be processed before mining the next one + for i := uint64(0); i < numBlocks; i++ { + testutil.MineBlocks(t, node, wm.Address(), 1) + testutil.WaitForSync(t, node.Chain, idx) + } + } - assertAnnouncement := func(t *testing.T, expectedAddr string, height uint64) { + assertV2Announcement := func(t *testing.T, expectedAddr string, height uint64) { t.Helper() index, ok := node.Chain.BestIndex(height) @@ -175,42 +220,43 @@ func TestAutoAnnounceV2(t *testing.T) { t.Fatal("failed to get index") } - ann, err := sm.LastAnnouncement() + hash, announceIndex, err := node.Store.LastV2AnnouncementHash() if err != nil { t.Fatal(err) - } else if ann.Address != expectedAddr { - t.Fatalf("expected address %q, got %q", expectedAddr, ann.Address) - } else if ann.Index != index { - t.Fatalf("expected index %q, got %q", index, ann.Index) } - } - // helper that mines blocks and waits for them to be processed before mining - // the next one. This is necessary because test blocks can be extremely fast - // and the host may not have time to process the broadcast before the next - // block is mined. - mineAndSync := func(t *testing.T, numBlocks int) { - t.Helper() + h := types.NewHasher() + types.EncodeSlice(h.E, chain.V2HostAnnouncement{{Protocol: rhp4.ProtocolTCPSiaMux, Address: expectedAddr}}) + if err := h.E.Flush(); err != nil { + t.Fatal(err) + } + expectedHash := h.Sum() - // waits for each block to be processed before mining the next one - for i := 0; i < numBlocks; i++ { - testutil.MineBlocks(t, node, wm.Address(), 1) - testutil.WaitForSync(t, node.Chain, idx) + if hash != expectedHash { + t.Fatalf("expected hash %v, got %v", expectedHash, hash) + } else if announceIndex != index { + t.Fatalf("expected index %v, got %v", index, announceIndex) } } - // trigger an auto-announce - mineAndSync(t, 2) - assertAnnouncement(t, "foo.bar:1234", 152) + settings := settings.DefaultSettings + settings.NetAddress = "foo.bar:1234" + sm.UpdateSettings(settings) + + // fund the wallet and trigger the first auto-announce + mineAndSync(t, network.MaturityDelay+1+1) + assertV2Announcement(t, "foo.bar:1234", network.MaturityDelay+1+1) // first maturity height + funds available + confirmation // mine until the next announcement and confirm it + lastHeight := node.Chain.Tip().Height mineAndSync(t, 51) - assertAnnouncement(t, "foo.bar:1234", 203) // 152 (first confirm) + 50 (interval) + 1 (confirmation) + assertV2Announcement(t, "foo.bar:1234", lastHeight+50+1) // first confirm + interval + confirmation // change the address settings.NetAddress = "baz.qux:5678" sm.UpdateSettings(settings) // trigger and confirm the new announcement + lastHeight = node.Chain.Tip().Height mineAndSync(t, 2) - assertAnnouncement(t, "baz.qux:5678", 205) + assertV2Announcement(t, "baz.qux:5678", lastHeight+2) } diff --git a/host/settings/settings.go b/host/settings/settings.go index 8e957a9c..104e86ed 100644 --- a/host/settings/settings.go +++ b/host/settings/settings.go @@ -39,6 +39,8 @@ type ( UpdateSettings(s Settings) error LastAnnouncement() (Announcement, error) + // LastV2AnnouncementHash returns the hash of the last v2 announcement. + LastV2AnnouncementHash() (types.Hash256, types.ChainIndex, error) } // ChainManager defines the interface required by the contract manager to diff --git a/host/settings/update.go b/host/settings/update.go index cf6f720c..7202899a 100644 --- a/host/settings/update.go +++ b/host/settings/update.go @@ -5,6 +5,7 @@ import ( "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" + rhp4 "go.sia.tech/coreutils/rhp/v4" "go.uber.org/zap" ) @@ -14,6 +15,13 @@ type UpdateStateTx interface { LastAnnouncement() (Announcement, error) RevertLastAnnouncement() error SetLastAnnouncement(Announcement) error + + // LastV2AnnouncementHash returns the hash of the last v2 announcement. + LastV2AnnouncementHash() (types.Hash256, types.ChainIndex, error) + // RevertLastV2Announcement reverts the last v2 announcement. + RevertLastV2Announcement() error + // SetLastV2Announcement sets the last v2 announcement. + SetLastV2AnnouncementHash(types.Hash256, types.ChainIndex) error } // UpdateChainState updates the host's announcement state based on the given @@ -25,67 +33,114 @@ func (cm *ConfigManager) UpdateChainState(tx UpdateStateTx, reverted []chain.Rev return fmt.Errorf("failed to get last announcement: %w", err) } + _, v2AnnouncementIndex, err := tx.LastV2AnnouncementHash() + if err != nil { + return fmt.Errorf("failed to get last v2 announcement: %w", err) + } + for _, cru := range reverted { if cru.State.Index == lastAnnouncement.Index { if err := tx.RevertLastAnnouncement(); err != nil { return fmt.Errorf("failed to revert last announcement: %w", err) } } + + if cru.State.Index == v2AnnouncementIndex { + if err := tx.RevertLastV2Announcement(); err != nil { + return fmt.Errorf("failed to revert last v2 announcement: %w", err) + } + } } - var nextAnnouncement *Announcement + var announcement Announcement + var v2AnnounceAddresses []chain.NetAddress + var v2AnnounceIndex types.ChainIndex for _, cau := range applied { index := cau.State.Index - chain.ForEachHostAnnouncement(cau.Block, func(announcement chain.HostAnnouncement) { - if announcement.PublicKey != pk { + chain.ForEachHostAnnouncement(cau.Block, func(a chain.HostAnnouncement) { + if a.PublicKey != pk { return } - nextAnnouncement = &Announcement{ - Address: announcement.NetAddress, + announcement = Announcement{ + Address: a.NetAddress, Index: index, } }) + + chain.ForEachV2HostAnnouncement(cau.Block, func(hostKey types.PublicKey, addresses []chain.NetAddress) { + if hostKey != pk { + return + } + + v2AnnounceAddresses = addresses + v2AnnounceIndex = index + }) } - if nextAnnouncement == nil { - return nil + if announcement.Index != (types.ChainIndex{}) { + if err := tx.SetLastAnnouncement(announcement); err != nil { + return fmt.Errorf("failed to set last announcement: %w", err) + } + cm.log.Debug("announcement confirmed", zap.String("netaddress", announcement.Address), zap.Stringer("index", announcement.Index)) } - if err := tx.SetLastAnnouncement(*nextAnnouncement); err != nil { - return fmt.Errorf("failed to set last announcement: %w", err) + if len(v2AnnounceAddresses) > 0 { + h := types.NewHasher() + types.EncodeSlice(h.E, v2AnnounceAddresses) + if err := h.E.Flush(); err != nil { + return fmt.Errorf("failed to hash v2 announcement addresses: %w", err) + } else if err := tx.SetLastV2AnnouncementHash(h.Sum(), v2AnnounceIndex); err != nil { + return fmt.Errorf("failed to set last v2 announcement: %w", err) + } + + addresses := make([]string, 0, len(v2AnnounceAddresses)) + for _, addr := range v2AnnounceAddresses { + addresses = append(addresses, fmt.Sprintf("%s/%s", addr.Protocol, addr.Address)) // TODO: Stringer? + } + cm.log.Debug("v2 announcement confirmed", zap.Strings("addresses", addresses), zap.Stringer("index", v2AnnounceIndex)) } - cm.log.Debug("announcement confirmed", zap.String("netaddress", nextAnnouncement.Address), zap.Stringer("index", nextAnnouncement.Index)) return nil } // ProcessActions processes announcement actions based on the given chain index. func (m *ConfigManager) ProcessActions(index types.ChainIndex) error { - announcement, err := m.store.LastAnnouncement() - if err != nil { - return fmt.Errorf("failed to get last announcement: %w", err) - } + n := m.chain.TipState().Network - nextHeight := announcement.Index.Height + m.announceInterval - netaddress := m.Settings().NetAddress - if err := validateNetAddress(netaddress); err != nil { - if m.validateNetAddress { + var shouldAnnounce bool + if index.Height < n.HardforkV2.AllowHeight { + announcement, err := m.store.LastAnnouncement() + if err != nil { + return fmt.Errorf("failed to get last announcement: %w", err) + } + + nextHeight := announcement.Index.Height + m.announceInterval + netaddress := m.Settings().NetAddress + if err := validateNetAddress(netaddress); err != nil && m.validateNetAddress { + m.log.Warn("invalid net address", zap.String("address", netaddress), zap.Error(err)) return nil } - } + shouldAnnounce = index.Height >= nextHeight || announcement.Address != netaddress + } else { + announceHash, announceIndex, err := m.store.LastV2AnnouncementHash() + if err != nil { + return fmt.Errorf("failed to get last v2 announcement: %w", err) + } - // check if a new announcement is needed - n := m.chain.TipState().Network - // re-announce if the v2 hardfork has activated and the last announcement was before activation - reannounceV2 := index.Height >= n.HardforkV2.AllowHeight && announcement.Index.Height < n.HardforkV2.AllowHeight - if !reannounceV2 && index.Height < nextHeight && announcement.Address == netaddress { - return nil + nextHeight := announceIndex.Height + m.announceInterval + h := types.NewHasher() + types.EncodeSlice(h.E, chain.V2HostAnnouncement{{Protocol: rhp4.ProtocolTCPSiaMux, Address: m.Settings().NetAddress}}) + if err := h.E.Flush(); err != nil { + return fmt.Errorf("failed to hash v2 announcement: %w", err) + } + shouldAnnounce = index.Height >= nextHeight || announceHash != h.Sum() } - // re-announce - if err := m.Announce(); err != nil { - m.log.Warn("failed to announce", zap.Error(err)) + if shouldAnnounce { + if err := m.Announce(); err != nil { + m.log.Warn("failed to announce", zap.Error(err)) + } } return nil } diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index ee7439aa..83e7f747 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -13,6 +13,7 @@ import ( "go.sia.tech/coreutils" "go.sia.tech/coreutils/chain" "go.sia.tech/coreutils/syncer" + "go.sia.tech/coreutils/testutil" "go.sia.tech/coreutils/wallet" "go.sia.tech/hostd/host/accounts" "go.sia.tech/hostd/host/contracts" @@ -51,35 +52,13 @@ type ( // V1Network is a test helper that returns a consensus.Network and genesis block // suited for testing the v1 network func V1Network() (*consensus.Network, types.Block) { - // use a modified version of Zen - n, genesisBlock := chain.TestnetZen() - n.InitialTarget = types.BlockID{0xFF} - n.HardforkDevAddr.Height = 1 - n.HardforkTax.Height = 1 - n.HardforkStorageProof.Height = 1 - n.HardforkOak.Height = 1 - n.HardforkASIC.Height = 1 - n.HardforkFoundation.Height = 1 - n.HardforkV2.AllowHeight = 500 // comfortably above MaturityHeight - n.HardforkV2.RequireHeight = 600 - return n, genesisBlock + return testutil.Network() } // V2Network is a test helper that returns a consensus.Network and genesis block // suited for testing after the v2 hardfork func V2Network() (*consensus.Network, types.Block) { - // use a modified version of Zen - n, genesisBlock := chain.TestnetZen() - n.InitialTarget = types.BlockID{0xFF} - n.HardforkDevAddr.Height = 1 - n.HardforkTax.Height = 1 - n.HardforkStorageProof.Height = 1 - n.HardforkOak.Height = 1 - n.HardforkASIC.Height = 1 - n.HardforkFoundation.Height = 1 - n.HardforkV2.AllowHeight = 145 // just above the maturity height - n.HardforkV2.RequireHeight = 180 - return n, genesisBlock + return testutil.V2Network() } // WaitForSync is a helper to wait for the chain and indexer to sync diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 38608d34..4cf40f48 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -35,6 +35,16 @@ type ( Usage contracts.V2Usage Status contracts.V2ContractStatus } + + stateElement struct { + ID types.Hash256 + types.StateElement + } + + contractStateElement struct { + ID int64 + types.StateElement + } ) var _ index.UpdateTx = (*updateTx)(nil) @@ -58,49 +68,168 @@ UPDATE global_settings SET last_scanned_index=NULL, last_announce_index=NULL, la }) } -// WalletStateElements returns all state elements related to the wallet. It is used -// to update the proofs of all state elements affected by the update. -func (ux *updateTx) WalletStateElements() (elements []types.StateElement, err error) { - const query = `SELECT id, merkle_proof, leaf_index FROM wallet_siacoin_elements` - rows, err := ux.tx.Query(query) +func getSiacoinStateElements(tx *txn) (elements []stateElement, err error) { + const query = `SELECT id, leaf_index, merkle_proof FROM wallet_siacoin_elements` + rows, err := tx.Query(query) if err != nil { - return nil, fmt.Errorf("failed to query wallet state elements: %w", err) + return nil, fmt.Errorf("failed to query siacoin elements: %w", err) } defer rows.Close() for rows.Next() { - var se types.StateElement - if err := rows.Scan(decode(&se.ID), decode(&se.MerkleProof), decode(&se.LeafIndex)); err != nil { - return nil, fmt.Errorf("failed to scan element: %w", err) + var se stateElement + if err := rows.Scan(decode(&se.ID), decode(&se.LeafIndex), decode(&se.MerkleProof)); err != nil { + return nil, fmt.Errorf("failed to scan siacoin element: %w", err) } elements = append(elements, se) } if err := rows.Err(); err != nil { - return nil, fmt.Errorf("scan error: %w", err) + return nil, fmt.Errorf("failed to scan siacoin elements: %w", err) } return elements, nil } -// UpdateWalletStateElements updates the proofs of all state elements affected by the -// update. -func (ux *updateTx) UpdateWalletStateElements(elements []types.StateElement) error { - if len(elements) == 0 { - return nil +func updateSiacoinStateElements(tx *txn, elements []stateElement) error { + stmt, err := tx.Prepare(`UPDATE wallet_siacoin_elements SET merkle_proof=?, leaf_index=? WHERE id=?`) + if err != nil { + return fmt.Errorf("failed to prepare update statement: %w", err) + } + defer stmt.Close() + + for _, se := range elements { + if res, err := stmt.Exec(encode(se.MerkleProof), encode(se.LeafIndex), encode(se.ID)); err != nil { + return fmt.Errorf("failed to update siacoin element: %w", err) + } else if n, err := res.RowsAffected(); err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } else if n != 1 { + return fmt.Errorf("failed to update siacoin element %q: not found", se.ID) + } + } + return nil +} + +func getContractStateElements(tx *txn) (elements []contractStateElement, err error) { + const query = `SELECT contract_id, leaf_index, merkle_proof FROM contract_v2_state_elements` + rows, err := tx.Query(query) + if err != nil { + return nil, fmt.Errorf("failed to query siacoin elements: %w", err) + } + defer rows.Close() + + for rows.Next() { + var se contractStateElement + if err := rows.Scan(&se.ID, decode(&se.LeafIndex), decode(&se.MerkleProof)); err != nil { + return nil, fmt.Errorf("failed to scan siacoin element: %w", err) + } + elements = append(elements, se) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("failed to scan siacoin elements: %w", err) + } + return elements, nil +} + +func updateContractStateElements(tx *txn, elements []contractStateElement) error { + stmt, err := tx.Prepare(`UPDATE contract_v2_state_elements SET merkle_proof=?, leaf_index=? WHERE contract_id=?`) + if err != nil { + return fmt.Errorf("failed to prepare update statement: %w", err) + } + defer stmt.Close() + + for _, se := range elements { + if res, err := stmt.Exec(encode(se.MerkleProof), encode(se.LeafIndex), se.ID); err != nil { + return fmt.Errorf("failed to update siacoin element: %w", err) + } else if n, err := res.RowsAffected(); err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } else if n != 1 { + return fmt.Errorf("failed to update siacoin element %q: not found", se.ID) + } } - stmt, err := ux.tx.Prepare(`UPDATE wallet_siacoin_elements SET merkle_proof=?, leaf_index=? WHERE id=?`) + return nil +} + +func getChainStateElements(tx *txn) (elements []stateElement, err error) { + const query = `SELECT id, leaf_index, merkle_proof FROM contracts_v2_chain_index_elements` + rows, err := tx.Query(query) + if err != nil { + return nil, fmt.Errorf("failed to query siacoin elements: %w", err) + } + defer rows.Close() + + for rows.Next() { + var se stateElement + if err := rows.Scan(decode(&se.ID), decode(&se.LeafIndex), decode(&se.MerkleProof)); err != nil { + return nil, fmt.Errorf("failed to scan siacoin element: %w", err) + } + elements = append(elements, se) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("failed to scan siacoin elements: %w", err) + } + return elements, nil +} + +func updateChainStateElements(tx *txn, elements []stateElement) error { + stmt, err := tx.Prepare(`UPDATE contracts_v2_chain_index_elements SET merkle_proof=?, leaf_index=? WHERE id=?`) if err != nil { return fmt.Errorf("failed to prepare update statement: %w", err) } defer stmt.Close() for _, se := range elements { - if _, err := stmt.Exec(encode(se.MerkleProof), encode(se.LeafIndex), encode(se.ID)); err != nil { - return fmt.Errorf("failed to update wallet state element: %w", err) + if res, err := stmt.Exec(encode(se.MerkleProof), encode(se.LeafIndex), encode(se.ID)); err != nil { + return fmt.Errorf("failed to update siacoin element: %w", err) + } else if n, err := res.RowsAffected(); err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } else if n != 1 { + return fmt.Errorf("failed to update siacoin element %q: not found", se.ID) } } return nil } +// UpdateWalletSiacoinElementProofs updates the proofs of all state elements +// affected by the update. ProofUpdater.UpdateElementProof must be called +// for each state element in the database. +func (ux *updateTx) UpdateWalletSiacoinElementProofs(updater wallet.ProofUpdater) error { + se, err := getSiacoinStateElements(ux.tx) + if err != nil { + return fmt.Errorf("failed to get siacoin state elements: %w", err) + } + for i := range se { + updater.UpdateElementProof(&se[i].StateElement) + } + return updateSiacoinStateElements(ux.tx, se) +} + +// UpdateContractElementProofs updates the proofs of all state elements +// affected by the update. ProofUpdater.UpdateElementProof must be called +// for each state element in the database. +func (ux *updateTx) UpdateContractElementProofs(updater wallet.ProofUpdater) error { + se, err := getContractStateElements(ux.tx) + if err != nil { + return fmt.Errorf("failed to get contract state elements: %w", err) + } + for i := range se { + updater.UpdateElementProof(&se[i].StateElement) + } + return updateContractStateElements(ux.tx, se) +} + +// UpdateChainIndexElementProofs updates the proofs of all state elements affected +// by the update. ProofUpdater.UpdateElementProof must be called for each state +// element in the database. +func (ux *updateTx) UpdateChainIndexElementProofs(updater wallet.ProofUpdater) error { + se, err := getChainStateElements(ux.tx) + if err != nil { + return fmt.Errorf("failed to get contract state elements: %w", err) + } + for i := range se { + updater.UpdateElementProof(&se[i].StateElement) + } + return updateChainStateElements(ux.tx, se) +} + // WalletApplyIndex is called with the chain index that is being applied. // Any transactions and siacoin elements that were created by the index // should be added and any siacoin elements that were spent should be @@ -176,101 +305,20 @@ func (ux *updateTx) RevertContractChainIndexElement(index types.ChainIndex) erro return err } -// ContractChainIndexElements returns chain index state elements that -// need to be updated. The elements must be ordered by height. -func (ux *updateTx) ContractChainIndexElements() (elements []types.ChainIndexElement, err error) { - rows, err := ux.tx.Query(`SELECT id, height, merkle_proof, leaf_index FROM contracts_v2_chain_index_elements ORDER BY height ASC`) - if err != nil { - return nil, fmt.Errorf("failed to query contract chain index state elements: %w", err) - } - defer rows.Close() - - for rows.Next() { - var ele types.ChainIndexElement - if err := rows.Scan(decode(&ele.ChainIndex.ID), &ele.ChainIndex.Height, decode(&ele.MerkleProof), decode(&ele.LeafIndex)); err != nil { - return nil, fmt.Errorf("failed to scan state element: %w", err) - } - ele.ID = types.Hash256(ele.ChainIndex.ID) - elements = append(elements, ele) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("failed to scan contract chain index state elements: %w", err) - } - return elements, nil -} - // ApplyContractChainIndexElements adds or updates the merkle proof of // chain index state elements -func (ux *updateTx) ApplyContractChainIndexElements(elements []types.ChainIndexElement) error { - if len(elements) == 0 { - return nil - } - - stmt, err := ux.tx.Prepare(`INSERT INTO contracts_v2_chain_index_elements (id, height, merkle_proof, leaf_index) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET merkle_proof=EXCLUDED.merkle_proof, leaf_index=EXCLUDED.leaf_index, height=EXCLUDED.height`) - if err != nil { - return fmt.Errorf("failed to prepare update statement: %w", err) - } - defer stmt.Close() - - for _, se := range elements { - if _, err := stmt.Exec(encode(se.ChainIndex.ID), se.ChainIndex.Height, encode(se.MerkleProof), encode(se.LeafIndex)); err != nil { - return fmt.Errorf("failed to update contract chain index state element: %w", err) - } - } - return nil +func (ux *updateTx) AddContractChainIndexElement(ci types.ChainIndexElement) error { + _, err := ux.tx.Exec(`INSERT INTO contracts_v2_chain_index_elements (id, height, merkle_proof, leaf_index) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET merkle_proof=EXCLUDED.merkle_proof, leaf_index=EXCLUDED.leaf_index, height=EXCLUDED.height`, encode(ci.ChainIndex.ID), ci.ChainIndex.Height, encode(ci.StateElement.MerkleProof), encode(ci.StateElement.LeafIndex)) + return err } -// DeleteExpiredContractChainIndexElements deletes chain index state +// DeleteExpiredChainIndexElements deletes chain index state // elements that are no long necessary -func (ux *updateTx) DeleteExpiredContractChainIndexElements(height uint64) error { +func (ux *updateTx) DeleteExpiredChainIndexElements(height uint64) error { _, err := ux.tx.Exec(`DELETE FROM contracts_v2_chain_index_elements WHERE height <= ?`, height) return err } -// ContractStateElements returns all state elements from the contract -// store -func (ux *updateTx) ContractStateElements() (elements []types.StateElement, err error) { - rows, err := ux.tx.Query(`SELECT c.contract_id, cs.merkle_proof, cs.leaf_index FROM contract_v2_state_elements cs -INNER JOIN contracts_v2 c ON (c.id=cs.contract_id)`) - if err != nil { - return nil, fmt.Errorf("failed to query contract state elements: %w", err) - } - defer rows.Close() - - for rows.Next() { - var se types.StateElement - if err := rows.Scan(decode(&se.ID), decode(&se.MerkleProof), decode(&se.LeafIndex)); err != nil { - return nil, fmt.Errorf("failed to scan contract state element: %w", err) - } - elements = append(elements, se) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("failed to scan contract state elements: %w", err) - } - return elements, nil -} - -// UpdateContractStateElements updates the state elements in the host -// contract store -func (ux *updateTx) UpdateContractStateElements(elements []types.StateElement) error { - if len(elements) == 0 { - return nil - } - - stmt, err := ux.tx.Prepare(`UPDATE contract_v2_state_elements SET merkle_proof=?, leaf_index=? WHERE contract_id=(SELECT id FROM contracts_v2 WHERE contract_id=?)`) - if err != nil { - return fmt.Errorf("failed to prepare update statement: %w", err) - } - defer stmt.Close() - - for _, se := range elements { - if _, err := stmt.Exec(encode(se.MerkleProof), encode(se.LeafIndex), encode(se.ID)); err != nil { - return fmt.Errorf("failed to update contract state element %q: %w", se.ID, err) - } - } - return nil -} - // ApplyContracts applies relevant contract changes to the contract // store func (ux *updateTx) ApplyContracts(index types.ChainIndex, state contracts.StateChanges) error { @@ -468,6 +516,25 @@ func (ux *updateTx) LastAnnouncement() (announcement settings.Announcement, err return } +func (ux *updateTx) LastV2AnnouncementHash() (h types.Hash256, index types.ChainIndex, err error) { + err = ux.tx.QueryRow(`SELECT last_v2_announce_hash, last_announce_index FROM global_settings`). + Scan(decodeNullable(&h), decodeNullable(&index)) + if errors.Is(err, sql.ErrNoRows) { + return types.Hash256{}, types.ChainIndex{}, nil + } + return +} + +func (ux *updateTx) RevertLastV2Announcement() error { + _, err := ux.tx.Exec(`UPDATE global_settings SET last_v2_announce_hash=NULL, last_announce_index=NULL`) + return err +} + +func (ux *updateTx) SetLastV2AnnouncementHash(h types.Hash256, index types.ChainIndex) error { + _, err := ux.tx.Exec(`UPDATE global_settings SET last_announce_index=?, last_v2_announce_hash=?`, encode(index), encode(h)) + return err +} + func (ux *updateTx) RevertLastAnnouncement() error { _, err := ux.tx.Exec(`UPDATE global_settings SET last_announce_address=NULL, last_announce_index=NULL`) return err @@ -534,7 +601,7 @@ func createSiacoinElements(tx *txn, index types.ChainIndex, created []types.Siac defer stmt.Close() for _, elem := range created { - if _, err := stmt.Exec(encode(elem.ID), encode(elem.SiacoinOutput.Value), encode(elem.SiacoinOutput.Address), encode(elem.MerkleProof), encode(elem.LeafIndex), elem.MaturityHeight); err != nil { + if _, err := stmt.Exec(encode(elem.ID), encode(elem.SiacoinOutput.Value), encode(elem.SiacoinOutput.Address), encode(elem.StateElement.MerkleProof), encode(elem.StateElement.LeafIndex), elem.MaturityHeight); err != nil { return types.ZeroCurrency, types.ZeroCurrency, fmt.Errorf("failed to insert siacoin element %q: %w", elem.ID, err) } @@ -1342,7 +1409,7 @@ func applyV2ContractFormation(tx *txn, index types.ChainIndex, confirmed []types return fmt.Errorf("failed to get contract state %q: %w", fce.ID, err) } - if _, err := insertElementStmt.Exec(state.ID, fce.LeafIndex, encode(fce.MerkleProof), encode(fce.V2FileContract), encode(fce.V2FileContract.RevisionNumber)); err != nil { + if _, err := insertElementStmt.Exec(state.ID, fce.StateElement.LeafIndex, encode(fce.StateElement.MerkleProof), encode(fce.V2FileContract), encode(fce.V2FileContract.RevisionNumber)); err != nil { return fmt.Errorf("failed to insert contract state element %q: %w", fce.ID, err) } @@ -1474,7 +1541,7 @@ func applyV2ContractRevision(tx *txn, revised []types.V2FileContractElement) err var contractID int64 if err := selectIDStmt.QueryRow(encode(fce.ID)).Scan(&contractID); err != nil { return fmt.Errorf("failed to update contract revision %q: %w", fce.ID, err) - } else if _, err := updateElementStmt.Exec(fce.LeafIndex, encode(fce.MerkleProof), encode(fce.V2FileContract), encode(fce.V2FileContract.RevisionNumber), contractID); err != nil { + } else if _, err := updateElementStmt.Exec(fce.StateElement.LeafIndex, encode(fce.StateElement.MerkleProof), encode(fce.V2FileContract), encode(fce.V2FileContract.RevisionNumber), contractID); err != nil { return fmt.Errorf("failed to update contract state element %q: %w", fce.ID, err) } } diff --git a/persist/sqlite/contracts.go b/persist/sqlite/contracts.go index a4044cad..90d3b785 100644 --- a/persist/sqlite/contracts.go +++ b/persist/sqlite/contracts.go @@ -133,11 +133,11 @@ FROM contracts_v2 c INNER JOIN contract_v2_state_elements cs ON (c.id = cs.contract_id) WHERE c.contract_id=?` - err := tx.QueryRow(query, encode(contractID)).Scan(decode(&ele.V2FileContract), decode(&ele.LeafIndex), decode(&ele.MerkleProof)) + err := tx.QueryRow(query, encode(contractID)).Scan(decode(&ele.V2FileContract), decode(&ele.StateElement.LeafIndex), decode(&ele.StateElement.MerkleProof)) if errors.Is(err, sql.ErrNoRows) { return contracts.ErrNotFound } - ele.ID = types.Hash256(contractID) + ele.ID = contractID return err }) return @@ -551,9 +551,9 @@ func (s *Store) ContractActions(index types.ChainIndex, revisionBroadcastHeight // ContractChainIndexElement returns the chain index element for the given height. func (s *Store) ContractChainIndexElement(index types.ChainIndex) (element types.ChainIndexElement, err error) { err = s.transaction(func(tx *txn) error { - err := tx.QueryRow(`SELECT leaf_index, merkle_proof FROM contracts_v2_chain_index_elements WHERE id=? AND height=?`, encode(index.ID), index.Height).Scan(decode(&element.LeafIndex), decode(&element.MerkleProof)) + err := tx.QueryRow(`SELECT leaf_index, merkle_proof FROM contracts_v2_chain_index_elements WHERE id=? AND height=?`, encode(index.ID), index.Height).Scan(decode(&element.StateElement.LeafIndex), decode(&element.StateElement.MerkleProof)) element.ChainIndex = index - element.ID = types.Hash256(index.ID) + element.ID = index.ID return err }) return @@ -986,8 +986,8 @@ func broadcastV2Revision(tx *txn, index types.ChainIndex, revisionBroadcastHeigh err = rows.Scan(decode(&rev.Revision), decode(&rev.Parent.ID), - decode(&rev.Parent.LeafIndex), - decode(&rev.Parent.MerkleProof), + decode(&rev.Parent.StateElement.LeafIndex), + decode(&rev.Parent.StateElement.MerkleProof), decode(&rev.Parent.V2FileContract)) if err != nil { return nil, fmt.Errorf("failed to scan contract: %w", err) @@ -1014,7 +1014,7 @@ func proofV2Contracts(tx *txn, index types.ChainIndex) (elements []types.V2FileC for rows.Next() { var fce types.V2FileContractElement - if err := rows.Scan(decode(&fce.ID), decode(&fce.V2FileContract), decode(&fce.LeafIndex), decode(&fce.MerkleProof)); err != nil { + if err := rows.Scan(decode(&fce.ID), decode(&fce.V2FileContract), decode(&fce.StateElement.LeafIndex), decode(&fce.StateElement.MerkleProof)); err != nil { return nil, fmt.Errorf("failed to scan contract: %w", err) } elements = append(elements, fce) @@ -1039,7 +1039,7 @@ func expireV2Contracts(tx *txn, index types.ChainIndex) (elements []types.V2File for rows.Next() { var fce types.V2FileContractElement - if err := rows.Scan(decode(&fce.ID), decode(&fce.V2FileContract), decode(&fce.LeafIndex), decode(&fce.MerkleProof)); err != nil { + if err := rows.Scan(decode(&fce.ID), decode(&fce.V2FileContract), decode(&fce.StateElement.LeafIndex), decode(&fce.StateElement.MerkleProof)); err != nil { return nil, fmt.Errorf("failed to scan contract: %w", err) } elements = append(elements, fce) diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 0d59d98d..fdaec9c6 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -296,7 +296,8 @@ CREATE TABLE global_settings ( wallet_hash BLOB, -- used to prevent wallet seed changes last_scanned_index BLOB, -- chain index of the last scanned block last_announce_index BLOB, -- chain index of the last host announcement - last_announce_address TEXT -- address of the last host announcement + last_announce_address TEXT, -- address of the last host announcement + last_v2_announce_hash BLOB -- hash of the last v2 host announcement ); -- initialize the global settings table diff --git a/persist/sqlite/migrations.go b/persist/sqlite/migrations.go index 88b06a47..b59b05f7 100644 --- a/persist/sqlite/migrations.go +++ b/persist/sqlite/migrations.go @@ -10,6 +10,22 @@ import ( "go.uber.org/zap" ) +func migrateVersion31(tx *txn, _ *zap.Logger) error { + _, err := tx.Exec(` +ALTER TABLE global_settings ADD COLUMN last_v2_announce_hash BLOB; +-- reset v2 contracts (should be no-op) +DELETE FROM contracts_v2_chain_index_elements; +DELETE FROM contract_v2_state_elements; +-- reset wallet +DELETE FROM wallet_siacoin_elements; +DELETE FROM wallet_events; +-- reset wallet metrics +DELETE FROM host_stats WHERE stat IN (?,?); -- reset wallet stats since they are derived from the chain +-- trigger rescan +UPDATE global_settings SET last_scanned_index=NULL;`, metricWalletBalance, metricWalletImmatureBalance) + return err +} + func migrateVersion30(tx *txn, _ *zap.Logger) error { _, err := tx.Exec(` ALTER TABLE contracts_v2 DROP COLUMN registry_read; @@ -923,4 +939,5 @@ var migrations = []func(tx *txn, log *zap.Logger) error{ migrateVersion28, migrateVersion29, migrateVersion30, + migrateVersion31, } diff --git a/persist/sqlite/settings.go b/persist/sqlite/settings.go index 14b591d6..103a45cb 100644 --- a/persist/sqlite/settings.go +++ b/persist/sqlite/settings.go @@ -180,6 +180,19 @@ func (s *Store) LastAnnouncement() (ann settings.Announcement, err error) { return } +// LastV2AnnouncementHash returns the hash of the last v2 announcement and the +// chain index it was confirmed in. +func (s *Store) LastV2AnnouncementHash() (h types.Hash256, index types.ChainIndex, err error) { + err = s.transaction(func(tx *txn) error { + return tx.QueryRow(`SELECT last_v2_announce_hash, last_announce_index FROM global_settings`). + Scan(decodeNullable(&h), decodeNullable(&index)) + }) + if errors.Is(err, sql.ErrNoRows) { + return types.Hash256{}, types.ChainIndex{}, nil + } + return +} + // UpdateLastAnnouncement updates the last announcement. func (s *Store) UpdateLastAnnouncement(ann settings.Announcement) error { const query = `UPDATE global_settings SET last_announce_index=$1, last_announce_address=$2;` diff --git a/persist/sqlite/wallet.go b/persist/sqlite/wallet.go index 1fc87e94..1ecaad48 100644 --- a/persist/sqlite/wallet.go +++ b/persist/sqlite/wallet.go @@ -22,7 +22,7 @@ func (s *Store) UnspentSiacoinElements() (utxos []types.SiacoinElement, err erro defer rows.Close() for rows.Next() { var se types.SiacoinElement - if err := rows.Scan(decode(&se.ID), decode(&se.SiacoinOutput.Value), decode(&se.SiacoinOutput.Address), decode(&se.LeafIndex), decode(&se.MerkleProof), &se.MaturityHeight); err != nil { + if err := rows.Scan(decode(&se.ID), decode(&se.SiacoinOutput.Value), decode(&se.SiacoinOutput.Address), decode(&se.StateElement.LeafIndex), decode(&se.StateElement.MerkleProof), &se.MaturityHeight); err != nil { return fmt.Errorf("failed to scan unspent siacoin element: %w", err) } utxos = append(utxos, se) diff --git a/persist/sqlite/wallet_test.go b/persist/sqlite/wallet_test.go index 07f50060..e53694ec 100644 --- a/persist/sqlite/wallet_test.go +++ b/persist/sqlite/wallet_test.go @@ -52,14 +52,14 @@ func TestWalletMetrics(t *testing.T) { assertWalletMetrics(t, h1.Store, expectedMature, expectedImmature) // mine until the first block reward matures - mineAndSync(t, n1, types.VoidAddress, 144) + mineAndSync(t, n1, types.VoidAddress, int(network.MaturityDelay)) expectedMature = expectedImmature expectedImmature = types.ZeroCurrency assertWalletMetrics(t, h1.Store, expectedMature, expectedImmature) // mine a secondary chain to reorg the first chain n2 := testutil.NewConsensusNode(t, network, genesis, log.Named("node2")) - testutil.MineBlocks(t, n2, types.VoidAddress, 250) + testutil.MineBlocks(t, n2, types.VoidAddress, int(network.MaturityDelay*4)) t.Log("connecting peer 2") if _, err := h1.Syncer.Connect(context.Background(), n2.Syncer.Addr()); err != nil { diff --git a/rhp/v2/rpc_test.go b/rhp/v2/rpc_test.go index 7d780416..10f01932 100644 --- a/rhp/v2/rpc_test.go +++ b/rhp/v2/rpc_test.go @@ -107,7 +107,7 @@ func TestUploadDownload(t *testing.T) { } // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) l, err := net.Listen("tcp", "localhost:0") if err != nil { @@ -213,7 +213,7 @@ func TestRenew(t *testing.T) { } // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) l, err := net.Listen("tcp", "localhost:0") if err != nil { @@ -421,7 +421,7 @@ func TestRPCV2(t *testing.T) { } // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) l, err := net.Listen("tcp", "localhost:0") if err != nil { diff --git a/rhp/v3/rpc_test.go b/rhp/v3/rpc_test.go index 43ed1292..ae4ef18e 100644 --- a/rhp/v3/rpc_test.go +++ b/rhp/v3/rpc_test.go @@ -129,7 +129,7 @@ func TestPriceTable(t *testing.T) { node := testutil.NewHostNode(t, hostKey, network, genesis, log) // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) // start the node sh2, sh3 := setupRHP3Host(t, node, hostKey, 10, log) @@ -199,7 +199,7 @@ func TestAppendSector(t *testing.T) { node := testutil.NewHostNode(t, hostKey, network, genesis, log) // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) // start the node sh2, sh3 := setupRHP3Host(t, node, hostKey, 10, log) @@ -278,7 +278,7 @@ func TestStoreSector(t *testing.T) { node := testutil.NewHostNode(t, hostKey, network, genesis, log) // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) // start the node sh2, sh3 := setupRHP3Host(t, node, hostKey, 10, log) @@ -348,7 +348,7 @@ func TestReadSectorOffset(t *testing.T) { node := testutil.NewHostNode(t, hostKey, network, genesis, log) // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) // start the node sh2, sh3 := setupRHP3Host(t, node, hostKey, 10, log) @@ -412,7 +412,7 @@ func TestRenew(t *testing.T) { node := testutil.NewHostNode(t, hostKey, network, genesis, log) // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) // start the node sh2, sh3 := setupRHP3Host(t, node, hostKey, 10, log) @@ -606,7 +606,7 @@ func TestRPCV2(t *testing.T) { } // fund the wallet - testutil.MineAndSync(t, node, node.Wallet.Address(), 150) + testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5)) // start the node sh2, sh3 := setupRHP3Host(t, node, hostKey, 10, log)