diff --git a/explorer/types.go b/explorer/types.go index 57733697..4f859320 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -171,6 +171,9 @@ type V2Transaction struct { ID types.TransactionID `json:"id"` ArbitraryData []byte `json:"arbitraryData,omitempty"` + NewFoundationAddress *types.Address `json:"newFoundationAddress,omitempty"` + MinerFee types.Currency `json:"minerFee"` + HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements,omitempty"` } @@ -205,6 +208,8 @@ type Metrics struct { SiafundPool types.Currency `json:"siafundPool"` // Total announced hosts TotalHosts uint64 `json:"totalHosts"` + // Number of leaves in the accumulator + NumLeaves uint64 `json:"numLeaves"` // Number of active contracts ActiveContracts uint64 `json:"activeContracts"` // Number of failed contracts diff --git a/explorer/update.go b/explorer/update.go index f5447503..0db1701f 100644 --- a/explorer/update.go +++ b/explorer/update.go @@ -199,6 +199,7 @@ func applyChainUpdate(tx UpdateTx, cau chain.ApplyUpdate) error { state.Metrics.Index = cau.State.Index state.Metrics.Difficulty = cau.State.Difficulty state.Metrics.SiafundPool = cau.State.SiafundPool + state.Metrics.NumLeaves = cau.State.Elements.NumLeaves return tx.ApplyIndex(state) } diff --git a/go.mod b/go.mod index a2602f86..f166c60c 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ toolchain go1.23.2 require ( github.com/mattn/go-sqlite3 v1.14.24 - go.sia.tech/core v0.4.7 - go.sia.tech/coreutils v0.4.1 + go.sia.tech/core v0.4.8-0.20241015191424-3a45c8b415e7 + go.sia.tech/coreutils v0.4.2-0.20241007200058-9a2654c61a97 go.sia.tech/jape v0.12.1 go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 @@ -23,8 +23,8 @@ require ( go.etcd.io/bbolt v1.3.11 // indirect go.sia.tech/mux v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.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 eb8ca55f..5323c9a1 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,12 @@ 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.7 h1:UAyErZ3nk5/7N0gIG0OEEJJrxh7ru8lgGLlaNtT/Jq0= go.sia.tech/core v0.4.7/go.mod h1:j2Ke8ihV8or7d2VDrFZWcCkwSVHO0DNMQJAGs9Qop2M= +go.sia.tech/core v0.4.8-0.20241015191424-3a45c8b415e7 h1:xN8iVwYd5/qGYCBHejI5vzxXt8P7kAGelyFK+nKGfSc= +go.sia.tech/core v0.4.8-0.20241015191424-3a45c8b415e7/go.mod h1:CpiFY0jL5OlU6sm/6fwd6/LQe6Ao8G6OtHtq21ggIoA= 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/coreutils v0.4.2-0.20241007200058-9a2654c61a97 h1:DPK4fA7HNdTgb02fsdFHbtaB2+ydy/78M1sHhznQkMw= +go.sia.tech/coreutils v0.4.2-0.20241007200058-9a2654c61a97/go.mod h1:JIaR+zdGZsqPLBM5mVsnwWJ7hBsES+SAEDQg5EFBitM= 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= @@ -35,12 +39,16 @@ 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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.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/internal/testutil/chain.go b/internal/testutil/chain.go index c7e04fe9..4b2e1dbf 100644 --- a/internal/testutil/chain.go +++ b/internal/testutil/chain.go @@ -78,11 +78,18 @@ func CreateAnnouncement(priv types.PrivateKey, netAddress string) []byte { // MineBlock mines sets the metadata fields of the block along with the // transactions and then generates a valid nonce for the block. func MineBlock(state consensus.State, txns []types.Transaction, minerAddr types.Address) types.Block { + reward := state.BlockReward() + for _, txn := range txns { + for _, fee := range txn.MinerFees { + reward = reward.Add(fee) + } + } + b := types.Block{ ParentID: state.Index.ID, Timestamp: types.CurrentTimestamp(), Transactions: txns, - MinerPayouts: []types.SiacoinOutput{{Address: minerAddr, Value: state.BlockReward()}}, + MinerPayouts: []types.SiacoinOutput{{Address: minerAddr, Value: reward}}, } if !coreutils.FindBlockNonce(state, &b, time.Minute) { panic("failed to mine test block quickly enough") @@ -93,10 +100,15 @@ func MineBlock(state consensus.State, txns []types.Transaction, minerAddr types. // MineV2Block mines sets the metadata fields of the block along with the // transactions and then generates a valid nonce for the block. func MineV2Block(state consensus.State, txns []types.V2Transaction, minerAddr types.Address) types.Block { + reward := state.BlockReward() + for _, txn := range txns { + reward = reward.Add(txn.MinerFee) + } + b := types.Block{ ParentID: state.Index.ID, Timestamp: types.CurrentTimestamp(), - MinerPayouts: []types.SiacoinOutput{{Address: minerAddr, Value: state.BlockReward()}}, + MinerPayouts: []types.SiacoinOutput{{Address: minerAddr, Value: reward}}, V2: &types.V2BlockData{ Transactions: txns, @@ -142,3 +154,40 @@ func SignTransaction(cs consensus.State, pk types.PrivateKey, txn *types.Transac } SignTransactionWithContracts(cs, pk, types.PrivateKey{}, types.PrivateKey{}, txn) } + +// SignV2TransactionWithContracts signs a transaction using the specified +// private keys, including contracts and revisions. +func SignV2TransactionWithContracts(cs consensus.State, pk, renterPK, hostPK types.PrivateKey, txn *types.V2Transaction) { + for i := range txn.SiacoinInputs { + txn.SiacoinInputs[i].SatisfiedPolicy.Signatures = []types.Signature{pk.SignHash(cs.InputSigHash(*txn))} + } + for i := range txn.SiafundInputs { + txn.SiafundInputs[i].SatisfiedPolicy.Signatures = []types.Signature{pk.SignHash(cs.InputSigHash(*txn))} + } + for i := range txn.FileContracts { + txn.FileContracts[i].RenterSignature = renterPK.SignHash(cs.ContractSigHash(txn.FileContracts[i])) + txn.FileContracts[i].HostSignature = hostPK.SignHash(cs.ContractSigHash(txn.FileContracts[i])) + } + for i := range txn.FileContractRevisions { + txn.FileContractRevisions[i].Revision.RenterSignature = renterPK.SignHash(cs.ContractSigHash(txn.FileContractRevisions[i].Revision)) + txn.FileContractRevisions[i].Revision.HostSignature = hostPK.SignHash(cs.ContractSigHash(txn.FileContractRevisions[i].Revision)) + } + for i := range txn.FileContractResolutions { + switch r := txn.FileContractResolutions[i].Resolution.(type) { + case *types.V2FileContractRenewal: + r.RenterSignature = renterPK.SignHash(cs.RenewalSigHash(*r)) + r.HostSignature = hostPK.SignHash(cs.RenewalSigHash(*r)) + case *types.V2FileContractFinalization: + *r = types.V2FileContractFinalization(renterPK.SignHash(cs.ContractSigHash(txn.FileContractResolutions[i].Parent.V2FileContract))) + } + } +} + +// SignV2Transaction signs a transaction that does not have any contracts with +// the specified private key. +func SignV2Transaction(cs consensus.State, pk types.PrivateKey, txn *types.V2Transaction) { + if len(txn.FileContracts) > 0 || len(txn.FileContractRevisions) > 0 { + panic("use SignV2TransactionWithContracts instead") + } + SignV2TransactionWithContracts(cs, pk, types.PrivateKey{}, types.PrivateKey{}, txn) +} diff --git a/internal/testutil/check.go b/internal/testutil/check.go index 95fbe4f3..9f112dad 100644 --- a/internal/testutil/check.go +++ b/internal/testutil/check.go @@ -108,6 +108,9 @@ func CheckTransaction(t *testing.T, expectTxn types.Transaction, gotTxn explorer func CheckV2Transaction(t *testing.T, expectTxn types.V2Transaction, gotTxn explorer.V2Transaction) { t.Helper() + Equal(t, "new foundation address", expectTxn.NewFoundationAddress, gotTxn.NewFoundationAddress) + Equal(t, "miner fee", expectTxn.MinerFee, gotTxn.MinerFee) + Equal(t, "arbitrary data", len(expectTxn.ArbitraryData), len(gotTxn.ArbitraryData)) for i := range expectTxn.ArbitraryData { diff --git a/persist/sqlite/addresses.go b/persist/sqlite/addresses.go index 73166838..e4326b33 100644 --- a/persist/sqlite/addresses.go +++ b/persist/sqlite/addresses.go @@ -53,7 +53,7 @@ func scanEvent(tx *txn, s scanner) (ev explorer.Event, eventID int64, err error) if err != nil { return explorer.Event{}, 0, fmt.Errorf("failed to fetch v2 transaction ID: %w", err) } - txns, err := getV2Transactions(tx, map[int64]transactionID{txnID: {id: types.TransactionID(ev.ID)}}) + 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) } diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 65fec026..a66d3f08 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -1006,11 +1006,12 @@ func updateFileContractIndices(tx *txn, revert bool, index types.ChainIndex, fce } func addMetrics(tx *txn, s explorer.UpdateState) error { - _, err := tx.Exec(`INSERT INTO network_metrics(block_id, height, difficulty, siafund_pool, total_hosts, active_contracts, failed_contracts, successful_contracts, storage_utilization, circulating_supply, contract_revenue) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + _, err := tx.Exec(`INSERT INTO network_metrics(block_id, height, difficulty, siafund_pool, num_leaves, total_hosts, active_contracts, failed_contracts, successful_contracts, storage_utilization, circulating_supply, contract_revenue) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, encode(s.Metrics.Index.ID), s.Metrics.Index.Height, encode(s.Metrics.Difficulty), encode(s.Metrics.SiafundPool), + encode(s.Metrics.NumLeaves), s.Metrics.TotalHosts, s.Metrics.ActiveContracts, s.Metrics.FailedContracts, diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 485fd494..e1235629 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "go.sia.tech/core/consensus" "go.sia.tech/core/types" "go.sia.tech/coreutils" "go.sia.tech/coreutils/chain" @@ -16,7 +17,7 @@ import ( "go.uber.org/zap/zaptest" ) -func syncDB(t *testing.T, db *sqlite.Store, cm *chain.Manager) { +func syncDB(t *testing.T, db explorer.Store, cm *chain.Manager) { index, err := db.Tip() if err != nil && !errors.Is(err, explorer.ErrNoTip) { t.Fatal(err) @@ -40,6 +41,44 @@ func syncDB(t *testing.T, db *sqlite.Store, cm *chain.Manager) { } } +func newStore(t *testing.T, v2 bool, f func(*consensus.Network, types.Block)) (*consensus.Network, types.Block, *chain.Manager, explorer.Store) { + log := zaptest.NewLogger(t) + dir := t.TempDir() + db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) + if err != nil { + t.Fatal(err) + } + + bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) + if err != nil { + t.Fatal(err) + } + + var network *consensus.Network + var genesisBlock types.Block + if v2 { + network, genesisBlock = ctestutil.V2Network() + } else { + network, genesisBlock = ctestutil.Network() + } + if f != nil { + f(network, genesisBlock) + } + + store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) + if err != nil { + t.Fatal(err) + } + cm := chain.NewManager(store, genesisState) + syncDB(t, db, cm) + + t.Cleanup(func() { + db.Close() + bdb.Close() + }) + return network, genesisBlock, cm, db +} + // CheckMetrics checks the that the metrics from the DB match what we expect. func CheckMetrics(t *testing.T, db explorer.Store, cm *chain.Manager, expected explorer.Metrics) { t.Helper() @@ -94,28 +133,7 @@ func CheckFCRevisions(t *testing.T, confirmationIndex types.ChainIndex, confirma } func TestBalance(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - - network, genesisBlock := ctestutil.Network() - - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) + _, _, cm, db := newStore(t, false, nil) // Generate three addresses: addr1, addr2, addr3 pk1 := types.GeneratePrivateKey() @@ -198,20 +216,6 @@ func TestBalance(t *testing.T) { } func TestSiafundBalance(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - // Generate three addresses: addr1, addr2, addr3 pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) @@ -222,17 +226,11 @@ func TestSiafundBalance(t *testing.T) { pk3 := types.GeneratePrivateKey() addr3 := types.StandardUnlockHash(pk3.PublicKey()) - network, genesisBlock := ctestutil.Network() - genesisBlock.Transactions[0].SiafundOutputs[0].Address = addr1 + _, genesisBlock, cm, db := newStore(t, false, func(network *consensus.Network, genesisBlock types.Block) { + genesisBlock.Transactions[0].SiafundOutputs[0].Address = addr1 + }) giftSF := genesisBlock.Transactions[0].SiafundOutputs[0].Value - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - // Send all of the payout except 100 SF to addr2 unlockConditions := types.StandardUnlockConditions(pk1.PublicKey()) parentTxn := types.Transaction{ @@ -276,20 +274,6 @@ func TestSiafundBalance(t *testing.T) { } func TestSendTransactions(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - // Generate three addresses: addr1, addr2, addr3 pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) @@ -300,17 +284,11 @@ func TestSendTransactions(t *testing.T) { pk3 := types.GeneratePrivateKey() addr3 := types.StandardUnlockHash(pk3.PublicKey()) - network, genesisBlock := ctestutil.Network() - genesisBlock.Transactions[0].SiafundOutputs[0].Address = addr1 + _, genesisBlock, cm, db := newStore(t, false, func(network *consensus.Network, genesisBlock types.Block) { + genesisBlock.Transactions[0].SiafundOutputs[0].Address = addr1 + }) giftSF := genesisBlock.Transactions[0].SiafundOutputs[0].Value - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - expectedPayout := cm.TipState().BlockReward() maturityHeight := cm.TipState().MaturityHeight() @@ -465,28 +443,7 @@ func TestSendTransactions(t *testing.T) { } func TestTip(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - - network, genesisBlock := ctestutil.Network() - - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) + _, _, cm, db := newStore(t, false, nil) const n = 100 for i := cm.Tip().Height; i < n; i++ { @@ -514,21 +471,6 @@ func TestTip(t *testing.T) { } func TestFileContract(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) @@ -538,17 +480,11 @@ func TestFileContract(t *testing.T) { hostPrivateKey := types.GeneratePrivateKey() hostPublicKey := hostPrivateKey.PublicKey() - network, genesisBlock := ctestutil.Network() - genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + _, genesisBlock, cm, db := newStore(t, false, func(network *consensus.Network, genesisBlock types.Block) { + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + }) giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - scOutputID := genesisBlock.Transactions[0].SiacoinOutputID(0) unlockConditions := types.StandardUnlockConditions(pk1.PublicKey()) @@ -770,21 +706,6 @@ func TestFileContract(t *testing.T) { } func TestEphemeralFileContract(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) @@ -794,17 +715,11 @@ func TestEphemeralFileContract(t *testing.T) { hostPrivateKey := types.GeneratePrivateKey() hostPublicKey := hostPrivateKey.PublicKey() - network, genesisBlock := ctestutil.Network() - genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + _, genesisBlock, cm, db := newStore(t, false, func(network *consensus.Network, genesisBlock types.Block) { + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + }) giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - scOutputID := genesisBlock.Transactions[0].SiacoinOutputID(0) unlockConditions := types.StandardUnlockConditions(pk1.PublicKey()) @@ -1019,35 +934,15 @@ func TestEphemeralFileContract(t *testing.T) { } func TestRevertTip(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - - network, genesisBlock := ctestutil.Network() - - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) pk2 := types.GeneratePrivateKey() addr2 := types.StandardUnlockHash(pk2.PublicKey()) + _, _, cm, db := newStore(t, false, nil) + genesisState := cm.TipState() + const n = 100 for i := cm.Tip().Height; i < n; i++ { if err := cm.AddBlocks([]types.Block{testutil.MineBlock(cm.TipState(), nil, addr1)}); err != nil { @@ -1107,29 +1002,6 @@ func TestRevertTip(t *testing.T) { } func TestRevertBalance(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - - network, genesisBlock := ctestutil.Network() - - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - // Generate three addresses: addr1, addr2, addr3 pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) @@ -1140,6 +1012,9 @@ func TestRevertBalance(t *testing.T) { pk3 := types.GeneratePrivateKey() addr3 := types.StandardUnlockHash(pk3.PublicKey()) + _, _, cm, db := newStore(t, false, nil) + genesisState := cm.TipState() + // t.Log("addr1:", addr1) // t.Log("addr2:", addr2) // t.Log("addr3:", addr3) @@ -1626,33 +1501,12 @@ func TestRevertSendTransactions(t *testing.T) { } func TestHostAnnouncement(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - - network, genesisBlock := ctestutil.Network() - - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - pk1 := types.GeneratePrivateKey() pk2 := types.GeneratePrivateKey() pk3 := types.GeneratePrivateKey() + _, _, cm, db := newStore(t, false, nil) + checkHostAnnouncements := func(expectedArbitraryData [][]byte, got []chain.HostAnnouncement) { t.Helper() @@ -1833,20 +1687,6 @@ func TestHostAnnouncement(t *testing.T) { } func TestMultipleReorg(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - // Generate three addresses: addr1, addr2, addr3 pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) @@ -1857,23 +1697,13 @@ func TestMultipleReorg(t *testing.T) { pk3 := types.GeneratePrivateKey() addr3 := types.StandardUnlockHash(pk3.PublicKey()) - // t.Log("addr1:", addr1) - // t.Log("addr2:", addr2) - // t.Log("addr3:", addr3) - - network, genesisBlock := ctestutil.Network() - genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 - genesisBlock.Transactions[0].SiafundOutputs[0].Address = addr1 + _, genesisBlock, cm, db := newStore(t, false, func(network *consensus.Network, genesisBlock types.Block) { + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + genesisBlock.Transactions[0].SiafundOutputs[0].Address = addr1 + }) giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value giftSF := genesisBlock.Transactions[0].SiafundOutputs[0].Value - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - uc1 := types.StandardUnlockConditions(pk1.PublicKey()) // transfer gift from addr1 to addr2 // element gets added at height 1 @@ -2169,21 +1999,6 @@ func TestMultipleReorg(t *testing.T) { } func TestMultipleReorgFileContract(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) @@ -2193,17 +2008,12 @@ func TestMultipleReorgFileContract(t *testing.T) { hostPrivateKey := types.GeneratePrivateKey() hostPublicKey := hostPrivateKey.PublicKey() - network, genesisBlock := ctestutil.Network() - genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + _, genesisBlock, cm, db := newStore(t, false, func(network *consensus.Network, genesisBlock types.Block) { + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + }) + genesisState := cm.TipState() giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } - - cm := chain.NewManager(store, genesisState) - scOutputID := genesisBlock.Transactions[0].SiacoinOutputID(0) unlockConditions := types.StandardUnlockConditions(pk1.PublicKey()) @@ -2505,35 +2315,19 @@ func TestMultipleReorgFileContract(t *testing.T) { } func TestMetricCirculatingSupply(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) - if err != nil { - t.Fatal(err) - } - defer bdb.Close() - pk1 := types.GeneratePrivateKey() addr1 := types.StandardUnlockHash(pk1.PublicKey()) - network, genesisBlock := ctestutil.Network() - genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + _, genesisBlock, cm, db := newStore(t, false, func(network *consensus.Network, genesisBlock types.Block) { + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + }) + genesisState := cm.TipState() - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) + var circulatingSupply types.Currency + if subsidy, ok := genesisState.FoundationSubsidy(); ok { + circulatingSupply = circulatingSupply.Add(subsidy.Value) } - cm := chain.NewManager(store, genesisState) - - circulatingSupply := genesisState.FoundationSubsidy().Value for _, txn := range genesisBlock.Transactions { for _, sco := range txn.SiacoinOutputs { circulatingSupply = circulatingSupply.Add(sco.Value) diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index a34074ca..446d9498 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -21,6 +21,7 @@ CREATE TABLE network_metrics ( height INTEGER NOT NULL, difficulty BLOB NOT NULL, siafund_pool BLOB NOT NULL, + num_leaves BLOB NOT NULL, total_hosts INTEGER NOT NULL, active_contracts INTEGER NOT NULL, failed_contracts INTEGER NOT NULL, @@ -255,28 +256,23 @@ CREATE INDEX transaction_file_contract_revisions_transaction_id_index ON transac CREATE TABLE v2_transactions ( id INTEGER PRIMARY KEY, - transaction_id BLOB UNIQUE NOT NULL + transaction_id BLOB UNIQUE NOT NULL, + + new_foundation_address BLOB, + miner_fee BLOB NOT NULL, + arbitrary_data BLOB ); CREATE INDEX v2_transactions_transaction_id_index ON v2_transactions(transaction_id); CREATE TABLE v2_block_transactions ( block_id BLOB REFERENCES blocks(id) ON DELETE CASCADE NOT NULL, - transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, block_order INTEGER NOT NULL, + transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, UNIQUE(block_id, block_order) ); CREATE INDEX v2_block_transactions_block_id_index ON v2_block_transactions(block_id); -CREATE INDEX v2_block_transactions_transaction_id_index ON v2_block_transactions(transaction_id); CREATE INDEX v2_block_transactions_transaction_id_block_id ON v2_block_transactions(transaction_id, block_id); -CREATE TABLE v2_transaction_arbitrary_data ( - transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, - data BLOB NOT NULL, - UNIQUE(transaction_id) -); - -CREATE INDEX v2_transaction_arbitrary_data_transaction_id_index ON v2_transaction_arbitrary_data(transaction_id); - CREATE TABLE state_tree ( row INTEGER NOT NULL, column INTEGER NOT NULL, diff --git a/persist/sqlite/merkle.go b/persist/sqlite/merkle.go index 4e8f19ef..bcbd47a8 100644 --- a/persist/sqlite/merkle.go +++ b/persist/sqlite/merkle.go @@ -10,12 +10,12 @@ import ( func (s *Store) MerkleProof(leafIndex uint64) (proof []types.Hash256, err error) { err = s.transaction(func(tx *txn) error { var numLeaves uint64 - if err := tx.QueryRow("SELECT COUNT(*) FROM state_tree WHERE i = 0").Scan(&numLeaves); err != nil { + if err := tx.QueryRow("SELECT num_leaves FROM network_metrics ORDER BY height DESC LIMIT 1").Scan(decode(&numLeaves)); err != nil { return err } pos := leafIndex - stmt, err := tx.Prepare("SELECT hash FROM state_tree WHERE row = ? AND column = ?") + stmt, err := tx.Prepare("SELECT value FROM state_tree WHERE row = ? AND column = ?") if err != nil { return err } diff --git a/persist/sqlite/metrics.go b/persist/sqlite/metrics.go index 8a0103af..6258e54f 100644 --- a/persist/sqlite/metrics.go +++ b/persist/sqlite/metrics.go @@ -11,7 +11,7 @@ import ( // Metrics implements explorer.Store func (s *Store) Metrics(id types.BlockID) (result explorer.Metrics, err error) { err = s.transaction(func(tx *txn) error { - err = tx.QueryRow(`SELECT block_id, height, difficulty, siafund_pool, total_hosts, active_contracts, failed_contracts, successful_contracts, storage_utilization, circulating_supply, contract_revenue FROM network_metrics WHERE block_id = ?`, encode(id)).Scan(decode(&result.Index.ID), &result.Index.Height, decode(&result.Difficulty), decode(&result.SiafundPool), &result.TotalHosts, &result.ActiveContracts, &result.FailedContracts, &result.SuccessfulContracts, &result.StorageUtilization, decode(&result.CirculatingSupply), decode(&result.ContractRevenue)) + err = tx.QueryRow(`SELECT block_id, height, difficulty, siafund_pool, num_leaves, total_hosts, active_contracts, failed_contracts, successful_contracts, storage_utilization, circulating_supply, contract_revenue FROM network_metrics WHERE block_id = ?`, encode(id)).Scan(decode(&result.Index.ID), &result.Index.Height, decode(&result.Difficulty), decode(&result.SiafundPool), decode(&result.NumLeaves), &result.TotalHosts, &result.ActiveContracts, &result.FailedContracts, &result.SuccessfulContracts, &result.StorageUtilization, decode(&result.CirculatingSupply), decode(&result.ContractRevenue)) if err != nil { return fmt.Errorf("failed to get metrics: %w", err) } diff --git a/persist/sqlite/v2consensus.go b/persist/sqlite/v2consensus.go index 43874134..32561683 100644 --- a/persist/sqlite/v2consensus.go +++ b/persist/sqlite/v2consensus.go @@ -8,20 +8,6 @@ import ( "go.sia.tech/explored/explorer" ) -func addV2ArbitraryData(tx *txn, id int64, txn types.V2Transaction) error { - stmt, err := tx.Prepare(`INSERT INTO v2_transaction_arbitrary_data(transaction_id, data) VALUES (?, ?)`) - - if err != nil { - return fmt.Errorf("addV2ArbitraryData: failed to prepare statement: %w", err) - } - defer stmt.Close() - - if _, err := stmt.Exec(id, txn.ArbitraryData); err != nil { - return fmt.Errorf("addV2ArbitraryData: failed to execute statement: %w", err) - } - return nil -} - func addV2Transactions(tx *txn, bid types.BlockID, txns []types.V2Transaction) (map[types.TransactionID]txnDBId, error) { checkTransactionStmt, err := tx.Prepare(`SELECT id FROM v2_transactions WHERE transaction_id = ?`) if err != nil { @@ -29,7 +15,7 @@ func addV2Transactions(tx *txn, bid types.BlockID, txns []types.V2Transaction) ( } defer checkTransactionStmt.Close() - insertTransactionStmt, err := tx.Prepare(`INSERT INTO v2_transactions (transaction_id) VALUES (?)`) + insertTransactionStmt, err := tx.Prepare(`INSERT INTO v2_transactions (transaction_id, new_foundation_address, miner_fee, arbitrary_data) VALUES (?, ?, ?, ?)`) if err != nil { return nil, fmt.Errorf("failed to prepare insert v2_transaction statement: %v", err) } @@ -52,7 +38,12 @@ func addV2Transactions(tx *txn, bid types.BlockID, txns []types.V2Transaction) ( } if !exist { - result, err := insertTransactionStmt.Exec(encode(txn.ID())) + var newFoundationAddress any + if txn.NewFoundationAddress != nil { + newFoundationAddress = encode(txn.NewFoundationAddress) + } + + result, err := insertTransactionStmt.Exec(encode(txn.ID()), newFoundationAddress, encode(txn.MinerFee), txn.ArbitraryData) if err != nil { return nil, fmt.Errorf("failed to insert into v2_transactions: %w", err) } @@ -81,10 +72,6 @@ func addV2TransactionFields(tx *txn, txns []types.V2Transaction, scDBIds map[typ if dbID.exist { continue } - - if err := addV2ArbitraryData(tx, dbID.id, txn); err != nil { - return fmt.Errorf("failed to add arbitrary data: %w", err) - } } return nil diff --git a/persist/sqlite/v2consensus_test.go b/persist/sqlite/v2consensus_test.go index a7688f94..8e848b71 100644 --- a/persist/sqlite/v2consensus_test.go +++ b/persist/sqlite/v2consensus_test.go @@ -1,43 +1,36 @@ package sqlite_test import ( - "path/filepath" "testing" + "go.sia.tech/core/consensus" "go.sia.tech/core/types" - "go.sia.tech/coreutils" - "go.sia.tech/coreutils/chain" - ctestutil "go.sia.tech/coreutils/testutil" + "go.sia.tech/explored/explorer" "go.sia.tech/explored/internal/testutil" - "go.sia.tech/explored/persist/sqlite" - "go.uber.org/zap/zaptest" ) -func TestV2ArbitraryData(t *testing.T) { - log := zaptest.NewLogger(t) - dir := t.TempDir() - db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) +func getSCE(t *testing.T, db explorer.Store, scid types.SiacoinOutputID) types.SiacoinElement { + sces, err := db.SiacoinElements([]types.SiacoinOutputID{scid}) if err != nil { t.Fatal(err) + } else if len(sces) == 0 { + t.Fatal("can't find sce") } - defer db.Close() + sce := sces[0] - bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) + sce.SiacoinElement.MerkleProof, err = db.MerkleProof(sce.StateElement.LeafIndex) if err != nil { t.Fatal(err) } - defer bdb.Close() - - network, genesisBlock := ctestutil.V2Network() - network.HardforkV2.AllowHeight = 1 - network.HardforkV2.RequireHeight = 2 - store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) - if err != nil { - t.Fatal(err) - } + return sce.SiacoinElement +} - cm := chain.NewManager(store, genesisState) +func TestV2ArbitraryData(t *testing.T) { + _, _, cm, db := newStore(t, true, func(network *consensus.Network, genesisBlock types.Block) { + network.HardforkV2.AllowHeight = 1 + network.HardforkV2.RequireHeight = 2 + }) txn1 := types.V2Transaction{ ArbitraryData: []byte("hello"), @@ -106,3 +99,79 @@ func TestV2ArbitraryData(t *testing.T) { testutil.CheckV2ChainIndices(t, db, txn2.ID(), []types.ChainIndex{cm.Tip(), prev}) testutil.CheckV2ChainIndices(t, db, txn3.ID(), []types.ChainIndex{cm.Tip()}) } + +func TestV2MinerFee(t *testing.T) { + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + addr1Policy := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + + _, genesisBlock, cm, db := newStore(t, true, func(network *consensus.Network, genesisBlock types.Block) { + network.HardforkV2.AllowHeight = 1 + network.HardforkV2.RequireHeight = 2 + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + }) + giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value + + txn1 := types.V2Transaction{ + ArbitraryData: []byte("hello"), + MinerFee: giftSC, + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, genesisBlock.Transactions[0].SiacoinOutputID(0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + } + testutil.SignV2Transaction(cm.TipState(), pk1, &txn1) + + if err := cm.AddBlocks([]types.Block{testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn1}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn1.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn1, dbTxns[0]) + } +} + +func TestV2FoundationAddress(t *testing.T) { + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + addr1Policy := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + + pk2 := types.GeneratePrivateKey() + addr2 := types.StandardUnlockHash(pk2.PublicKey()) + + _, genesisBlock, cm, db := newStore(t, true, func(network *consensus.Network, genesisBlock types.Block) { + network.HardforkV2.AllowHeight = 1 + network.HardforkV2.RequireHeight = 2 + network.HardforkFoundation.PrimaryAddress = addr1 + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + }) + giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value + + txn1 := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, genesisBlock.Transactions[0].SiacoinOutputID(0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + MinerFee: giftSC, + NewFoundationAddress: &addr2, + } + testutil.SignV2Transaction(cm.TipState(), pk1, &txn1) + + if err := cm.AddBlocks([]types.Block{testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn1}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn1.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn1, dbTxns[0]) + } +} diff --git a/persist/sqlite/v2transactions.go b/persist/sqlite/v2transactions.go index 666dee26..0c401571 100644 --- a/persist/sqlite/v2transactions.go +++ b/persist/sqlite/v2transactions.go @@ -35,10 +35,10 @@ LIMIT ? OFFSET ?`, encode(txnID), limit, offset) return } -// blockV2TransactionIDs returns the database ID for each v2 transaction in the -// block. -func blockV2TransactionIDs(tx *txn, blockID types.BlockID) (idMap map[int64]transactionID, err error) { - rows, err := tx.Query(`SELECT bt.transaction_id, block_order, t.transaction_id +// blockV2TransactionIDs returns the transaction id as a types.TransactionID +// for each v2 transaction in the block. +func blockV2TransactionIDs(tx *txn, blockID types.BlockID) (ids []types.TransactionID, err error) { + rows, err := tx.Query(`SELECT t.transaction_id FROM v2_block_transactions bt INNER JOIN v2_transactions t ON (t.id = bt.transaction_id) WHERE block_id = ? ORDER BY block_order ASC`, encode(blockID)) @@ -47,111 +47,59 @@ WHERE block_id = ? ORDER BY block_order ASC`, encode(blockID)) } defer rows.Close() - idMap = make(map[int64]transactionID) for rows.Next() { - var dbID int64 - var blockOrder int64 - var txnID types.TransactionID - if err := rows.Scan(&dbID, &blockOrder, decode(&txnID)); err != nil { + var id types.TransactionID + if err := rows.Scan(decode(&id)); err != nil { return nil, fmt.Errorf("failed to scan block transaction: %w", err) } - idMap[blockOrder] = transactionID{id: txnID, dbID: dbID} + ids = append(ids, id) } return } -// v2TransactionArbitraryData returns the arbitrary data for each v2 transaction. -func v2TransactionArbitraryData(tx *txn, txnIDs []int64) (map[int64][]byte, error) { - query := `SELECT transaction_id, data -FROM v2_transaction_arbitrary_data -WHERE transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `)` - rows, err := tx.Query(query, queryArgs(txnIDs)...) +// getV2Transactions fetches v2 transactions in the correct order using +// prepared statements. +func getV2Transactions(tx *txn, ids []types.TransactionID) ([]explorer.V2Transaction, error) { + _, txns, err := getV2TransactionBase(tx, ids) if err != nil { - return nil, err + return nil, fmt.Errorf("getV2Transactions: failed to get base transactions: %w", err) } - defer rows.Close() - result := make(map[int64][]byte) - for rows.Next() { - var txnID int64 - var data []byte - if err := rows.Scan(&txnID, &data); err != nil { - return nil, fmt.Errorf("failed to scan arbitrary data: %w", err) - } - result[txnID] = data - } - return result, nil + return txns, nil } -func getV2Transactions(tx *txn, idMap map[int64]transactionID) ([]explorer.V2Transaction, error) { - dbIDs := make([]int64, len(idMap)) - for order, id := range idMap { - dbIDs[order] = id.dbID - } - - txnArbitraryData, err := v2TransactionArbitraryData(tx, dbIDs) +// getV2TransactionBase fetches the base transaction data for a given list of +// transaction IDs. +func getV2TransactionBase(tx *txn, txnIDs []types.TransactionID) ([]int64, []explorer.V2Transaction, error) { + stmt, err := tx.Prepare(`SELECT id, transaction_id, new_foundation_address, miner_fee, arbitrary_data FROM v2_transactions WHERE transaction_id = ?`) if err != nil { - return nil, fmt.Errorf("getV2Transactions: failed to get arbitrary data: %w", err) + return nil, nil, fmt.Errorf("getV2TransactionBase: failed to prepare statement: %w", err) } - - var results []explorer.V2Transaction - for order, dbID := range dbIDs { - txn := explorer.V2Transaction{ - ID: idMap[int64(order)].id, - ArbitraryData: txnArbitraryData[dbID], + defer stmt.Close() + + var dbID int64 + dbIDs := make([]int64, 0, len(txnIDs)) + txns := make([]explorer.V2Transaction, 0, len(txnIDs)) + for _, id := range txnIDs { + var txn explorer.V2Transaction + var newFoundationAddress types.Address + if err := stmt.QueryRow(encode(id)).Scan(&dbID, decode(&txn.ID), decodeNull(&newFoundationAddress), decode(&txn.MinerFee), &txn.ArbitraryData); err != nil { + return nil, nil, fmt.Errorf("failed to scan base transaction: %w", err) } - - // for _, attestation := range txn.Attestations { - // var ha chain.HostAnnouncement - // if ha.FromAttestation(attestation) { - // txn.HostAnnouncements = append(txn.HostAnnouncements, ha) - // } - // } - - results = append(results, txn) - } - return results, nil -} - -// v2TransactionDatabaseIDs returns the database ID for each transaction. -func v2TransactionDatabaseIDs(tx *txn, txnIDs []types.TransactionID) (dbIDs map[int64]transactionID, err error) { - encodedIDs := func(ids []types.TransactionID) []any { - result := make([]any, len(ids)) - for i, id := range ids { - result[i] = encode(id) + if (newFoundationAddress != types.Address{}) { + txn.NewFoundationAddress = &newFoundationAddress } - return result - } - - query := `SELECT id, transaction_id FROM v2_transactions WHERE transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) ORDER BY id` - rows, err := tx.Query(query, encodedIDs(txnIDs)...) - if err != nil { - return nil, err - } - defer rows.Close() - var i int64 - dbIDs = make(map[int64]transactionID) - for rows.Next() { - var dbID int64 - var txnID types.TransactionID - if err := rows.Scan(&dbID, decode(&txnID)); err != nil { - return nil, fmt.Errorf("failed to scan transaction: %w", err) - } - dbIDs[i] = transactionID{id: txnID, dbID: dbID} - i++ + dbIDs = append(dbIDs, dbID) + txns = append(txns, txn) } - return + return dbIDs, txns, nil } // V2Transactions implements explorer.Store. func (s *Store) V2Transactions(ids []types.TransactionID) (results []explorer.V2Transaction, err error) { err = s.transaction(func(tx *txn) error { - dbIDs, err := v2TransactionDatabaseIDs(tx, ids) - if err != nil { - return fmt.Errorf("failed to get transaction IDs: %w", err) - } - results, err = getV2Transactions(tx, dbIDs) + results, err = getV2Transactions(tx, ids) if err != nil { return fmt.Errorf("failed to get transactions: %w", err) }