From e16f1ed7ba74bff23239d0dd3036fc9d2f9d134b Mon Sep 17 00:00:00 2001 From: Christopher Tarry Date: Wed, 16 Oct 2024 16:46:01 -0400 Subject: [PATCH] add miner fee test --- go.mod | 8 +- go.sum | 8 + internal/testutil/chain.go | 53 +++++- internal/testutil/check.go | 3 + persist/sqlite/consensus_test.go | 6 +- persist/sqlite/v2consensus_test.go | 250 ++++++++++++++++++++++++++++- 6 files changed, 320 insertions(+), 8 deletions(-) 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..9cd25128 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) } + +// SignTransactionWithContracts 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/consensus_test.go b/persist/sqlite/consensus_test.go index 485fd494..6c12eff8 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -2533,7 +2533,11 @@ func TestMetricCirculatingSupply(t *testing.T) { cm := chain.NewManager(store, genesisState) - circulatingSupply := genesisState.FoundationSubsidy().Value + var circulatingSupply types.Currency + if subsidy, ok := genesisState.FoundationSubsidy(); ok { + circulatingSupply = circulatingSupply.Add(subsidy.Value) + } + for _, txn := range genesisBlock.Transactions { for _, sco := range txn.SiacoinOutputs { circulatingSupply = circulatingSupply.Add(sco.Value) diff --git a/persist/sqlite/v2consensus_test.go b/persist/sqlite/v2consensus_test.go index a7688f94..365e32b3 100644 --- a/persist/sqlite/v2consensus_test.go +++ b/persist/sqlite/v2consensus_test.go @@ -1,18 +1,208 @@ package sqlite_test import ( + "errors" "path/filepath" "testing" + "time" + "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" ) +type consensusDB struct { + sces map[types.SiacoinOutputID]types.SiacoinElement + sfes map[types.SiafundOutputID]types.SiafundElement + fces map[types.FileContractID]types.FileContractElement + v2fces map[types.FileContractID]types.V2FileContractElement +} + +func (db *consensusDB) applyBlock(au consensus.ApplyUpdate) { + for id, sce := range db.sces { + au.UpdateElementProof(&sce.StateElement) + db.sces[id] = sce + } + for id, sfe := range db.sfes { + au.UpdateElementProof(&sfe.StateElement) + db.sfes[id] = sfe + } + for id, fce := range db.fces { + au.UpdateElementProof(&fce.StateElement) + db.fces[id] = fce + } + for id, fce := range db.v2fces { + au.UpdateElementProof(&fce.StateElement) + db.v2fces[id] = fce + } + au.ForEachSiacoinElement(func(sce types.SiacoinElement, created, spent bool) { + if spent { + delete(db.sces, types.SiacoinOutputID(sce.ID)) + } else { + db.sces[types.SiacoinOutputID(sce.ID)] = sce + } + }) + au.ForEachSiafundElement(func(sfe types.SiafundElement, created, spent bool) { + if spent { + delete(db.sfes, types.SiafundOutputID(sfe.ID)) + } else { + db.sfes[types.SiafundOutputID(sfe.ID)] = sfe + } + }) + au.ForEachFileContractElement(func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool) { + if created { + db.fces[types.FileContractID(fce.ID)] = fce + } else if rev != nil { + db.fces[types.FileContractID(fce.ID)] = *rev + } else if resolved { + delete(db.fces, types.FileContractID(fce.ID)) + } + }) + au.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + if created { + db.v2fces[types.FileContractID(fce.ID)] = fce + } else if rev != nil { + db.v2fces[types.FileContractID(fce.ID)] = *rev + } else if res != nil { + delete(db.v2fces, types.FileContractID(fce.ID)) + } + }) +} + +func (db *consensusDB) revertBlock(ru consensus.RevertUpdate) { + ru.ForEachSiacoinElement(func(sce types.SiacoinElement, created, spent bool) { + if spent { + db.sces[types.SiacoinOutputID(sce.ID)] = sce + } else { + delete(db.sces, types.SiacoinOutputID(sce.ID)) + } + }) + ru.ForEachSiafundElement(func(sfe types.SiafundElement, created, spent bool) { + if spent { + db.sfes[types.SiafundOutputID(sfe.ID)] = sfe + } else { + delete(db.sfes, types.SiafundOutputID(sfe.ID)) + } + }) + ru.ForEachFileContractElement(func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool) { + if created { + delete(db.fces, types.FileContractID(fce.ID)) + } else if rev != nil { + db.fces[types.FileContractID(fce.ID)] = fce + } else if resolved { + db.fces[types.FileContractID(fce.ID)] = fce + } + }) + ru.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + if created { + delete(db.v2fces, types.FileContractID(fce.ID)) + } else if rev != nil { + db.v2fces[types.FileContractID(fce.ID)] = fce + } else if res != nil { + db.v2fces[types.FileContractID(fce.ID)] = fce + } + }) + for id, sce := range db.sces { + ru.UpdateElementProof(&sce.StateElement) + db.sces[id] = sce + } + for id, sfe := range db.sfes { + ru.UpdateElementProof(&sfe.StateElement) + db.sfes[id] = sfe + } + for id, fce := range db.fces { + ru.UpdateElementProof(&fce.StateElement) + db.fces[id] = fce + } + for id, fce := range db.v2fces { + ru.UpdateElementProof(&fce.StateElement) + db.v2fces[id] = fce + } +} + +func (db *consensusDB) supplementTipBlock(b types.Block) (bs consensus.V1BlockSupplement) { + bs = consensus.V1BlockSupplement{ + Transactions: make([]consensus.V1TransactionSupplement, len(b.Transactions)), + } + for i, txn := range b.Transactions { + ts := &bs.Transactions[i] + for _, sci := range txn.SiacoinInputs { + if sce, ok := db.sces[sci.ParentID]; ok { + ts.SiacoinInputs = append(ts.SiacoinInputs, sce) + } + } + for _, sfi := range txn.SiafundInputs { + if sfe, ok := db.sfes[sfi.ParentID]; ok { + ts.SiafundInputs = append(ts.SiafundInputs, sfe) + } + } + for _, fcr := range txn.FileContractRevisions { + if fce, ok := db.fces[fcr.ParentID]; ok { + ts.RevisedFileContracts = append(ts.RevisedFileContracts, fce) + } + } + } + return bs +} + +func (db *consensusDB) ancestorTimestamp(types.BlockID) time.Time { + return time.Time{} +} + +// v2SyncDB is the same as syncDB but it updates the consensusDB `edb` to keep +// track of elements and update their proofs to make teseting easier. +func v2SyncDB(t *testing.T, edb *consensusDB, db *sqlite.Store, cm *chain.Manager) { + index, err := db.Tip() + if err != nil && !errors.Is(err, explorer.ErrNoTip) { + t.Fatal(err) + } + + for index != cm.Tip() { + crus, caus, err := cm.UpdatesSince(index, 1000) + if err != nil { + t.Fatal(err) + } + + if err := db.UpdateChainState(crus, caus); err != nil { + t.Fatal("failed to process updates:", err) + } + + if edb != nil { + for _, cru := range crus { + edb.revertBlock(cru.RevertUpdate) + } + for _, cau := range caus { + edb.applyBlock(cau.ApplyUpdate) + } + } + + if len(crus) > 0 { + index = crus[len(crus)-1].State.Index + } + if len(caus) > 0 { + index = caus[len(caus)-1].State.Index + } + } +} + +func newConsensusDB(n *consensus.Network, genesisBlock types.Block) (*consensusDB, consensus.State) { + db := &consensusDB{ + 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), + } + cs, au := consensus.ApplyBlock(n.GenesisState(), genesisBlock, db.supplementTipBlock(genesisBlock), time.Time{}) + db.applyBlock(au) + return db, cs +} + func TestV2ArbitraryData(t *testing.T) { log := zaptest.NewLogger(t) dir := t.TempDir() @@ -50,7 +240,7 @@ func TestV2ArbitraryData(t *testing.T) { if err := cm.AddBlocks([]types.Block{testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn1, txn2}, types.VoidAddress)}); err != nil { t.Fatal(err) } - syncDB(t, db, cm) + v2SyncDB(t, nil, db, cm) prev := cm.Tip() { @@ -106,3 +296,61 @@ 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) { + 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()) + addr1Policy := types.SpendPolicy{types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + + network, genesisBlock := ctestutil.V2Network() + network.HardforkV2.AllowHeight = 1 + network.HardforkV2.RequireHeight = 2 + + genesisBlock.Transactions[0].SiacoinOutputs[0].Address = addr1 + giftSC := genesisBlock.Transactions[0].SiacoinOutputs[0].Value + + edb, _ := newConsensusDB(network, genesisBlock) + store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) + if err != nil { + t.Fatal(err) + } + + cm := chain.NewManager(store, genesisState) + + txn1 := types.V2Transaction{ + ArbitraryData: []byte("hello"), + MinerFee: giftSC, + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: edb.sces[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) + } + v2SyncDB(t, edb, db, cm) + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn1.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn1, dbTxns[0]) + } +}