From 9e5777a88a0c1a1fbd125aa0325498cd43dfb8a9 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Sun, 15 Dec 2024 23:56:41 -0500 Subject: [PATCH 1/4] use types similar to those in coreutils/wallet but with explorer enhanced types --- explorer/events.go | 406 ++++++++++++++++------------- go.mod | 1 + go.sum | 2 + internal/testutil/check.go | 7 +- persist/sqlite/addresses.go | 134 +++------- persist/sqlite/consensus.go | 102 ++++---- persist/sqlite/consensus_test.go | 48 +++- persist/sqlite/init.sql | 50 ++-- persist/sqlite/v2consensus_test.go | 70 ++++- 9 files changed, 459 insertions(+), 361 deletions(-) diff --git a/explorer/events.go b/explorer/events.go index 4c4f0a5..30df1f2 100644 --- a/explorer/events.go +++ b/explorer/events.go @@ -5,104 +5,69 @@ import ( "go.sia.tech/core/consensus" "go.sia.tech/core/types" - "go.sia.tech/coreutils/chain" + "go.sia.tech/coreutils/wallet" ) -// event type constants -const ( - EventTypeTransaction = "transaction" - EventTypeV2Transaction = "v2transaction" - EventTypeMinerPayout = "miner payout" - EventTypeContractPayout = "contract payout" - EventTypeSiafundClaim = "siafund claim" - EventTypeFoundationSubsidy = "foundation subsidy" -) - -// Arbitrary data specifiers -var ( - SpecifierAnnouncement = types.NewSpecifier("HostAnnouncement") -) - -type eventData interface { - EventType() string -} - -// An Event is something interesting that happened on the Sia blockchain. -type Event struct { - ID types.Hash256 `json:"id"` - Index types.ChainIndex `json:"index"` - Timestamp time.Time `json:"timestamp"` - MaturityHeight uint64 `json:"maturityHeight"` - Addresses []types.Address `json:"addresses"` - Data eventData `json:"data"` -} - -// EventType implements Event. -func (*EventTransaction) EventType() string { return EventTypeTransaction } - -// EventType implements Event. -func (*EventV2Transaction) EventType() string { return EventTypeV2Transaction } - -// EventType implements Event. -func (*EventMinerPayout) EventType() string { return EventTypeMinerPayout } - -// EventType implements Event. -func (*EventFoundationSubsidy) EventType() string { return EventTypeFoundationSubsidy } - -// EventType implements Event. -func (*EventContractPayout) EventType() string { return EventTypeContractPayout } - -// An EventSiafundInput represents a siafund input within an EventTransaction. -type EventSiafundInput struct { - SiafundElement types.SiafundElement `json:"siafundElement"` - ClaimElement types.SiacoinElement `json:"claimElement"` -} +type ( + // An EventPayout represents a miner payout, siafund claim, or foundation + // subsidy. + EventPayout struct { + SiacoinElement SiacoinOutput `json:"siacoinElement"` + } -// An EventFileContract represents a file contract within an EventTransaction. -type EventFileContract struct { - FileContract types.FileContractElement `json:"fileContract"` - // only non-nil if transaction revised contract - Revision *types.FileContract `json:"revision,omitempty"` - // only non-nil if transaction resolved contract - ValidOutputs []types.SiacoinElement `json:"validOutputs,omitempty"` -} + // An EventV1Transaction pairs a v1 transaction with its spent siacoin and + // siafund elements. + EventV1Transaction struct { + Transaction Transaction `json:"transaction"` + // v1 siacoin inputs do not describe the value of the spent utxo + SpentSiacoinElements []SiacoinOutput `json:"spentSiacoinElements,omitempty"` + // v1 siafund inputs do not describe the value of the spent utxo + SpentSiafundElements []SiacoinOutput `json:"spentSiafundElements,omitempty"` + } -// An EventV2FileContract represents a v2 file contract within an EventTransaction. -type EventV2FileContract struct { - FileContract types.V2FileContractElement `json:"fileContract"` - // only non-nil if transaction revised contract - Revision *types.V2FileContract `json:"revision,omitempty"` - // only non-nil if transaction resolved contract - Resolution types.V2FileContractResolutionType `json:"resolution,omitempty"` - Outputs []types.SiacoinElement `json:"outputs,omitempty"` -} + // An EventV1ContractResolution represents a file contract payout from a v1 + // contract. + EventV1ContractResolution struct { + Parent ExtendedFileContract `json:"parent"` + SiacoinElement SiacoinOutput `json:"siacoinElement"` + Missed bool `json:"missed"` + } -// An EventTransaction represents a transaction that affects the wallet. -type EventTransaction struct { - Transaction Transaction `json:"transaction"` - HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements"` - Fee types.Currency `json:"fee"` -} + // An EventV2ContractResolution represents a file contract payout from a v2 + // contract. + EventV2ContractResolution struct { + Resolution V2FileContractResolution `json:"resolution"` + SiacoinElement SiacoinOutput `json:"siacoinElement"` + Missed bool `json:"missed"` + } -// An EventV2Transaction represents a v2 transaction that affects the wallet. -type EventV2Transaction V2Transaction + // EventV2Transaction is a transaction event that includes the transaction + EventV2Transaction V2Transaction -// An EventMinerPayout represents a miner payout from a block. -type EventMinerPayout struct { - SiacoinOutput types.SiacoinElement `json:"siacoinOutput"` -} + // EventData contains the data associated with an event. + EventData interface { + isEvent() bool + } -// EventFoundationSubsidy represents a foundation subsidy from a block. -type EventFoundationSubsidy struct { - SiacoinOutput types.SiacoinElement `json:"siacoinOutput"` -} + // An Event is a transaction or other event that affects the wallet including + // miner payouts, siafund claims, and file contract payouts. + Event struct { + ID types.Hash256 `json:"id"` + Index types.ChainIndex `json:"index"` + Confirmations uint64 `json:"confirmations"` + Type string `json:"type"` + Data EventData `json:"data"` + MaturityHeight uint64 `json:"maturityHeight"` + Timestamp time.Time `json:"timestamp"` + Relevant []types.Address `json:"relevant,omitempty"` + } +) -// An EventContractPayout represents a file contract payout -type EventContractPayout struct { - FileContract types.FileContractElement `json:"fileContract"` - SiacoinOutput types.SiacoinElement `json:"siacoinOutput"` - Missed bool `json:"missed"` -} +func (EventPayout) isEvent() bool { return true } +func (EventV1Transaction) isEvent() bool { return true } +func (EventV1ContractResolution) isEvent() bool { return true } +func (EventV2Transaction) isEvent() bool { return true } +func (EventV2ContractResolution) isEvent() bool { return true } // A ChainUpdate is a set of changes to the consensus state. type ChainUpdate interface { @@ -113,13 +78,12 @@ type ChainUpdate interface { } // AppliedEvents extracts a list of relevant events from a chain update. -func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) []Event { - var events []Event - addEvent := func(id types.Hash256, maturityHeight uint64, v eventData, addresses []types.Address) { +func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) (events []Event) { + addEvent := func(id types.Hash256, maturityHeight uint64, eventType string, v EventData, relevant []types.Address) { // dedup relevant addresses seen := make(map[types.Address]bool) - unique := addresses[:0] - for _, addr := range addresses { + unique := relevant[:0] + for _, addr := range relevant { if !seen[addr] { unique = append(unique, addr) seen[addr] = true @@ -131,7 +95,8 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) []Event { Timestamp: b.Timestamp, Index: cs.Index, MaturityHeight: maturityHeight, - Addresses: unique, + Relevant: unique, + Type: eventType, Data: v, }) } @@ -139,136 +104,225 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) []Event { // collect all elements sces := make(map[types.SiacoinOutputID]types.SiacoinElement) sfes := make(map[types.SiafundOutputID]types.SiafundElement) - fces := make(map[types.FileContractID]types.FileContractElement) - v2fces := make(map[types.FileContractID]types.V2FileContractElement) - cu.ForEachSiacoinElement(func(sce types.SiacoinElement, created, spent bool) { + cu.ForEachSiacoinElement(func(sce types.SiacoinElement, _, _ bool) { sce.StateElement.MerkleProof = nil - sces[sce.ID] = sce + sces[types.SiacoinOutputID(sce.ID)] = sce }) - cu.ForEachSiafundElement(func(sfe types.SiafundElement, created, spent bool) { + cu.ForEachSiafundElement(func(sfe types.SiafundElement, _, _ bool) { sfe.StateElement.MerkleProof = nil - sfes[sfe.ID] = sfe - }) - cu.ForEachFileContractElement(func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool) { - fce.StateElement.MerkleProof = nil - fces[fce.ID] = fce - }) - cu.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { - fce.StateElement.MerkleProof = nil - v2fces[fce.ID] = fce + sfes[types.SiafundOutputID(sfe.ID)] = sfe }) - relevantTxn := func(txn types.Transaction) (addrs []types.Address) { + // handle v1 transactions + for _, txn := range b.Transactions { + addresses := make(map[types.Address]struct{}) for _, sci := range txn.SiacoinInputs { - addrs = append(addrs, sces[sci.ParentID].SiacoinOutput.Address) - } - for _, sco := range txn.SiacoinOutputs { - addrs = append(addrs, sco.Address) - } - for _, sfi := range txn.SiafundInputs { - addrs = append(addrs, sfes[sfi.ParentID].SiafundOutput.Address) - } - for _, sfo := range txn.SiafundOutputs { - addrs = append(addrs, sfo.Address) - } - return - } + sce, ok := sces[sci.ParentID] + if !ok { + continue + } - relevantV2Txn := func(txn types.V2Transaction) (addrs []types.Address) { - for _, sci := range txn.SiacoinInputs { - addrs = append(addrs, sci.Parent.SiacoinOutput.Address) + // e.SpentSiacoinElements = append(e.SpentSiacoinElements, sce) + addresses[sce.SiacoinOutput.Address] = struct{}{} } for _, sco := range txn.SiacoinOutputs { - addrs = append(addrs, sco.Address) + addresses[sco.Address] = struct{}{} } + for _, sfi := range txn.SiafundInputs { - addrs = append(addrs, sfi.Parent.SiafundOutput.Address) + sfe, ok := sfes[sfi.ParentID] + if !ok { + continue + } + + // e.SpentSiafundElements = append(e.SpentSiafundElements, sfe) + addresses[sfe.SiafundOutput.Address] = struct{}{} + + sce, ok := sces[sfi.ParentID.ClaimOutputID()] + if ok { + addEvent(types.Hash256(sce.ID), sce.MaturityHeight, wallet.EventTypeSiafundClaim, EventPayout{ + SiacoinElement: SiacoinOutput{SiacoinElement: sce}, + }, []types.Address{sfi.ClaimAddress}) + } } for _, sfo := range txn.SiafundOutputs { - addrs = append(addrs, sfo.Address) + addresses[sfo.Address] = struct{}{} } - return - } - - // handle v1 transactions - for _, txn := range b.Transactions { - relevant := relevantTxn(txn) - var e EventTransaction - for _, arb := range txn.ArbitraryData { - var ha chain.HostAnnouncement - if ha.FromArbitraryData(arb) { - e.HostAnnouncements = append(e.HostAnnouncements, ha) + for _, fc := range txn.FileContracts { + addresses[fc.UnlockHash] = struct{}{} + for _, vpo := range fc.ValidProofOutputs { + addresses[vpo.Address] = struct{}{} } + for _, mpo := range fc.MissedProofOutputs { + addresses[mpo.Address] = struct{}{} + } + } + // skip transactions with no relevant addresses + if len(addresses) == 0 { + continue } - for i := range txn.MinerFees { - e.Fee = e.Fee.Add(txn.MinerFees[i]) + var ev EventV1Transaction + relevant := make([]types.Address, 0, len(addresses)) + for addr := range addresses { + relevant = append(relevant, addr) } - addEvent(types.Hash256(txn.ID()), cs.Index.Height, &e, relevant) // transaction maturity height is the current block height + addEvent(types.Hash256(txn.ID()), cs.Index.Height, wallet.EventTypeV1Transaction, ev, relevant) // transaction maturity height is the current block height } // handle v2 transactions for _, txn := range b.V2Transactions() { - relevant := relevantV2Txn(txn) - - var e EventV2Transaction - for _, a := range txn.Attestations { - var ha chain.V2HostAnnouncement - if ha.FromAttestation(a) == nil { - e.HostAnnouncements = append(e.HostAnnouncements, V2HostAnnouncement{ - PublicKey: a.PublicKey, - V2HostAnnouncement: ha, - }) + addresses := make(map[types.Address]struct{}) + for _, sci := range txn.SiacoinInputs { + addresses[sci.Parent.SiacoinOutput.Address] = struct{}{} + } + for _, sco := range txn.SiacoinOutputs { + addresses[sco.Address] = struct{}{} + } + for _, sfi := range txn.SiafundInputs { + addresses[sfi.Parent.SiafundOutput.Address] = struct{}{} + + sce, ok := sces[types.SiafundOutputID(sfi.Parent.ID).V2ClaimOutputID()] + if ok { + addEvent(types.Hash256(sce.ID), sce.MaturityHeight, wallet.EventTypeSiafundClaim, EventPayout{ + SiacoinElement: SiacoinOutput{SiacoinElement: sce}, + }, []types.Address{sfi.ClaimAddress}) } } + for _, sco := range txn.SiafundOutputs { + addresses[sco.Address] = struct{}{} + } - addEvent(types.Hash256(txn.ID()), cs.Index.Height, &e, relevant) // transaction maturity height is the current block height + // ev := EventV2Transaction(txn) + var ev EventV2Transaction + relevant := make([]types.Address, 0, len(addresses)) + for addr := range addresses { + relevant = append(relevant, addr) + } + addEvent(types.Hash256(txn.ID()), cs.Index.Height, wallet.EventTypeV2Transaction, ev, relevant) // transaction maturity height is the current block height } - // handle missed contracts - cu.ForEachFileContractElement(func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool) { + // handle contracts + cu.ForEachFileContractElement(func(fce types.FileContractElement, _ bool, rev *types.FileContractElement, resolved, valid bool) { if !resolved { return } + fce.StateElement.MerkleProof = nil + + var mpos, vpos []ContractSiacoinOutput + for _, mpo := range fce.FileContract.MissedProofOutputs { + mpos = append(mpos, ContractSiacoinOutput{SiacoinOutput: mpo}) + } + for _, vpo := range fce.FileContract.ValidProofOutputs { + vpos = append(vpos, ContractSiacoinOutput{SiacoinOutput: vpo}) + } + efc := ExtendedFileContract{ + ID: fce.ID, + Filesize: fce.FileContract.Filesize, + FileMerkleRoot: fce.FileContract.FileMerkleRoot, + WindowStart: fce.FileContract.WindowStart, + WindowEnd: fce.FileContract.WindowEnd, + Payout: fce.FileContract.Payout, + ValidProofOutputs: vpos, + MissedProofOutputs: mpos, + UnlockHash: fce.FileContract.UnlockHash, + RevisionNumber: fce.FileContract.RevisionNumber, + } + if valid { for i := range fce.FileContract.ValidProofOutputs { - outputID := fce.ID.ValidOutputID(i) - addEvent(types.Hash256(outputID), cs.MaturityHeight(), &EventContractPayout{ - FileContract: fce, - SiacoinOutput: sces[outputID], - Missed: false, - }, []types.Address{fce.FileContract.ValidProofOutputs[i].Address}) + address := fce.FileContract.ValidProofOutputs[i].Address + element := sces[types.FileContractID(fce.ID).ValidOutputID(i)] + + addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeV1ContractResolution, EventV1ContractResolution{ + Parent: efc, + SiacoinElement: SiacoinOutput{SiacoinElement: element}, + Missed: false, + }, []types.Address{address}) } } else { for i := range fce.FileContract.MissedProofOutputs { - outputID := fce.ID.MissedOutputID(i) - addEvent(types.Hash256(outputID), cs.MaturityHeight(), &EventContractPayout{ - FileContract: fce, - SiacoinOutput: sces[outputID], - Missed: true, - }, []types.Address{fce.FileContract.MissedProofOutputs[i].Address}) + address := fce.FileContract.MissedProofOutputs[i].Address + element := sces[types.FileContractID(fce.ID).MissedOutputID(i)] + + addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeV1ContractResolution, EventV1ContractResolution{ + Parent: efc, + SiacoinElement: SiacoinOutput{SiacoinElement: element}, + Missed: true, + }, []types.Address{address}) } } }) + cu.ForEachV2FileContractElement(func(fce types.V2FileContractElement, _ bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + if res == nil { + return + } + + fce.StateElement.MerkleProof = nil + + var missed bool + if _, ok := res.(*types.V2FileContractExpiration); ok { + missed = true + } + + var typ string + switch res.(type) { + case *types.V2FileContractRenewal: + typ = "renewal" + case *types.V2StorageProof: + typ = "storageProof" + case *types.V2FileContractExpiration: + typ = "expiration" + default: + panic("unknown resolution type") + } + + efc := V2FileContract{V2FileContractElement: fce} + { + element := sces[types.FileContractID(fce.ID).V2HostOutputID()] + addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeV2ContractResolution, EventV2ContractResolution{ + Resolution: V2FileContractResolution{ + Parent: efc, + Type: typ, + Resolution: res, + }, + SiacoinElement: SiacoinOutput{SiacoinElement: element}, + Missed: missed, + }, []types.Address{fce.V2FileContract.HostOutput.Address}) + } + + { + element := sces[types.FileContractID(fce.ID).V2RenterOutputID()] + addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeV2ContractResolution, EventV2ContractResolution{ + Resolution: V2FileContractResolution{ + Parent: efc, + Type: typ, + Resolution: res, + }, + SiacoinElement: SiacoinOutput{SiacoinElement: element}, + Missed: missed, + }, []types.Address{fce.V2FileContract.RenterOutput.Address}) + } + }) + // handle block rewards for i := range b.MinerPayouts { - outputID := cs.Index.ID.MinerOutputID(i) - addEvent(types.Hash256(outputID), cs.MaturityHeight(), &EventMinerPayout{ - SiacoinOutput: sces[outputID], + element := sces[cs.Index.ID.MinerOutputID(i)] + addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeMinerPayout, EventPayout{ + SiacoinElement: SiacoinOutput{SiacoinElement: element}, }, []types.Address{b.MinerPayouts[i].Address}) } // handle foundation subsidy - outputID := cs.Index.ID.FoundationOutputID() - sce, ok := sces[outputID] + element, ok := sces[cs.Index.ID.FoundationOutputID()] if ok { - addEvent(types.Hash256(outputID), cs.MaturityHeight(), &EventFoundationSubsidy{ - SiacoinOutput: sce, - }, []types.Address{cs.FoundationSubsidyAddress}) + addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeFoundationSubsidy, EventPayout{ + SiacoinElement: SiacoinOutput{SiacoinElement: element}, + }, []types.Address{element.SiacoinOutput.Address}) } return events diff --git a/go.mod b/go.mod index 97045cb..0afe145 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.1 toolchain go1.23.2 require ( + github.com/google/go-cmp v0.6.0 github.com/ip2location/ip2location-go v8.3.0+incompatible github.com/mattn/go-sqlite3 v1.14.24 go.sia.tech/core v0.7.1 diff --git a/go.sum b/go.sum index 436db51..ed08e80 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/ip2location/ip2location-go v8.3.0+incompatible h1:QwUE+FlSbo6bjOWZpv2Grb57vJhWYFNPyBj2KCvfWaM= github.com/ip2location/ip2location-go v8.3.0+incompatible/go.mod h1:3JUY1TBjTx1GdA7oRT7Zeqfc0bg3lMMuU5lXmzdpuME= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= diff --git a/internal/testutil/check.go b/internal/testutil/check.go index 13fa049..545c2b3 100644 --- a/internal/testutil/check.go +++ b/internal/testutil/check.go @@ -4,6 +4,9 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "go.sia.tech/core/consensus" "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" "go.sia.tech/explored/explorer" @@ -13,8 +16,8 @@ import ( func Equal[T any](t *testing.T, desc string, expect, got T) { t.Helper() - if !reflect.DeepEqual(expect, got) { - t.Fatalf("expected %v %s, got %v", expect, desc, got) + if !cmp.Equal(expect, got, cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(consensus.Work{}), cmpopts.IgnoreFields(types.StateElement{}, "MerkleProof")) { + t.Fatalf("%s expected != got, diff: %s", desc, cmp.Diff(expect, got)) } } diff --git a/persist/sqlite/addresses.go b/persist/sqlite/addresses.go index 760c908..8e8c31d 100644 --- a/persist/sqlite/addresses.go +++ b/persist/sqlite/addresses.go @@ -10,74 +10,50 @@ import ( "go.sia.tech/explored/explorer" ) -func scanEvent(tx *txn, s scanner) (ev explorer.Event, eventID int64, err error) { - var eventType string - - err = s.Scan(&eventID, decode(&ev.ID), &ev.MaturityHeight, decode(&ev.Timestamp), &ev.Index.Height, decode(&ev.Index.ID), &eventType) - if err != nil { - return - } +// AddressEvents returns the events of a single address. +func (s *Store) AddressEvents(address types.Address, offset, limit uint64) (events []explorer.Event, err error) { + err = s.transaction(func(tx *txn) error { + const query = ` +WITH last_chain_index (height) AS ( + SELECT MAX(height) FROM blocks +) +SELECT + ev.id, + ev.event_id, + ev.maturity_height, + ev.date_created, + b.height, + b.id, + CASE + WHEN last_chain_index.height < b.height THEN 0 + ELSE last_chain_index.height - b.height + END AS confirmations, + ev.event_type +FROM events ev INDEXED BY events_maturity_height_id_idx -- force the index to prevent temp-btree sorts +INNER JOIN event_addresses ea ON (ev.id = ea.event_id) +INNER JOIN address_balance sa ON (ea.address_id = sa.id) +INNER JOIN blocks b ON (ev.block_id = b.id) +CROSS JOIN last_chain_index +WHERE sa.address = $1 +ORDER BY ev.maturity_height DESC, ev.id DESC +LIMIT $2 OFFSET $3` - switch eventType { - case explorer.EventTypeTransaction: - var txnID int64 - var eventTx explorer.EventTransaction - err = tx.QueryRow(`SELECT transaction_id, fee FROM transaction_events WHERE event_id = ?`, eventID).Scan(&txnID, decode(&eventTx.Fee)) - if err != nil { - return explorer.Event{}, 0, fmt.Errorf("failed to fetch transaction ID: %w", err) - } - txns, err := getTransactions(tx, map[int64]transactionID{0: {dbID: txnID, id: types.TransactionID(ev.ID)}}) - if err != nil || len(txns) == 0 { - return explorer.Event{}, 0, fmt.Errorf("failed to fetch transaction: %w", err) - } - eventTx.Transaction = txns[0] - eventTx.HostAnnouncements = eventTx.Transaction.HostAnnouncements - ev.Data = &eventTx - case explorer.EventTypeV2Transaction: - var txnID int64 - err = tx.QueryRow(`SELECT transaction_id FROM v2_transaction_events WHERE event_id = ?`, eventID).Scan(&txnID) - if err != nil { - return explorer.Event{}, 0, fmt.Errorf("failed to fetch v2 transaction ID: %w", err) - } - txns, err := getV2Transactions(tx, []types.TransactionID{types.TransactionID(ev.ID)}) - if err != nil || len(txns) == 0 { - return explorer.Event{}, 0, fmt.Errorf("failed to fetch v2 transaction: %w", err) - } - eventTx := explorer.EventV2Transaction(txns[0]) - ev.Data = &eventTx - case explorer.EventTypeContractPayout: - var m explorer.EventContractPayout - err = tx.QueryRow(`SELECT sce.output_id, sce.leaf_index, sce.maturity_height, sce.address, sce.value, fce.contract_id, fce.leaf_index, fce.filesize, fce.file_merkle_root, fce.window_start, fce.window_end, fce.payout, fce.unlock_hash, fce.revision_number, ev.missed -FROM contract_payout_events ev -JOIN siacoin_elements sce ON ev.output_id = sce.id -JOIN file_contract_elements fce ON ev.contract_id = fce.id -WHERE ev.event_id = ?`, eventID).Scan(decode(&m.SiacoinOutput.ID), decode(&m.SiacoinOutput.StateElement.LeafIndex), &m.SiacoinOutput.MaturityHeight, decode(&m.SiacoinOutput.SiacoinOutput.Address), decode(&m.SiacoinOutput.SiacoinOutput.Value), decode(&m.FileContract.ID), decode(&m.FileContract.StateElement.LeafIndex), decode(&m.FileContract.FileContract.Filesize), decode(&m.FileContract.FileContract.FileMerkleRoot), decode(&m.FileContract.FileContract.WindowStart), decode(&m.FileContract.FileContract.WindowEnd), decode(&m.FileContract.FileContract.Payout), decode(&m.FileContract.FileContract.UnlockHash), decode(&m.FileContract.FileContract.RevisionNumber), &m.Missed) - ev.Data = &m - case explorer.EventTypeMinerPayout: - var m explorer.EventMinerPayout - err = tx.QueryRow(`SELECT sc.output_id, sc.leaf_index, sc.maturity_height, sc.address, sc.value -FROM siacoin_elements sc -INNER JOIN miner_payout_events ev ON ev.output_id = sc.id -WHERE ev.event_id = ?`, eventID).Scan(decode(&m.SiacoinOutput.ID), decode(&m.SiacoinOutput.StateElement.LeafIndex), decode(&m.SiacoinOutput.MaturityHeight), decode(&m.SiacoinOutput.SiacoinOutput.Address), decode(&m.SiacoinOutput.SiacoinOutput.Value)) + rows, err := tx.Query(query, encode(address), limit, offset) if err != nil { - return explorer.Event{}, 0, fmt.Errorf("failed to fetch miner payout event data: %w", err) + return err } - ev.Data = &m - case explorer.EventTypeFoundationSubsidy: - var m explorer.EventFoundationSubsidy - err = tx.QueryRow(`SELECT sc.output_id, sc.leaf_index, sc.maturity_height, sc.address, sc.value -FROM siacoin_elements sc -INNER JOIN foundation_subsidy_events ev ON ev.output_id = sc.id -WHERE ev.event_id = ?`, eventID).Scan(decode(&m.SiacoinOutput.ID), decode(&m.SiacoinOutput.StateElement.LeafIndex), decode(&m.SiacoinOutput.MaturityHeight), decode(&m.SiacoinOutput.SiacoinOutput.Address), decode(&m.SiacoinOutput.SiacoinOutput.Value)) - ev.Data = &m - default: - return explorer.Event{}, 0, fmt.Errorf("unknown event type: %s", eventType) - } - - if err != nil { - return explorer.Event{}, 0, fmt.Errorf("failed to fetch transaction event data: %w", err) - } + defer rows.Close() + for rows.Next() { + event, _, err := scanEvent(tx, rows) + if err != nil { + return fmt.Errorf("failed to scan event: %w", err) + } + event.Relevant = []types.Address{address} + events = append(events, event) + } + return rows.Err() + }) return } @@ -186,36 +162,6 @@ func (s *Store) HostsForScanning(maxLastScan, minLastAnnouncement time.Time, off return } -// AddressEvents returns the events of a single address. -func (s *Store) AddressEvents(address types.Address, offset, limit uint64) (events []explorer.Event, err error) { - err = s.transaction(func(tx *txn) error { - const query = `SELECT ev.id, ev.event_id, ev.maturity_height, ev.date_created, ev.height, ev.block_id, ev.event_type - FROM events ev - INNER JOIN event_addresses ea ON ev.id = ea.event_id - INNER JOIN address_balance sa ON ea.address_id = sa.id - WHERE sa.address = $1 - ORDER BY ev.maturity_height DESC, ev.id DESC - LIMIT $2 OFFSET $3` - - rows, err := tx.Query(query, encode(address), limit, offset) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - event, _, err := scanEvent(tx, rows) - if err != nil { - return fmt.Errorf("failed to scan event: %w", err) - } - - events = append(events, event) - } - return rows.Err() - }) - return -} - func scanSiacoinOutput(s scanner) (sco explorer.SiacoinOutput, err error) { var spentIndex types.ChainIndex err = s.Scan(decode(&sco.ID), decode(&sco.StateElement.LeafIndex), &sco.Source, decodeNull(&spentIndex), &sco.MaturityHeight, decode(&sco.SiacoinOutput.Address), decode(&sco.SiacoinOutput.Value)) diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 8330cbd..63408e4 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -1,11 +1,11 @@ package sqlite import ( - "bytes" "database/sql" - "encoding/json" "errors" "fmt" + "log" + "reflect" "time" "go.sia.tech/core/types" @@ -620,18 +620,18 @@ func addSiafundElements(tx *txn, index types.ChainIndex, spentElements, newEleme return sfDBIds, nil } -func addEvents(tx *txn, scDBIds map[types.SiacoinOutputID]int64, fcDBIds map[explorer.DBFileContract]int64, txnDBIds map[types.TransactionID]txnDBId, v2TxnDBIds map[types.TransactionID]txnDBId, events []explorer.Event) error { +func addEvents(tx *txn, bid types.BlockID, scDBIds map[types.SiacoinOutputID]int64, sfDBIds map[types.SiafundOutputID]int64, fcDBIds map[explorer.DBFileContract]int64, v2FcDBIds map[explorer.DBFileContract]int64, txnDBIds map[types.TransactionID]txnDBId, v2TxnDBIds map[types.TransactionID]txnDBId, events []explorer.Event) error { if len(events) == 0 { return nil } - insertEventStmt, err := tx.Prepare(`INSERT INTO events (event_id, maturity_height, date_created, event_type, block_id, height) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (event_id) DO NOTHING RETURNING id`) + insertEventStmt, err := tx.Prepare(`INSERT INTO events (event_id, maturity_height, date_created, event_type, block_id) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (event_id) DO NOTHING RETURNING id`) if err != nil { return fmt.Errorf("failed to prepare event statement: %w", err) } defer insertEventStmt.Close() - addrStmt, err := tx.Prepare(`INSERT INTO address_balance (address, siacoin_balance, immature_siacoin_balance, siafund_balance) VALUES ($1, $2, $3, 0) ON CONFLICT (address) DO UPDATE SET address=EXCLUDED.address RETURNING id`) + addrStmt, err := tx.Prepare(`INSERT INTO address_balance (address, siacoin_balance, immature_siacoin_balance, siafund_balance) VALUES ($1, $2, $2, 0) ON CONFLICT (address) DO UPDATE SET address=EXCLUDED.address RETURNING id`) if err != nil { return fmt.Errorf("failed to prepare address statement: %w", err) } @@ -643,11 +643,11 @@ func addEvents(tx *txn, scDBIds map[types.SiacoinOutputID]int64, fcDBIds map[exp } defer relevantAddrStmt.Close() - transactionEventStmt, err := tx.Prepare(`INSERT INTO transaction_events (event_id, transaction_id, fee) VALUES (?, ?, ?)`) + v1TransactionEventStmt, err := tx.Prepare(`INSERT INTO v1_transaction_events (event_id, transaction_id) VALUES (?, ?)`) if err != nil { - return fmt.Errorf("failed to prepare transaction event statement: %w", err) + return fmt.Errorf("failed to prepare v1 transaction event statement: %w", err) } - defer transactionEventStmt.Close() + defer v1TransactionEventStmt.Close() v2TransactionEventStmt, err := tx.Prepare(`INSERT INTO v2_transaction_events (event_id, transaction_id) VALUES (?, ?)`) if err != nil { @@ -655,72 +655,41 @@ func addEvents(tx *txn, scDBIds map[types.SiacoinOutputID]int64, fcDBIds map[exp } defer v2TransactionEventStmt.Close() - minerPayoutEventStmt, err := tx.Prepare(`INSERT INTO miner_payout_events (event_id, output_id) VALUES (?, ?)`) + payoutEventStmt, err := tx.Prepare(`INSERT INTO payout_events (event_id, output_id) VALUES (?, ?)`) if err != nil { - return fmt.Errorf("failed to prepare miner payout event statement: %w", err) + return fmt.Errorf("failed to prepare minerpayout event statement: %w", err) } - defer minerPayoutEventStmt.Close() + defer payoutEventStmt.Close() - contractPayoutEventStmt, err := tx.Prepare(`INSERT INTO contract_payout_events (event_id, output_id, contract_id, missed) VALUES (?, ?, ?, ?)`) + v1ContractResolutionEventStmt, err := tx.Prepare(`INSERT INTO v1_contract_resolution_events (event_id, output_id, parent_id, missed) VALUES (?, ?, ?, ?)`) if err != nil { - return fmt.Errorf("failed to prepare contract payout event statement: %w", err) + return fmt.Errorf("failed to prepare v1 contract resolution event statement: %w", err) } - defer contractPayoutEventStmt.Close() + defer v1ContractResolutionEventStmt.Close() - foundationSubsidyEventStmt, err := tx.Prepare(`INSERT INTO foundation_subsidy_events (event_id, output_id) VALUES (?, ?)`) + v2ContractResolutionEventStmt, err := tx.Prepare(`INSERT INTO v2_contract_resolution_events (event_id, output_id, parent_id, missed) VALUES (?, ?, ?, ?)`) if err != nil { - return fmt.Errorf("failed to prepare foundation subsidy event statement: %w", err) + return fmt.Errorf("failed to prepare v2 contract resolution event statement: %w", err) } - defer foundationSubsidyEventStmt.Close() + defer v2ContractResolutionEventStmt.Close() - var buf bytes.Buffer - enc := json.NewEncoder(&buf) for _, event := range events { - buf.Reset() - if err := enc.Encode(event.Data); err != nil { - return fmt.Errorf("failed to encode event: %w", err) - } - var eventID int64 - err = insertEventStmt.QueryRow(encode(event.ID), event.MaturityHeight, encode(event.Timestamp), event.Data.EventType(), encode(event.Index.ID), event.Index.Height).Scan(&eventID) + err = insertEventStmt.QueryRow(encode(event.ID), event.MaturityHeight, encode(event.Timestamp), event.Type, encode(bid)).Scan(&eventID) if errors.Is(err, sql.ErrNoRows) { continue // skip if the event already exists } else if err != nil { return fmt.Errorf("failed to add event: %w", err) } - switch v := event.Data.(type) { - case *explorer.EventTransaction: - dbID := txnDBIds[types.TransactionID(event.ID)].id - if _, err = transactionEventStmt.Exec(eventID, dbID, encode(v.Fee)); err != nil { - return fmt.Errorf("failed to insert transaction event: %w", err) - } - case *explorer.EventV2Transaction: - dbID := v2TxnDBIds[types.TransactionID(event.ID)].id - if _, err = v2TransactionEventStmt.Exec(eventID, dbID); err != nil { - return fmt.Errorf("failed to insert transaction event: %w", err) - } - case *explorer.EventMinerPayout: - _, err = minerPayoutEventStmt.Exec(eventID, scDBIds[types.SiacoinOutputID(event.ID)]) - case *explorer.EventContractPayout: - _, err = contractPayoutEventStmt.Exec(eventID, scDBIds[v.SiacoinOutput.ID], fcDBIds[explorer.DBFileContract{ID: v.FileContract.ID, RevisionNumber: v.FileContract.FileContract.RevisionNumber}], v.Missed) - case *explorer.EventFoundationSubsidy: - _, err = foundationSubsidyEventStmt.Exec(eventID, scDBIds[types.SiacoinOutputID(event.ID)]) - default: - return errors.New("unknown event type") - } - if err != nil { - return fmt.Errorf("failed to insert %s event: %w", event.Data.EventType(), err) - } - used := make(map[types.Address]bool) - for _, addr := range event.Addresses { + for _, addr := range event.Relevant { if used[addr] { continue } var addressID int64 - err = addrStmt.QueryRow(encode(addr), encode(types.ZeroCurrency), encode(types.ZeroCurrency)).Scan(&addressID) + err = addrStmt.QueryRow(encode(addr), encode(types.ZeroCurrency)).Scan(&addressID) if err != nil { return fmt.Errorf("failed to get address: %w", err) } @@ -732,6 +701,33 @@ func addEvents(tx *txn, scDBIds map[types.SiacoinOutputID]int64, fcDBIds map[exp used[addr] = true } + + switch v := event.Data.(type) { + case explorer.EventV1Transaction: + dbID := txnDBIds[types.TransactionID(event.ID)].id + if _, err = v1TransactionEventStmt.Exec(eventID, dbID); err != nil { + return fmt.Errorf("failed to insert transaction event: %w", err) + } + case explorer.EventV2Transaction: + dbID := v2TxnDBIds[types.TransactionID(event.ID)].id + if _, err = v2TransactionEventStmt.Exec(eventID, dbID); err != nil { + return fmt.Errorf("failed to insert transaction event: %w", err) + } + case explorer.EventPayout: + _, err = payoutEventStmt.Exec(eventID, scDBIds[types.SiacoinOutputID(event.ID)]) + case explorer.EventV1ContractResolution: + ddd := explorer.DBFileContract{ID: v.Parent.ID, RevisionNumber: v.Parent.RevisionNumber} + log.Printf("scDBIDs[%+v] = %v, fcDBIds[%+v] = %d", v.SiacoinElement.ID, scDBIds[v.SiacoinElement.ID], ddd, fcDBIds[ddd]) + + _, err = v1ContractResolutionEventStmt.Exec(eventID, scDBIds[v.SiacoinElement.ID], fcDBIds[explorer.DBFileContract{ID: v.Parent.ID, RevisionNumber: v.Parent.RevisionNumber}], v.Missed) + case explorer.EventV2ContractResolution: + _, err = v2ContractResolutionEventStmt.Exec(eventID, scDBIds[v.SiacoinElement.ID], v2FcDBIds[explorer.DBFileContract{ID: v.Resolution.Parent.ID, RevisionNumber: v.Resolution.Parent.V2FileContract.RevisionNumber}], v.Missed) + default: + return fmt.Errorf("unknown event type: %T", reflect.TypeOf(event.Data)) + } + if err != nil { + return fmt.Errorf("failed to insert %v event: %w", reflect.TypeOf(event.Data), err) + } } return nil } @@ -1062,12 +1058,12 @@ func (ut *updateTx) ApplyIndex(state explorer.UpdateState) error { return fmt.Errorf("ApplyIndex: failed to update metrics: %w", err) } else if err := addHostAnnouncements(ut.tx, state.Block.Timestamp, state.HostAnnouncements, state.V2HostAnnouncements); err != nil { return fmt.Errorf("ApplyIndex: failed to add host announcements: %w", err) - } else if err := addEvents(ut.tx, scDBIds, fcDBIds, txnDBIds, v2TxnDBIds, state.Events); err != nil { - return fmt.Errorf("ApplyIndex: failed to add events: %w", err) } else if err := updateFileContractIndices(ut.tx, false, state.Metrics.Index, state.FileContractElements); err != nil { return fmt.Errorf("ApplyIndex: failed to update file contract element indices: %w", err) } else if err := updateV2FileContractIndices(ut.tx, false, state.Metrics.Index, state.V2FileContractElements); err != nil { return fmt.Errorf("ApplyIndex: failed to update v2 file contract element indices: %w", err) + } else if err := addEvents(ut.tx, state.Block.ID(), scDBIds, sfDBIds, fcDBIds, v2FcDBIds, txnDBIds, v2TxnDBIds, state.Events); err != nil { + return fmt.Errorf("ApplyIndex: failed to add events: %w", err) } return nil diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index f5dffb7..f2fed79 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -498,6 +498,9 @@ func TestFileContract(t *testing.T) { pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) + pk2 := types.GeneratePrivateKey() + addr2 := types.StandardUnlockHash(pk2.PublicKey()) + renterPrivateKey := types.GeneratePrivateKey() renterPublicKey := renterPrivateKey.PublicKey() @@ -514,7 +517,7 @@ func TestFileContract(t *testing.T) { windowStart := cm.Tip().Height + 10 windowEnd := windowStart + 10 - fc := testutil.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), windowStart, windowEnd, types.VoidAddress) + fc := testutil.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), windowStart, windowEnd, addr2) txn := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ ParentID: scOutputID, @@ -679,6 +682,22 @@ func TestFileContract(t *testing.T) { StorageUtilization: 0, }) + { + events, err := db.AddressEvents(addr2, 0, math.MaxInt64) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "events", 2, len(events)) + + ev0 := events[0].Data.(explorer.EventV1ContractResolution) + testutil.Equal(t, "event 0 parent ID", fcID, ev0.Parent.ID) + testutil.Equal(t, "event 0 output ID", fcID.MissedOutputID(0), ev0.SiacoinElement.ID) + testutil.Equal(t, "event 0 missed", true, ev0.Missed) + + ev1 := events[1].Data.(explorer.EventV1Transaction) + testutil.CheckTransaction(t, txn, ev1.Transaction) + } + { dbFCs, err := db.Contracts([]types.FileContractID{fcID}) if err != nil { @@ -1619,11 +1638,9 @@ func TestHostAnnouncement(t *testing.T) { if err != nil { t.Fatal(err) } - if v, ok := events[0].Data.(*explorer.EventTransaction); !ok { - t.Fatal("expected EventTransaction") - } else { - testutil.CheckTransaction(t, txn1, v.Transaction) - } + testutil.Equal(t, "events", 2, len(events)) + testutil.CheckTransaction(t, txn1, events[0].Data.(explorer.EventV1Transaction).Transaction) + testutil.CheckTransaction(t, genesisBlock.Transactions[0], events[1].Data.(explorer.EventV1Transaction).Transaction) } { @@ -2107,6 +2124,16 @@ func TestMultipleReorgFileContract(t *testing.T) { testutil.CheckFC(t, false, false, false, fc, txns[0].FileContracts[0]) } + { + events, err := db.AddressEvents(addr1, 0, math.MaxInt64) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "events", 2, len(events)) + testutil.CheckTransaction(t, txn, events[0].Data.(explorer.EventV1Transaction).Transaction) + testutil.CheckTransaction(t, genesisBlock.Transactions[0], events[1].Data.(explorer.EventV1Transaction).Transaction) + } + uc := types.UnlockConditions{ PublicKeys: []types.UnlockKey{ renterPublicKey.UnlockKey(), @@ -2341,6 +2368,15 @@ func TestMultipleReorgFileContract(t *testing.T) { TotalHosts: 0, }) } + + { + events, err := db.AddressEvents(addr1, 0, math.MaxInt64) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "events", 1, len(events)) + testutil.CheckTransaction(t, genesisBlock.Transactions[0], events[0].Data.(explorer.EventV1Transaction).Transaction) + } } func TestMetricCirculatingSupply(t *testing.T) { diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index f9675b6..2fff9df 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -381,44 +381,51 @@ CREATE TABLE state_tree ( CREATE TABLE events ( id INTEGER PRIMARY KEY, + block_id BLOB NOT NULL REFERENCES blocks(id) ON DELETE CASCADE, event_id BLOB UNIQUE NOT NULL, maturity_height INTEGER NOT NULL, date_created INTEGER NOT NULL, - event_type TEXT NOT NULL, - block_id BLOB NOT NULL REFERENCES blocks(id) ON DELETE CASCADE, -- add an index to all foreign keys - height INTEGER NOT NULL + event_type TEXT NOT NULL ); -CREATE INDEX events_block_id_height_index ON events(block_id, height); +CREATE INDEX events_block_id_idx ON events (block_id); +CREATE INDEX events_maturity_height_id_idx ON events (maturity_height DESC, id DESC); CREATE TABLE event_addresses ( - event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE, - address_id INTEGER NOT NULL REFERENCES address_balance(id), + event_id INTEGER NOT NULL REFERENCES events (id) ON DELETE CASCADE, + address_id INTEGER NOT NULL REFERENCES address_balance (id), PRIMARY KEY (event_id, address_id) ); -CREATE INDEX event_addresses_event_id_index ON event_addresses(event_id); -CREATE INDEX event_addresses_address_id_index ON event_addresses(address_id); +CREATE INDEX event_addresses_event_id_idx ON event_addresses (event_id); +CREATE INDEX event_addresses_address_id_idx ON event_addresses (address_id); +CREATE INDEX event_addresses_event_id_address_id_idx ON event_addresses (event_id, address_id); -CREATE TABLE transaction_events ( +CREATE TABLE v1_transaction_events ( event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL, - transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL, - fee BLOB NOT NULL + transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL ); -CREATE TABLE contract_payout_events ( +CREATE TABLE v2_transaction_events ( event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL, - output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL, - contract_id INTEGER REFERENCES file_contract_elements(id) ON DELETE CASCADE NOT NULL, - missed INTEGER NOT NULL + transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL ); -CREATE TABLE miner_payout_events ( +CREATE TABLE payout_events ( event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL, output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL ); -CREATE TABLE foundation_subsidy_events ( +CREATE TABLE v1_contract_resolution_events ( event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL, - output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL + parent_id INTEGER REFERENCES file_contract_elements(id) ON DELETE CASCADE NOT NULL, + output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL, + missed INTEGER NOT NULL +); + +CREATE TABLE v2_contract_resolution_events ( + event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL, + parent_id INTEGER REFERENCES v2_file_contract_elements(id) ON DELETE CASCADE NOT NULL, + output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL, + missed INTEGER NOT NULL ); CREATE TABLE v2_file_contract_elements ( @@ -465,11 +472,6 @@ CREATE TABLE v2_last_contract_revision ( contract_element_id INTEGER UNIQUE REFERENCES v2_file_contract_elements(id) ON DELETE CASCADE NOT NULL ); -CREATE TABLE v2_transaction_events ( - event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL, - transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL -); - CREATE TABLE host_info ( public_key BLOB PRIMARY KEY NOT NULL, net_address TEXT NOT NULL, @@ -573,4 +575,4 @@ CREATE TABLE host_info_v2_netaddresses( CREATE INDEX host_info_v2_netaddresses_public_key ON host_info_v2_netaddresses(public_key); -- initialize the global settings table -INSERT INTO global_settings (id, db_version) VALUES (0, 0); -- should not be changed +INSERT INTO global_settings (id, db_version) VALUES (0, 0); -- should not be changed \ No newline at end of file diff --git a/persist/sqlite/v2consensus_test.go b/persist/sqlite/v2consensus_test.go index 24a883e..e1839a0 100644 --- a/persist/sqlite/v2consensus_test.go +++ b/persist/sqlite/v2consensus_test.go @@ -242,6 +242,18 @@ func TestV2FoundationAddress(t *testing.T) { } testutil.CheckV2Transaction(t, txn1, dbTxns[0]) } + + { + events, err := db.AddressEvents(addr1, 0, math.MaxInt64) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "events", 3, len(events)) + + testutil.Equal(t, "event 0 type", "foundation", events[0].Type) + testutil.Equal(t, "event 1 type", "v2Transaction", events[1].Type) + testutil.Equal(t, "event 2 type", "v1Transaction", events[2].Type) + } } func TestV2Attestations(t *testing.T) { @@ -295,11 +307,10 @@ func TestV2Attestations(t *testing.T) { if err != nil { t.Fatal(err) } - if v, ok := events[0].Data.(*explorer.EventV2Transaction); !ok { - t.Fatal("expected EventV2Transaction") - } else { - testutil.CheckV2Transaction(t, txn1, explorer.V2Transaction(*v)) - } + testutil.Equal(t, "events", 2, len(events)) + + testutil.CheckV2Transaction(t, txn1, explorer.V2Transaction(events[0].Data.(explorer.EventV2Transaction))) + testutil.CheckTransaction(t, genesisBlock.Transactions[0], events[1].Data.(explorer.EventV1Transaction).Transaction) } { @@ -952,6 +963,9 @@ func TestV2FileContractResolution(t *testing.T) { addr1 := types.StandardUnlockHash(pk1.PublicKey()) addr1Policy := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + pk2 := types.GeneratePrivateKey() + addr2 := types.StandardUnlockHash(pk2.PublicKey()) + renterPrivateKey := types.GeneratePrivateKey() renterPublicKey := renterPrivateKey.PublicKey() @@ -965,7 +979,7 @@ func TestV2FileContractResolution(t *testing.T) { }) giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value - v1FC := testutil.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), 100, 105, types.VoidAddress) + v1FC := testutil.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), 100, 105, addr2) v1FC.Filesize = 65 data := make([]byte, 2*rhp2.LeafSize) @@ -1164,6 +1178,50 @@ func TestV2FileContractResolution(t *testing.T) { testutil.Equal(t, "resolution transaction ID", txn4.ID(), *dbTxns[0].FileContractResolutions[0].Parent.ResolutionTransactionID) } + { + events, err := db.AddressEvents(addr2, 0, math.MaxInt64) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "events", 3, len(events)) + + ev0 := events[0].Data.(explorer.EventV2ContractResolution) + testutil.Equal(t, "event 0 parent ID", v2FC3ID, ev0.Resolution.Parent.ID) + testutil.Equal(t, "event 0 output ID", v2FC3ID.V2RenterOutputID(), ev0.SiacoinElement.ID) + testutil.Equal(t, "event 0 missed", true, ev0.Missed) + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn4.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "event 0 resolution", dbTxns[0].FileContractResolutions[0], ev0.Resolution) + } + + ev1 := events[1].Data.(explorer.EventV2ContractResolution) + testutil.Equal(t, "event 1 parent ID", v2FC2ID, ev1.Resolution.Parent.ID) + testutil.Equal(t, "event 1 output ID", v2FC2ID.V2RenterOutputID(), ev1.SiacoinElement.ID) + testutil.Equal(t, "event 1 missed", false, ev1.Missed) + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn3.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "event 1 resolution", dbTxns[0].FileContractResolutions[0], ev1.Resolution) + } + + ev2 := events[2].Data.(explorer.EventV2ContractResolution) + testutil.Equal(t, "event 2 parent ID", v2FC1ID, ev2.Resolution.Parent.ID) + testutil.Equal(t, "event 2 output ID", v2FC1ID.V2RenterOutputID(), ev2.SiacoinElement.ID) + testutil.Equal(t, "event 2 missed", false, ev2.Missed) + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn2.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.Equal(t, "event 2 resolution", dbTxns[0].FileContractResolutions[0], ev2.Resolution) + } + } + // revert the block { state := prevState From 1400b0325d1307c00140930bbd5e691029026cc1 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Sun, 15 Dec 2024 23:59:51 -0500 Subject: [PATCH 2/4] add events.go --- persist/sqlite/events.go | 154 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 persist/sqlite/events.go diff --git a/persist/sqlite/events.go b/persist/sqlite/events.go new file mode 100644 index 0000000..6971287 --- /dev/null +++ b/persist/sqlite/events.go @@ -0,0 +1,154 @@ +package sqlite + +import ( + "database/sql" + "errors" + "fmt" + + "go.sia.tech/core/types" + "go.sia.tech/coreutils/wallet" + "go.sia.tech/explored/explorer" +) + +// Events returns the events with the given event IDs. If an event is not found, +// it is skipped. +func (s *Store) Events(eventIDs []types.Hash256) (events []explorer.Event, err error) { + err = s.transaction(func(tx *txn) error { + // sqlite doesn't have easy support for IN clauses, use a statement since + // the number of event IDs is likely to be small instead of dynamically + // building the query + const query = ` +WITH last_chain_index (height) AS ( + SELECT MAX(height) FROM blocks +) +SELECT + ev.id, + ev.event_id, + ev.maturity_height, + ev.date_created, + b.height, + b.id, + CASE + WHEN last_chain_index.height < b.height THEN 0 + ELSE last_chain_index.height - b.height + END AS confirmations, + ev.event_type +FROM events ev +INNER JOIN event_addresses ea ON (ev.id = ea.event_id) +INNER JOIN address_balance sa ON (ea.address_id = sa.id) +INNER JOIN blocks b ON (ev.block_id = b.id) +CROSS JOIN last_chain_index +WHERE ev.event_id = $1` + + stmt, err := tx.Prepare(query) + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Close() + + events = make([]explorer.Event, 0, len(eventIDs)) + for _, id := range eventIDs { + event, _, err := scanEvent(tx, stmt.QueryRow(encode(id))) + if errors.Is(err, sql.ErrNoRows) { + continue + } else if err != nil { + return fmt.Errorf("failed to query transaction %q: %w", id, err) + } + events = append(events, event) + } + return nil + }) + return +} + +func scanEvent(tx *txn, s scanner) (ev explorer.Event, eventID int64, err error) { + err = s.Scan(&eventID, decode(&ev.ID), &ev.MaturityHeight, decode(&ev.Timestamp), &ev.Index.Height, decode(&ev.Index.ID), &ev.Confirmations, &ev.Type) + if err != nil { + return + } + + switch ev.Type { + case wallet.EventTypeV1Transaction: + var txnID int64 + err = tx.QueryRow(`SELECT transaction_id FROM v1_transaction_events WHERE event_id = ?`, eventID).Scan(&txnID) + if err != nil { + return explorer.Event{}, 0, fmt.Errorf("failed to fetch v1 transaction ID: %w", err) + } + txns, err := getTransactions(tx, map[int64]transactionID{0: {dbID: txnID, id: types.TransactionID(ev.ID)}}) + if err != nil || len(txns) == 0 { + return explorer.Event{}, 0, fmt.Errorf("failed to fetch v1 transaction: %w", err) + } + ev.Data = explorer.EventV1Transaction{ + Transaction: txns[0], + } + case wallet.EventTypeV2Transaction: + txns, err := getV2Transactions(tx, []types.TransactionID{types.TransactionID(ev.ID)}) + if err != nil || len(txns) == 0 { + return explorer.Event{}, 0, fmt.Errorf("failed to fetch v2 transaction: %w", err) + } + ev.Data = explorer.EventV2Transaction(txns[0]) + case wallet.EventTypeV1ContractResolution: + var resolution explorer.EventV1ContractResolution + fce, sce := &resolution.Parent, &resolution.SiacoinElement + err := tx.QueryRow(`SELECT sce.output_id, sce.leaf_index, sce.maturity_height, sce.address, sce.value, fce.contract_id, fce.filesize, fce.file_merkle_root, fce.window_start, fce.window_end, fce.payout, fce.unlock_hash, fce.revision_number, ev.missed + FROM v1_contract_resolution_events ev + JOIN siacoin_elements sce ON ev.output_id = sce.id + JOIN file_contract_elements fce ON ev.parent_id = fce.id + WHERE ev.event_id = ?`, eventID).Scan(decode(&sce.ID), decode(&sce.StateElement.LeafIndex), decode(&sce.MaturityHeight), decode(&sce.SiacoinOutput.Address), decode(&sce.SiacoinOutput.Value), decode(&fce.ID), decode(&fce.Filesize), decode(&fce.FileMerkleRoot), decode(&fce.WindowStart), decode(&fce.WindowEnd), decode(&fce.Payout), decode(&fce.UnlockHash), decode(&fce.RevisionNumber), &resolution.Missed) + if err != nil { + return explorer.Event{}, 0, fmt.Errorf("failed to retrieve v1 resolution event: %w", err) + } + ev.Data = resolution + case wallet.EventTypeV2ContractResolution: + var resolution explorer.EventV2ContractResolution + var parentContractID types.FileContractID + var resolutionTransactionID types.TransactionID + sce := &resolution.SiacoinElement + err := tx.QueryRow(`SELECT sce.output_id, sce.leaf_index, sce.maturity_height, sce.address, sce.value, rev.contract_id, rev.resolution_transaction_id, ev.missed + FROM v2_contract_resolution_events ev + JOIN siacoin_elements sce ON ev.output_id = sce.id + JOIN v2_file_contract_elements fce ON ev.parent_id = fce.id + JOIN v2_last_contract_revision rev ON fce.contract_id = rev.contract_id + WHERE ev.event_id = ?`, eventID).Scan(decode(&sce.ID), decode(&sce.StateElement.LeafIndex), decode(&sce.MaturityHeight), decode(&sce.SiacoinOutput.Address), decode(&sce.SiacoinOutput.Value), decode(&parentContractID), decode(&resolutionTransactionID), &resolution.Missed) + if err != nil { + return explorer.Event{}, 0, fmt.Errorf("failed to retrieve v2 resolution event: %w", err) + } + + resolutionTxns, err := getV2Transactions(tx, []types.TransactionID{resolutionTransactionID}) + if err != nil { + return explorer.Event{}, 0, fmt.Errorf("failed to get transaction with v2 resolution: %w", err) + } else if len(resolutionTxns) == 0 { + return explorer.Event{}, 0, fmt.Errorf("v2 resolution transaction not found") + } + txn := resolutionTxns[0] + + found := false + for _, fcr := range txn.FileContractResolutions { + if fcr.Parent.ID == parentContractID { + found = true + resolution.Resolution = fcr + break + } + } + if !found { + return explorer.Event{}, 0, fmt.Errorf("failed to find resolution in v2 resolution transaction") + } + + ev.Data = resolution + case wallet.EventTypeSiafundClaim, wallet.EventTypeMinerPayout, wallet.EventTypeFoundationSubsidy: + var payout explorer.EventPayout + sce := &payout.SiacoinElement + err := tx.QueryRow(`SELECT sce.output_id, sce.leaf_index, sce.maturity_height, sce.address, sce.value + FROM payout_events ev + JOIN siacoin_elements sce ON ev.output_id = sce.id + WHERE ev.event_id = ?`, eventID).Scan(decode(&sce.ID), decode(&sce.StateElement.LeafIndex), decode(&sce.MaturityHeight), decode(&sce.SiacoinOutput.Address), decode(&sce.SiacoinOutput.Value)) + if err != nil { + return explorer.Event{}, 0, fmt.Errorf("failed to retrieve payout event: %w", err) + } + ev.Data = payout + default: + return explorer.Event{}, 0, fmt.Errorf("unknown event type: %q", ev.Type) + } + + return +} From a02681fcefcb431e3bde874e31cb61286ea97f00 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Thu, 19 Dec 2024 09:10:09 -0500 Subject: [PATCH 3/4] remove SpentSiacoinElements/SpentSiafundElements because the explorer types already describe the value of SC/SF inputs --- explorer/events.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/explorer/events.go b/explorer/events.go index 30df1f2..bf145bc 100644 --- a/explorer/events.go +++ b/explorer/events.go @@ -19,10 +19,6 @@ type ( // siafund elements. EventV1Transaction struct { Transaction Transaction `json:"transaction"` - // v1 siacoin inputs do not describe the value of the spent utxo - SpentSiacoinElements []SiacoinOutput `json:"spentSiacoinElements,omitempty"` - // v1 siafund inputs do not describe the value of the spent utxo - SpentSiafundElements []SiacoinOutput `json:"spentSiafundElements,omitempty"` } // An EventV1ContractResolution represents a file contract payout from a v1 @@ -122,7 +118,6 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) (events [] continue } - // e.SpentSiacoinElements = append(e.SpentSiacoinElements, sce) addresses[sce.SiacoinOutput.Address] = struct{}{} } for _, sco := range txn.SiacoinOutputs { @@ -135,7 +130,6 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) (events [] continue } - // e.SpentSiafundElements = append(e.SpentSiafundElements, sfe) addresses[sfe.SiafundOutput.Address] = struct{}{} sce, ok := sces[sfi.ParentID.ClaimOutputID()] From eda230e53cb2388b2c0ef563b29cf091e2d46d51 Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Thu, 19 Dec 2024 09:14:42 -0500 Subject: [PATCH 4/4] reduce code duplication for v2 events --- explorer/events.go | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/explorer/events.go b/explorer/events.go index bf145bc..d1350d7 100644 --- a/explorer/events.go +++ b/explorer/events.go @@ -275,9 +275,8 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) (events [] panic("unknown resolution type") } - efc := V2FileContract{V2FileContractElement: fce} - { - element := sces[types.FileContractID(fce.ID).V2HostOutputID()] + addV2Resolution := func(element types.SiacoinElement) { + efc := V2FileContract{V2FileContractElement: fce} addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeV2ContractResolution, EventV2ContractResolution{ Resolution: V2FileContractResolution{ Parent: efc, @@ -286,21 +285,10 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) (events [] }, SiacoinElement: SiacoinOutput{SiacoinElement: element}, Missed: missed, - }, []types.Address{fce.V2FileContract.HostOutput.Address}) - } - - { - element := sces[types.FileContractID(fce.ID).V2RenterOutputID()] - addEvent(types.Hash256(element.ID), element.MaturityHeight, wallet.EventTypeV2ContractResolution, EventV2ContractResolution{ - Resolution: V2FileContractResolution{ - Parent: efc, - Type: typ, - Resolution: res, - }, - SiacoinElement: SiacoinOutput{SiacoinElement: element}, - Missed: missed, - }, []types.Address{fce.V2FileContract.RenterOutput.Address}) + }, []types.Address{element.SiacoinOutput.Address}) } + addV2Resolution(sces[types.FileContractID(fce.ID).V2RenterOutputID()]) + addV2Resolution(sces[types.FileContractID(fce.ID).V2HostOutputID()]) }) // handle block rewards