diff --git a/explorer/events.go b/explorer/events.go index 9f539275..b51b5c21 100644 --- a/explorer/events.go +++ b/explorer/events.go @@ -5,6 +5,7 @@ import ( "go.sia.tech/core/consensus" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" ) // event type constants @@ -47,12 +48,6 @@ func (*EventFoundationSubsidy) EventType() string { return EventTypeFoundationSu // EventType implements Event. func (*EventContractPayout) EventType() string { return EventTypeContractPayout } -// A HostAnnouncement represents a host announcement within an EventTransaction. -type HostAnnouncement struct { - PublicKey types.PublicKey `json:"publicKey"` - NetAddress string `json:"netAddress"` -} - // An EventSiafundInput represents a siafund input within an EventTransaction. type EventSiafundInput struct { SiafundElement types.SiafundElement `json:"siafundElement"` @@ -80,9 +75,9 @@ type EventV2FileContract struct { // An EventTransaction represents a transaction that affects the wallet. type EventTransaction struct { - Transaction Transaction `json:"transaction"` - HostAnnouncements []HostAnnouncement `json:"hostAnnouncements"` - Fee types.Currency `json:"fee"` + Transaction Transaction `json:"transaction"` + HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements"` + Fee types.Currency `json:"fee"` } // An EventMinerPayout represents a miner payout from a block. @@ -194,20 +189,12 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) []Event { var e EventTransaction for _, arb := range txn.ArbitraryData { - var prefix types.Specifier - var uk types.UnlockKey - d := types.NewBufDecoder(arb) - prefix.DecodeFrom(d) - netAddress := d.ReadString() - uk.DecodeFrom(d) - if d.Err() == nil && prefix == SpecifierAnnouncement && - uk.Algorithm == types.SpecifierEd25519 && len(uk.Key) == len(types.PublicKey{}) { - e.HostAnnouncements = append(e.HostAnnouncements, HostAnnouncement{ - PublicKey: *(*types.PublicKey)(uk.Key), - NetAddress: netAddress, - }) + var ha chain.HostAnnouncement + if ha.FromArbitraryData(arb) { + e.HostAnnouncements = append(e.HostAnnouncements, ha) } } + for i := range txn.MinerFees { e.Fee = e.Fee.Add(txn.MinerFees[i]) } @@ -221,11 +208,9 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) []Event { var e EventTransaction for _, a := range txn.Attestations { - if a.Key == "HostAnnouncement" { - e.HostAnnouncements = append(e.HostAnnouncements, HostAnnouncement{ - PublicKey: a.PublicKey, - NetAddress: string(a.Value), - }) + var ha chain.HostAnnouncement + if ha.FromAttestation(a) { + e.HostAnnouncements = append(e.HostAnnouncements, ha) } } addEvent(types.Hash256(txn.ID()), cs.Index.Height, &e, relevant) // transaction maturity height is the current block height diff --git a/explorer/explorer.go b/explorer/explorer.go index c26299e2..3e778476 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -56,7 +56,7 @@ type Store interface { SiafundElements(ids []types.SiafundOutputID) (result []SiafundOutput, err error) Hosts(pks []types.PublicKey) ([]Host, error) - HostsForScanning(maxLastScan, minLastAnnouncement time.Time, offset, limit uint64) ([]HostAnnouncement, error) + HostsForScanning(maxLastScan, minLastAnnouncement time.Time, offset, limit uint64) ([]chain.HostAnnouncement, error) } // Explorer implements a Sia explorer. diff --git a/explorer/scan.go b/explorer/scan.go index 3c33dc76..d6fdf4a6 100644 --- a/explorer/scan.go +++ b/explorer/scan.go @@ -9,6 +9,7 @@ import ( crhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" rhpv2 "go.sia.tech/explored/internal/rhp/v2" rhpv3 "go.sia.tech/explored/internal/rhp/v3" "go.uber.org/zap" @@ -50,7 +51,7 @@ func (e *Explorer) waitForSync() error { return nil } -func (e *Explorer) scanHost(host HostAnnouncement) (HostScan, error) { +func (e *Explorer) scanHost(host chain.HostAnnouncement) (HostScan, error) { ctx, cancel := context.WithTimeout(e.ctx, e.scanCfg.Timeout) defer cancel() @@ -98,7 +99,7 @@ func (e *Explorer) scanHost(host HostAnnouncement) (HostScan, error) { }, nil } -func (e *Explorer) addHostScans(hosts chan HostAnnouncement) { +func (e *Explorer) addHostScans(hosts chan chain.HostAnnouncement) { worker := func() { var scans []HostScan for host := range hosts { @@ -149,7 +150,7 @@ func (e *Explorer) isClosed() bool { } } -func (e *Explorer) fetchHosts(hosts chan HostAnnouncement) { +func (e *Explorer) fetchHosts(hosts chan chain.HostAnnouncement) { var exhausted bool offset := 0 @@ -187,7 +188,7 @@ func (e *Explorer) scanHosts() { for !e.isClosed() { // fetch hosts - hosts := make(chan HostAnnouncement, scanBatchSize) + hosts := make(chan chain.HostAnnouncement, scanBatchSize) e.wg.Add(1) go func() { defer e.wg.Done() diff --git a/explorer/types.go b/explorer/types.go index 223742a6..1621d2ac 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -9,6 +9,7 @@ import ( rhpv2 "go.sia.tech/core/rhp/v2" rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" ) // A Source represents where a siacoin output came from. @@ -125,6 +126,8 @@ type Transaction struct { MinerFees []types.Currency `json:"minerFees,omitempty"` ArbitraryData [][]byte `json:"arbitraryData,omitempty"` Signatures []types.TransactionSignature `json:"signatures,omitempty"` + + HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements,omitempty"` } // A Block is a block containing wrapped transactions and siacoin diff --git a/go.mod b/go.mod index 0abf9724..2edeee60 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,9 @@ module go.sia.tech/explored -go 1.22 - -toolchain go1.23.0 +go 1.23.0 require ( github.com/mattn/go-sqlite3 v1.14.23 - go.sia.tech/core v0.4.6 - go.sia.tech/coreutils v0.3.2 go.sia.tech/jape v0.12.1 go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 @@ -22,10 +18,12 @@ require ( github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/stretchr/testify v1.8.3 // indirect go.etcd.io/bbolt v1.3.11 // indirect - go.sia.tech/mux v1.2.0 // indirect + go.sia.tech/core v0.4.6 // indirect + go.sia.tech/coreutils v0.3.3-0.20240919173455-7fd91e1c4791 // indirect + go.sia.tech/mux v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/sys v0.24.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/sys v0.25.0 // indirect golang.org/x/tools v0.20.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 6c68b52d..8af4589f 100644 --- a/go.sum +++ b/go.sum @@ -23,27 +23,29 @@ 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.6 h1:QLm97a7GWBonfnMEOokqWRAqsWCUPL7kzo6k3Adwx8E= go.sia.tech/core v0.4.6/go.mod h1:Zuq0Tn2aIXJyO0bjGu8cMeVWe+vwQnUfZhG1LCmjD5c= -go.sia.tech/coreutils v0.3.2 h1:3gJqvs18n1FVZmcrnfIYyzS+rBu06OtIscDDAfUAYQI= -go.sia.tech/coreutils v0.3.2/go.mod h1:woPVmN6GUpIKHdi71Hkb9goIbl7b45TquCsAyEzyxnI= +go.sia.tech/coreutils v0.3.3-0.20240903190934-0dd7ac18e90f h1:qCHhdAMSbI7aku94q6a90Jj5adXQA5BSeMhag6lz9xk= +go.sia.tech/coreutils v0.3.3-0.20240903190934-0dd7ac18e90f/go.mod h1:woPVmN6GUpIKHdi71Hkb9goIbl7b45TquCsAyEzyxnI= +go.sia.tech/coreutils v0.3.3-0.20240919173455-7fd91e1c4791 h1:A0r5QkLysdiVHq0FV28JU0F5QZDUnjG/HSLBcjjqgp4= +go.sia.tech/coreutils v0.3.3-0.20240919173455-7fd91e1c4791/go.mod h1:Y7L4NIozE9EyjrzY+e/vXog7o3wREu9e2POXFDsdnVE= 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.2.0 h1:ofa1Us9mdymBbGMY2XH/lSpY8itFsKIo/Aq8zwe+GHU= -go.sia.tech/mux v1.2.0/go.mod h1:Yyo6wZelOYTyvrHmJZ6aQfRoer3o4xyKQ4NmQLJrBSo= +go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= +go.sia.tech/mux v1.3.0/go.mod h1:I46++RD4beqA3cW9Xm9SwXbezwPqLvHhVs9HLpDtt58= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/persist/sqlite/addresses.go b/persist/sqlite/addresses.go index 9e6e0f9b..a1a038d0 100644 --- a/persist/sqlite/addresses.go +++ b/persist/sqlite/addresses.go @@ -6,6 +6,7 @@ import ( "time" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" "go.sia.tech/explored/explorer" ) @@ -38,7 +39,7 @@ func scanEvent(tx *txn, s scanner) (ev explorer.Event, eventID int64, err error) eventTx.Transaction = txns[0] for rows.Next() { - var announcement explorer.HostAnnouncement + var announcement chain.HostAnnouncement if err := rows.Scan(decode(&announcement.PublicKey), &announcement.NetAddress); err != nil { return explorer.Event{}, 0, fmt.Errorf("failed to scan announcement: %w", err) } @@ -109,7 +110,7 @@ func (s *Store) Hosts(pks []types.PublicKey) (result []explorer.Host, err error) } // HostsForScanning returns hosts ordered by the transaction they were created in. -func (s *Store) HostsForScanning(maxLastScan, minLastAnnouncement time.Time, offset, limit uint64) (result []explorer.HostAnnouncement, err error) { +func (s *Store) HostsForScanning(maxLastScan, minLastAnnouncement time.Time, offset, limit uint64) (result []chain.HostAnnouncement, err error) { err = s.transaction(func(tx *txn) error { rows, err := tx.Query(`SELECT public_key, net_address FROM host_info WHERE last_scan <= ? AND last_announcement >= ? ORDER BY last_scan ASC LIMIT ? OFFSET ?`, encode(maxLastScan), encode(minLastAnnouncement), limit, offset) if err != nil { @@ -118,7 +119,7 @@ func (s *Store) HostsForScanning(maxLastScan, minLastAnnouncement time.Time, off defer rows.Close() for rows.Next() { - var host explorer.HostAnnouncement + var host chain.HostAnnouncement if err := rows.Scan(decode(&host.PublicKey), &host.NetAddress); err != nil { return err } diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 0f00ca70..d243dc37 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -2010,6 +2010,23 @@ func TestHostAnnouncement(t *testing.T) { pk2 := types.GeneratePrivateKey() pk3 := types.GeneratePrivateKey() + checkHostAnnouncements := func(expectedArbitraryData [][]byte, got []chain.HostAnnouncement) { + t.Helper() + + var expected []chain.HostAnnouncement + for _, arb := range expectedArbitraryData { + var ha chain.HostAnnouncement + if ha.FromArbitraryData(arb) { + expected = append(expected, ha) + } + } + check(t, "len(hostAnnouncements)", len(expected), len(got)) + for i := range expected { + check(t, "host public key", expected[i].PublicKey, got[i].PublicKey) + check(t, "host net address", expected[i].NetAddress, got[i].NetAddress) + } + } + txn1 := types.Transaction{ ArbitraryData: [][]byte{ createAnnouncement(pk1, "127.0.0.1:1234"), @@ -2057,6 +2074,33 @@ func TestHostAnnouncement(t *testing.T) { StorageUtilization: 0, }) + { + dbTxns, err := db.Transactions([]types.TransactionID{txn1.ID()}) + if err != nil { + t.Fatal(err) + } + check(t, "len(txns)", 1, len(dbTxns)) + checkHostAnnouncements(txn1.ArbitraryData, dbTxns[0].HostAnnouncements) + } + + { + dbTxns, err := db.Transactions([]types.TransactionID{txn2.ID()}) + if err != nil { + t.Fatal(err) + } + check(t, "len(txns)", 1, len(dbTxns)) + checkHostAnnouncements(txn2.ArbitraryData, dbTxns[0].HostAnnouncements) + } + + { + dbTxns, err := db.Transactions([]types.TransactionID{txn3.ID()}) + if err != nil { + t.Fatal(err) + } + check(t, "len(txns)", 1, len(dbTxns)) + checkHostAnnouncements(txn3.ArbitraryData, dbTxns[0].HostAnnouncements) + } + ts := time.Unix(0, 0) hosts, err := db.HostsForScanning(ts, ts, 0, 100) if err != nil { diff --git a/persist/sqlite/transactions.go b/persist/sqlite/transactions.go index 510ee5d2..1e142325 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -4,6 +4,7 @@ import ( "fmt" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" "go.sia.tech/explored/explorer" ) @@ -553,6 +554,14 @@ func getTransactions(tx *txn, idMap map[int64]transactionID) ([]explorer.Transac ArbitraryData: txnArbitraryData[dbID], Signatures: txnSignatures[dbID], } + + for _, arb := range txn.ArbitraryData { + var ha chain.HostAnnouncement + if ha.FromArbitraryData(arb) { + txn.HostAnnouncements = append(txn.HostAnnouncements, ha) + } + } + results = append(results, txn) } return results, nil