diff --git a/api/client.go b/api/client.go index ba83399b..6a5984a5 100644 --- a/api/client.go +++ b/api/client.go @@ -191,7 +191,7 @@ func (c *Client) Contract(id types.FileContractID) (resp explorer.FileContract, return } -// Contracts returns the transactions with the specified IDs. +// Contracts returns the contracts with the specified IDs. func (c *Client) Contracts(ids []types.FileContractID) (resp []explorer.FileContract, err error) { err = c.c.POST("/contracts", ids, &resp) return @@ -210,6 +210,31 @@ func (c *Client) ContractRevisions(id types.FileContractID) (resp []explorer.Fil return } +// V2Contract returns the v2 file contract with the specified ID. +func (c *Client) V2Contract(id types.FileContractID) (resp explorer.V2FileContract, err error) { + err = c.c.GET(fmt.Sprintf("/v2/contracts/%s", id), &resp) + return +} + +// V2Contracts returns the v2 contracts with the specified IDs. +func (c *Client) V2Contracts(ids []types.FileContractID) (resp []explorer.V2FileContract, err error) { + err = c.c.POST("/v2/contracts", ids, &resp) + return +} + +// V2ContractsKey returns the v2 contracts for a particular ed25519 key. +func (c *Client) V2ContractsKey(key types.PublicKey) (resp []explorer.V2FileContract, err error) { + err = c.c.GET(fmt.Sprintf("/pubkey/%s/v2/contracts", key), &resp) + return +} + +// V2ContractRevisions returns all the revisions of the contract with the +// specified ID. +func (c *Client) V2ContractRevisions(id types.FileContractID) (resp []explorer.V2FileContract, err error) { + err = c.c.GET(fmt.Sprintf("/v2/contracts/%s/revisions", id), &resp) + return +} + // Host returns information about the host with a given ed25519 key. func (c *Client) Host(key types.PublicKey) (resp explorer.Host, err error) { err = c.c.GET(fmt.Sprintf("/pubkey/%s/host", key), &resp) diff --git a/api/server.go b/api/server.go index d2a47042..b83d9f3a 100644 --- a/api/server.go +++ b/api/server.go @@ -65,6 +65,9 @@ type ( Contracts(ids []types.FileContractID) (result []explorer.FileContract, err error) ContractsKey(key types.PublicKey) (result []explorer.FileContract, err error) ContractRevisions(id types.FileContractID) (result []explorer.FileContract, err error) + V2Contracts(ids []types.FileContractID) (result []explorer.V2FileContract, err error) + V2ContractsKey(key types.PublicKey) (result []explorer.V2FileContract, err error) + V2ContractRevisions(id types.FileContractID) (result []explorer.V2FileContract, err error) Search(id types.Hash256) (explorer.SearchType, error) Hosts(pks []types.PublicKey) ([]explorer.Host, error) @@ -485,6 +488,7 @@ func (s *server) outputsSiafundHandler(jc jape.Context) { jc.Encode(outputs[0]) } + func (s *server) contractsIDHandler(jc jape.Context) { var id types.FileContractID if jc.DecodeParam("id", &id) != nil { @@ -532,6 +536,68 @@ func (s *server) contractsBatchHandler(jc jape.Context) { jc.Encode(fcs) } +func (s *server) v2ContractsIDHandler(jc jape.Context) { + var id types.FileContractID + if jc.DecodeParam("id", &id) != nil { + return + } + fcs, err := s.e.V2Contracts([]types.FileContractID{id}) + if jc.Check("failed to get contract", err) != nil { + return + } else if len(fcs) == 0 { + jc.Error(explorer.ErrContractNotFound, http.StatusNotFound) + return + } + jc.Encode(fcs[0]) +} + +func (s *server) v2ContractsBatchHandler(jc jape.Context) { + var ids []types.FileContractID + if jc.Decode(&ids) != nil { + return + } else if len(ids) > maxIDs { + jc.Error(ErrTooManyIDs, http.StatusBadRequest) + return + } + + fcs, err := s.e.V2Contracts(ids) + if jc.Check("failed to get contracts", err) != nil { + return + } + jc.Encode(fcs) +} + +func (s *server) v2ContractsIDRevisionsHandler(jc jape.Context) { + var id types.FileContractID + if jc.DecodeParam("id", &id) != nil { + return + } + + fcs, err := s.e.V2ContractRevisions(id) + if errors.Is(err, explorer.ErrContractNotFound) { + jc.Error(fmt.Errorf("%w: %v", err, id), http.StatusNotFound) + return + } else if jc.Check("failed to fetch contract revisions", err) != nil { + return + } + jc.Encode(fcs) +} + +func (s *server) pubkeyV2ContractsHandler(jc jape.Context) { + var key types.PublicKey + if jc.DecodeParam("key", &key) != nil { + return + } + fcs, err := s.e.V2ContractsKey(key) + if jc.Check("failed to get contracts", err) != nil { + return + } else if len(fcs) == 0 { + jc.Error(explorer.ErrContractNotFound, http.StatusNotFound) + return + } + jc.Encode(fcs) +} + func (s *server) pubkeyContractsHandler(jc jape.Context) { var key types.PublicKey if jc.DecodeParam("key", &key) != nil { @@ -630,8 +696,13 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler { "GET /contracts/:id/revisions": srv.contractsIDRevisionsHandler, "POST /contracts": srv.contractsBatchHandler, - "GET /pubkey/:key/contracts": srv.pubkeyContractsHandler, - "GET /pubkey/:key/host": srv.pubkeyHostHandler, + "GET /v2/contracts/:id": srv.v2ContractsIDHandler, + "GET /v2/contracts/:id/revisions": srv.v2ContractsIDRevisionsHandler, + "POST /v2/contracts": srv.v2ContractsBatchHandler, + + "GET /pubkey/:key/v2/contracts": srv.pubkeyV2ContractsHandler, + "GET /pubkey/:key/contracts": srv.pubkeyContractsHandler, + "GET /pubkey/:key/host": srv.pubkeyHostHandler, "GET /metrics/block": srv.blocksMetricsHandler, "GET /metrics/block/:id": srv.blocksMetricsIDHandler, diff --git a/explorer/explorer.go b/explorer/explorer.go index 292eaca5..390ff10e 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -61,6 +61,9 @@ type Store interface { Contracts(ids []types.FileContractID) (result []FileContract, err error) ContractsKey(key types.PublicKey) (result []FileContract, err error) ContractRevisions(id types.FileContractID) (result []FileContract, err error) + V2Contracts(ids []types.FileContractID) (result []V2FileContract, err error) + V2ContractsKey(key types.PublicKey) (result []V2FileContract, err error) + V2ContractRevisions(id types.FileContractID) (result []V2FileContract, err error) SiacoinElements(ids []types.SiacoinOutputID) (result []SiacoinOutput, err error) SiafundElements(ids []types.SiafundOutputID) (result []SiafundOutput, err error) @@ -263,6 +266,22 @@ func (e *Explorer) ContractRevisions(id types.FileContractID) (result []FileCont return e.s.ContractRevisions(id) } +// V2Contracts returns the v2 contracts with the specified IDs. +func (e *Explorer) V2Contracts(ids []types.FileContractID) (result []V2FileContract, err error) { + return e.s.V2Contracts(ids) +} + +// V2ContractsKey returns the v2 contracts for a particular ed25519 key. +func (e *Explorer) V2ContractsKey(key types.PublicKey) (result []V2FileContract, err error) { + return e.s.V2ContractsKey(key) +} + +// V2ContractRevisions returns all the revisions of the v2 contract with the +// specified ID. +func (e *Explorer) V2ContractRevisions(id types.FileContractID) (result []V2FileContract, err error) { + return e.s.V2ContractRevisions(id) +} + // SiacoinElements returns the siacoin elements with the specified IDs. func (e *Explorer) SiacoinElements(ids []types.SiacoinOutputID) (result []SiacoinOutput, err error) { return e.s.SiacoinElements(ids) diff --git a/explorer/types.go b/explorer/types.go index 1b142746..36942649 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -167,6 +167,20 @@ type Transaction struct { HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements,omitempty"` } +// A V2FileContract is a v2 file contract. +type V2FileContract struct { + TransactionID types.TransactionID `json:"transactionID"` + + ConfirmationIndex *types.ChainIndex `json:"confirmationIndex"` + ConfirmationTransactionID *types.TransactionID `json:"confirmationTransactionID"` + + Resolution *types.V2FileContractResolutionType + ResolutionIndex *types.ChainIndex `json:"resolutionIndex"` + ResolutionTransactionID *types.TransactionID `json:"resolutionTransactionID"` + + types.V2FileContractElement +} + // A V2Transaction is a v2 transaction that uses the wrapped types above. type V2Transaction struct { ID types.TransactionID `json:"id"` @@ -175,8 +189,11 @@ type V2Transaction struct { SiacoinOutputs []SiacoinOutput `json:"siacoinOutputs,omitempty"` SiafundInputs []types.V2SiafundInput `json:"siafundInputs,omitempty"` SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` - Attestations []types.Attestation `json:"attestations,omitempty"` - ArbitraryData []byte `json:"arbitraryData,omitempty"` + + FileContracts []V2FileContract `json:"fileContracts,omitempty"` + + Attestations []types.Attestation `json:"attestations,omitempty"` + ArbitraryData []byte `json:"arbitraryData,omitempty"` NewFoundationAddress *types.Address `json:"newFoundationAddress,omitempty"` MinerFee types.Currency `json:"minerFee"` diff --git a/explorer/update.go b/explorer/update.go index 0db1701f..e779d485 100644 --- a/explorer/update.go +++ b/explorer/update.go @@ -18,6 +18,17 @@ type ( ProofTransactionID *types.TransactionID } + // V2FileContractUpdate represents a v2 file contract from a consensus + // update. + V2FileContractUpdate struct { + FileContractElement types.V2FileContractElement + Revision *types.V2FileContractElement + Resolution types.V2FileContractResolutionType + + ConfirmationTransactionID *types.TransactionID + ResolutionTransactionID *types.TransactionID + } + // A DBFileContract represents a file contract element in the DB. DBFileContract struct { ID types.FileContractID @@ -48,7 +59,8 @@ type ( SpentSiafundElements []types.SiafundElement EphemeralSiafundElements []types.SiafundElement - FileContractElements []FileContractUpdate + FileContractElements []FileContractUpdate + V2FileContractElements []V2FileContractUpdate } // An UpdateTx atomically updates the state of a store. @@ -156,6 +168,37 @@ func applyChainUpdate(tx UpdateTx, cau chain.ApplyUpdate) error { fces = append(fces, fce) } + v2FceMap := make(map[types.FileContractID]V2FileContractUpdate) + cau.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + v2FceMap[types.FileContractID(fce.ID)] = V2FileContractUpdate{ + FileContractElement: fce, + Revision: rev, + Resolution: res, + } + }) + for _, txn := range cau.Block.V2Transactions() { + txnID := txn.ID() + for i := range txn.FileContracts { + fcID := txn.V2FileContractID(txn.ID(), i) + + v := v2FceMap[fcID] + v.ConfirmationTransactionID = &txnID + v2FceMap[fcID] = v + } + for _, fcr := range txn.FileContractResolutions { + fcID := types.FileContractID(fcr.Parent.ID) + + v := v2FceMap[fcID] + v.ResolutionTransactionID = &txnID + v2FceMap[fcID] = v + } + } + + var v2Fces []V2FileContractUpdate + for _, fce := range v2FceMap { + v2Fces = append(v2Fces, fce) + } + var treeUpdates []TreeNodeUpdate cau.ForEachTreeNode(func(row, column uint64, hash types.Hash256) { treeUpdates = append(treeUpdates, TreeNodeUpdate{ @@ -181,7 +224,8 @@ func applyChainUpdate(tx UpdateTx, cau chain.ApplyUpdate) error { SpentSiafundElements: spentSiafundElements, EphemeralSiafundElements: ephemeralSiafundElements, - FileContractElements: fces, + FileContractElements: fces, + V2FileContractElements: v2Fces, } var err error @@ -275,6 +319,37 @@ func revertChainUpdate(tx UpdateTx, cru chain.RevertUpdate, revertedIndex types. fces = append(fces, fce) } + v2FceMap := make(map[types.FileContractID]V2FileContractUpdate) + cru.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + v2FceMap[types.FileContractID(fce.ID)] = V2FileContractUpdate{ + FileContractElement: fce, + Revision: rev, + Resolution: res, + } + }) + for _, txn := range cru.Block.V2Transactions() { + txnID := txn.ID() + for i := range txn.FileContracts { + fcID := txn.V2FileContractID(txn.ID(), i) + + v := v2FceMap[fcID] + v.ConfirmationTransactionID = &txnID + v2FceMap[fcID] = v + } + for _, fcr := range txn.FileContractResolutions { + fcID := types.FileContractID(fcr.Parent.ID) + + v := v2FceMap[fcID] + v.ResolutionTransactionID = &txnID + v2FceMap[fcID] = v + } + } + + var v2Fces []V2FileContractUpdate + for _, fce := range v2FceMap { + v2Fces = append(v2Fces, fce) + } + var treeUpdates []TreeNodeUpdate cru.ForEachTreeNode(func(row, column uint64, hash types.Hash256) { treeUpdates = append(treeUpdates, TreeNodeUpdate{ @@ -297,7 +372,8 @@ func revertChainUpdate(tx UpdateTx, cru chain.RevertUpdate, revertedIndex types. SpentSiafundElements: spentSiafundElements, EphemeralSiafundElements: ephemeralSiafundElements, - FileContractElements: fces, + FileContractElements: fces, + V2FileContractElements: v2Fces, } state.Metrics.Index = revertedIndex diff --git a/go.mod b/go.mod index f166c60c..f6aaa199 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.8-0.20241015191424-3a45c8b415e7 - go.sia.tech/coreutils v0.4.2-0.20241007200058-9a2654c61a97 + go.sia.tech/core v0.5.0 + go.sia.tech/coreutils v0.5.0 go.sia.tech/jape v0.12.1 go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 5323c9a1..6ec573e3 100644 --- a/go.sum +++ b/go.sum @@ -19,14 +19,10 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/core v0.5.0 h1:feLC7DSCF+PhU157s/94106hFKyiGrGQ9HC3/dF/l7E= +go.sia.tech/core v0.5.0/go.mod h1:P3C1BWa/7J4XgdzWuaYHBvLo2RzZ0UBaJM4TG1GWB2g= +go.sia.tech/coreutils v0.5.0 h1:/xKxdw83iZy0jjLzI2NGHyG4azyjK5DJscxpkr6nIGQ= +go.sia.tech/coreutils v0.5.0/go.mod h1:VYM4FcmlhVrpDGvglLHjRW+gitoaxPNLvp5mL2quilo= 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= @@ -37,16 +33,12 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.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= diff --git a/internal/testutil/check.go b/internal/testutil/check.go index 3693f49d..a64b1a39 100644 --- a/internal/testutil/check.go +++ b/internal/testutil/check.go @@ -161,6 +161,15 @@ func CheckV2Transaction(t *testing.T, expectTxn types.V2Transaction, gotTxn expl Equal(t, "value", expected.Value, got.Value) } + Equal(t, "file contracts", len(expectTxn.FileContracts), len(gotTxn.FileContracts)) + for i := range expectTxn.FileContracts { + expected := expectTxn.FileContracts[i] + got := gotTxn.FileContracts[i] + + Equal(t, "id", expectTxn.V2FileContractID(expectTxn.ID(), i), types.FileContractID(got.ID)) + CheckV2FC(t, expected, got) + } + Equal(t, "attestations", len(expectTxn.Attestations), len(gotTxn.Attestations)) for i := range expectTxn.Attestations { expected := expectTxn.Attestations[i] @@ -240,3 +249,26 @@ func CheckFC(t *testing.T, revision, resolved, valid bool, expected types.FileCo Equal(t, "missed proof output value", expected.MissedProofOutputs[i].Value, gotFC.MissedProofOutputs[i].Value) } } + +// CheckV2FC checks the retrieved file contract with the source file contract +// in addition to checking the resolved and valid fields. +func CheckV2FC(t *testing.T, expected types.V2FileContract, got explorer.V2FileContract) { + t.Helper() + + gotFC := got.V2FileContractElement.V2FileContract + Equal(t, "capacity", expected.Capacity, gotFC.Capacity) + Equal(t, "filesize", expected.Filesize, gotFC.Filesize) + Equal(t, "proof height", expected.ProofHeight, gotFC.ProofHeight) + Equal(t, "expiration height", expected.ExpirationHeight, gotFC.ExpirationHeight) + Equal(t, "renter output address", expected.RenterOutput.Address, gotFC.RenterOutput.Address) + Equal(t, "renter output value", expected.RenterOutput.Address, gotFC.RenterOutput.Address) + Equal(t, "host output address", expected.HostOutput.Address, gotFC.HostOutput.Address) + Equal(t, "host output value", expected.HostOutput.Address, gotFC.HostOutput.Address) + Equal(t, "missed host value", expected.MissedHostValue, gotFC.MissedHostValue) + Equal(t, "total collateral", expected.TotalCollateral, gotFC.TotalCollateral) + Equal(t, "renter public key", expected.RenterPublicKey, gotFC.RenterPublicKey) + Equal(t, "host public key", expected.HostPublicKey, gotFC.HostPublicKey) + Equal(t, "revision number", expected.RevisionNumber, gotFC.RevisionNumber) + Equal(t, "renter signature", expected.RenterSignature, gotFC.RenterSignature) + Equal(t, "host signature", expected.HostSignature, gotFC.HostSignature) +} diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index 84283e18..fa12a78e 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -1071,9 +1071,14 @@ func (ut *updateTx) ApplyIndex(state explorer.UpdateState) error { return fmt.Errorf("ApplyIndex: failed to add file contracts: %w", err) } + v2FcDBIds, err := updateV2FileContractElements(ut.tx, false, state.Block, state.V2FileContractElements) + if err != nil { + return fmt.Errorf("ApplyIndex: failed to add v2 file contracts: %w", err) + } + if err := addTransactionFields(ut.tx, state.Block.Transactions, scDBIds, sfDBIds, fcDBIds, txnDBIds); err != nil { return fmt.Errorf("ApplyIndex: failed to add transaction fields: %w", err) - } else if err := addV2TransactionFields(ut.tx, state.Block.V2Transactions(), scDBIds, sfDBIds, fcDBIds, v2TxnDBIds); err != nil { + } else if err := addV2TransactionFields(ut.tx, state.Block.V2Transactions(), scDBIds, sfDBIds, v2FcDBIds, v2TxnDBIds); err != nil { return fmt.Errorf("ApplyIndex: failed to add v2 transaction fields: %w", err) } else if err := updateBalances(ut.tx, state.Metrics.Index.Height, state.SpentSiacoinElements, state.NewSiacoinElements, state.SpentSiafundElements, state.NewSiafundElements); err != nil { return fmt.Errorf("ApplyIndex: failed to update balances: %w", err) @@ -1087,6 +1092,8 @@ func (ut *updateTx) ApplyIndex(state explorer.UpdateState) error { 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) } return nil @@ -1119,6 +1126,8 @@ func (ut *updateTx) RevertIndex(state explorer.UpdateState) error { return fmt.Errorf("RevertIndex: failed to update state tree: %w", err) } else if err := updateFileContractIndices(ut.tx, true, state.Metrics.Index, state.FileContractElements); err != nil { return fmt.Errorf("RevertIndex: failed to update file contract element indices: %w", err) + } else if err := updateV2FileContractIndices(ut.tx, true, state.Metrics.Index, state.V2FileContractElements); err != nil { + return fmt.Errorf("ApplyIndex: failed to update v2 file contract element indices: %w", err) } return nil diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 6f829032..46c16ea1 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -308,8 +308,16 @@ CREATE TABLE v2_transaction_siafund_outputs ( ); CREATE INDEX v2_transaction_siafund_outputs_transaction_id_index ON v2_transaction_siafund_outputs(transaction_id); +CREATE TABLE v2_transaction_file_contracts ( + transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, + transaction_order INTEGER NOT NULL, + contract_id INTEGER REFERENCES v2_file_contract_elements(id) ON DELETE CASCADE NOT NULL, -- add an index to all foreign keys + UNIQUE(transaction_id, transaction_order) +); +CREATE INDEX v2_transaction_file_contracts_transaction_id_index ON v2_transaction_file_contracts(transaction_id); + CREATE TABLE v2_transaction_attestations ( - transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL, + transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, transaction_order INTEGER NOT NULL, public_key BLOB NOT NULL, key TEXT NOT NULL, @@ -378,6 +386,49 @@ CREATE TABLE foundation_subsidy_events ( output_id INTEGER REFERENCES siacoin_elements(id) ON DELETE CASCADE NOT NULL ); +CREATE TABLE v2_file_contract_elements ( + id INTEGER PRIMARY KEY, + block_id BLOB REFERENCES blocks(id) ON DELETE CASCADE NOT NULL, + transaction_id BLOB REFERENCES v2_transactions(transaction_id) ON DELETE CASCADE NOT NULL, + + contract_id BLOB NOT NULL, + leaf_index BLOB NOT NULL, + + capacity BLOB NOT NULL, + filesize BLOB NOT NULL, + file_merkle_root BLOB NOT NULL, + proof_height BLOB NOT NULL, + expiration_height BLOB NOT NULL, + renter_output_address BLOB NOT NULL, + renter_output_value BLOB NOT NULL, + host_output_address BLOB NOT NULL, + host_output_value BLOB NOT NULL, + missed_host_value BLOB NOT NULL, + total_collateral BLOB NOT NULL, + renter_public_key BLOB NOT NULL, + host_public_key BLOB NOT NULL, + revision_number BLOB NOT NULL, + + renter_signature BLOB NOT NULL, + host_signature BLOB NOT NULL, + + UNIQUE(contract_id, revision_number) +); +CREATE INDEX v2_file_contract_elements_contract_id_revision_number_index ON v2_file_contract_elements(contract_id, revision_number); + +CREATE TABLE v2_last_contract_revision ( + contract_id BLOB PRIMARY KEY NOT NULL, + + confirmation_index BLOB, + confirmation_transaction_id BLOB REFERENCES v2_transactions(transaction_id), + + resolution BLOB, + resolution_index BLOB, + resolution_transaction_id BLOB REFERENCES v2_transactions(transaction_id), + + contract_element_id INTEGER UNIQUE REFERENCES v2_file_contract_elements(id) ON DELETE CASCADE NOT NULL +); + CREATE TABLE v2_host_announcements ( transaction_id INTEGER REFERENCES v2_transactions(id) ON DELETE CASCADE NOT NULL, transaction_order INTEGER NOT NULL, diff --git a/persist/sqlite/v2consensus.go b/persist/sqlite/v2consensus.go index 9f9d6efb..c6e1e492 100644 --- a/persist/sqlite/v2consensus.go +++ b/persist/sqlite/v2consensus.go @@ -62,6 +62,181 @@ func addV2Transactions(tx *txn, bid types.BlockID, txns []types.V2Transaction) ( return txnDBIds, nil } +func updateV2FileContractElements(tx *txn, revert bool, b types.Block, fces []explorer.V2FileContractUpdate) (map[explorer.DBFileContract]int64, error) { + stmt, err := tx.Prepare(`INSERT INTO v2_file_contract_elements(contract_id, block_id, transaction_id, leaf_index, capacity, filesize, file_merkle_root, proof_height, expiration_height, renter_output_address, renter_output_value, host_output_address, host_output_value, missed_host_value, total_collateral, renter_public_key, host_public_key, revision_number, renter_signature, host_signature) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (contract_id, revision_number) + DO UPDATE SET leaf_index = ? + RETURNING id;`) + if err != nil { + return nil, fmt.Errorf("updateV2FileContractElements: failed to prepare main statement: %w", err) + } + defer stmt.Close() + + revisionStmt, err := tx.Prepare(`INSERT INTO v2_last_contract_revision(contract_id, contract_element_id) + VALUES (?, ?) + ON CONFLICT (contract_id) + DO UPDATE SET contract_element_id = ?`) + if err != nil { + return nil, fmt.Errorf("updateV2FileContractElements: failed to prepare last_contract_revision statement: %w", err) + } + defer revisionStmt.Close() + + fcTxns := make(map[explorer.DBFileContract]types.TransactionID) + for _, txn := range b.V2Transactions() { + id := txn.ID() + + for i, fc := range txn.FileContracts { + fcTxns[explorer.DBFileContract{ + ID: txn.V2FileContractID(id, i), + RevisionNumber: fc.RevisionNumber, + }] = id + } + for _, fcr := range txn.FileContractRevisions { + fcTxns[explorer.DBFileContract{ + ID: types.FileContractID(fcr.Parent.ID), + RevisionNumber: fcr.Revision.RevisionNumber, + }] = id + } + } + + fcDBIds := make(map[explorer.DBFileContract]int64) + addFC := func(fcID types.FileContractID, leafIndex uint64, fc types.V2FileContract, resolution types.V2FileContractResolutionType, lastRevision bool) error { + var dbID int64 + dbFC := explorer.DBFileContract{ID: fcID, RevisionNumber: fc.RevisionNumber} + err := stmt.QueryRow(encode(fcID), encode(b.ID()), encode(fcTxns[dbFC]), encode(leafIndex), encode(fc.Capacity), encode(fc.Filesize), encode(fc.FileMerkleRoot), encode(fc.ProofHeight), encode(fc.ExpirationHeight), encode(fc.RenterOutput.Address), encode(fc.RenterOutput.Value), encode(fc.HostOutput.Address), encode(fc.HostOutput.Value), encode(fc.MissedHostValue), encode(fc.TotalCollateral), encode(fc.RenterPublicKey), encode(fc.HostPublicKey), encode(fc.RevisionNumber), encode(fc.RenterSignature), encode(fc.HostSignature), encode(leafIndex)).Scan(&dbID) + if err != nil { + return fmt.Errorf("failed to execute file_contract_elements statement: %w", err) + } + + // only update if it's the most recent revision which will come from + // running ForEachFileContractElement on the update + if lastRevision { + if _, err := revisionStmt.Exec(encode(fcID), dbID, dbID); err != nil { + return fmt.Errorf("failed to update last revision number: %w", err) + } + } + + fcDBIds[dbFC] = dbID + return nil + } + + for _, update := range fces { + var fce *types.V2FileContractElement + + if revert { + // Reverting + if update.Revision != nil { + // Contract revision reverted. + // We are reverting the revision, so get the contract before + // the revision. + fce = update.Revision + } else { + // Contract formation reverted. + // The contract update has no revision, therefore it refers + // to the original contract formation. + continue + } + } else { + // Applying + fce = &update.FileContractElement + if update.Revision != nil { + // Contract is revised. + // We want last_contract_revision to refer to the latest + // revision, so use the revision FCE if there is one. + fce = update.Revision + } + } + + if err := addFC( + types.FileContractID(fce.StateElement.ID), + fce.StateElement.LeafIndex, + fce.V2FileContract, + update.Resolution, + true, + ); err != nil { + return nil, fmt.Errorf("updateFileContractElements: %w", err) + } + } + + if revert { + return fcDBIds, nil + } + + for _, txn := range b.V2Transactions() { + for j, fc := range txn.FileContracts { + fcID := txn.V2FileContractID(txn.ID(), j) + dbFC := explorer.DBFileContract{ID: fcID, RevisionNumber: fc.RevisionNumber} + if _, exists := fcDBIds[dbFC]; exists { + continue + } + + if err := addFC(fcID, 0, fc, nil, false); err != nil { + return nil, fmt.Errorf("updateFileContractElements: %w", err) + } + } + for _, fcr := range txn.FileContractRevisions { + fc := fcr.Revision + fcid := types.FileContractID(fcr.Parent.ID) + dbFC := explorer.DBFileContract{ID: fcid, RevisionNumber: fc.RevisionNumber} + if _, exists := fcDBIds[dbFC]; exists { + continue + } + + if err := addFC(fcid, 0, fc, nil, false); err != nil { + return nil, fmt.Errorf("updateFileContractElements: %w", err) + } + } + } + + return fcDBIds, nil +} + +func updateV2FileContractIndices(tx *txn, revert bool, index types.ChainIndex, fces []explorer.V2FileContractUpdate) error { + confirmationIndexStmt, err := tx.Prepare(`UPDATE v2_last_contract_revision SET confirmation_index = ?, confirmation_transaction_id = ? WHERE contract_id = ?`) + if err != nil { + return fmt.Errorf("updateV2FileContractIndices: failed to prepare confirmation index statement: %w", err) + } + defer confirmationIndexStmt.Close() + + resolutionIndexStmt, err := tx.Prepare(`UPDATE v2_last_contract_revision SET resolution = ?, resolution_index = ?, resolution_transaction_id = ? WHERE contract_id = ?`) + if err != nil { + return fmt.Errorf("updateV2FileContractIndices: failed to prepare resolution index statement: %w", err) + } + defer resolutionIndexStmt.Close() + + for _, update := range fces { + // id stays the same even if revert happens so we don't need to check that here + fcID := update.FileContractElement.ID + + if revert { + if update.ConfirmationTransactionID != nil { + if _, err := confirmationIndexStmt.Exec(nil, nil, encode(fcID)); err != nil { + return fmt.Errorf("updateV2FileContractIndices: failed to update confirmation index: %w", err) + } + } + if update.ResolutionTransactionID != nil { + if _, err := resolutionIndexStmt.Exec(nil, nil, encode(fcID)); err != nil { + return fmt.Errorf("updateV2FileContractIndices: failed to update proof index: %w", err) + } + } + } else { + if update.ConfirmationTransactionID != nil { + if _, err := confirmationIndexStmt.Exec(encode(index), encode(update.ConfirmationTransactionID), encode(fcID)); err != nil { + return fmt.Errorf("updateV2FileContractIndices: failed to update confirmation index: %w", err) + } + } + if update.ResolutionTransactionID != nil { + if _, err := resolutionIndexStmt.Exec(encode(index), encode(update.ResolutionTransactionID), encode(fcID)); err != nil { + return fmt.Errorf("updateV2FileContractIndices: failed to update proof index: %w", err) + } + } + } + } + + return nil +} + func addV2SiacoinInputs(tx *txn, txnID int64, txn types.V2Transaction, dbIDs map[types.SiacoinOutputID]int64) error { stmt, err := tx.Prepare(`INSERT INTO v2_transaction_siacoin_inputs(transaction_id, transaction_order, parent_id, satisfied_policy) VALUES (?, ?, ?, ?)`) if err != nil { @@ -144,6 +319,29 @@ func addV2SiafundOutputs(tx *txn, txnID int64, txn types.V2Transaction, dbIDs ma return nil } +func addV2FileContracts(tx *txn, txnID int64, txn types.V2Transaction, dbIDs map[explorer.DBFileContract]int64) error { + stmt, err := tx.Prepare(`INSERT INTO v2_transaction_file_contracts(transaction_id, transaction_order, contract_id) VALUES (?, ?, ?)`) + if err != nil { + return fmt.Errorf("addV2FileContracts: failed to prepare statement: %w", err) + } + defer stmt.Close() + + for i, fc := range txn.FileContracts { + dbID, ok := dbIDs[explorer.DBFileContract{ + ID: txn.V2FileContractID(txn.ID(), i), + RevisionNumber: fc.RevisionNumber, + }] + if !ok { + return errors.New("addV2FileContracts: dbID not in map") + } + + if _, err := stmt.Exec(txnID, i, dbID); err != nil { + return fmt.Errorf("addV2FileContracts: failed to execute statement: %w", err) + } + } + return nil +} + func addV2Attestations(tx *txn, txnID int64, txn types.V2Transaction) error { stmt, err := tx.Prepare(`INSERT INTO v2_transaction_attestations(transaction_id, transaction_order, public_key, key, value, signature) VALUES (?, ?, ?, ?, ?, ?)`) if err != nil { @@ -159,7 +357,7 @@ func addV2Attestations(tx *txn, txnID int64, txn types.V2Transaction) error { return nil } -func addV2TransactionFields(tx *txn, txns []types.V2Transaction, scDBIds map[types.SiacoinOutputID]int64, sfDBIds map[types.SiafundOutputID]int64, fcDBIds map[explorer.DBFileContract]int64, v2TxnDBIds map[types.TransactionID]txnDBId) error { +func addV2TransactionFields(tx *txn, txns []types.V2Transaction, scDBIds map[types.SiacoinOutputID]int64, sfDBIds map[types.SiafundOutputID]int64, v2FcDBIds map[explorer.DBFileContract]int64, v2TxnDBIds map[types.TransactionID]txnDBId) error { for _, txn := range txns { dbID, ok := v2TxnDBIds[txn.ID()] if !ok { @@ -181,6 +379,8 @@ func addV2TransactionFields(tx *txn, txns []types.V2Transaction, scDBIds map[typ return fmt.Errorf("failed to add siafund inputs: %w", err) } else if err := addV2SiafundOutputs(tx, dbID.id, txn, sfDBIds); err != nil { return fmt.Errorf("failed to add siafund outputs: %w", err) + } else if err := addV2FileContracts(tx, dbID.id, txn, v2FcDBIds); err != nil { + return fmt.Errorf("failed to add file contracts: %w", err) } } diff --git a/persist/sqlite/v2consensus_test.go b/persist/sqlite/v2consensus_test.go index 5f24e7e2..3528e6bb 100644 --- a/persist/sqlite/v2consensus_test.go +++ b/persist/sqlite/v2consensus_test.go @@ -2,6 +2,7 @@ package sqlite_test import ( "testing" + "time" "go.sia.tech/core/consensus" "go.sia.tech/core/types" @@ -339,3 +340,435 @@ func TestV2SiafundOutput(t *testing.T) { testutil.CheckBalance(t, db, addr1, types.ZeroCurrency, types.ZeroCurrency, giftSF/2) testutil.CheckBalance(t, db, addr2, types.ZeroCurrency, types.ZeroCurrency, giftSF/2) } + +func TestV2FileContract(t *testing.T) { + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + addr1Policy := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + + renterPrivateKey := types.GeneratePrivateKey() + renterPublicKey := renterPrivateKey.PublicKey() + + hostPrivateKey := types.GeneratePrivateKey() + hostPublicKey := hostPrivateKey.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 + + v1FC := testutil.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), 100, 105, types.VoidAddress) + v1FC.Filesize = 65 + v2FC := types.V2FileContract{ + Capacity: v1FC.Filesize, + Filesize: v1FC.Filesize, + FileMerkleRoot: v1FC.FileMerkleRoot, + ProofHeight: 20, + ExpirationHeight: 30, + RenterOutput: v1FC.ValidProofOutputs[0], + HostOutput: v1FC.ValidProofOutputs[1], + MissedHostValue: v1FC.MissedProofOutputs[1].Value, + TotalCollateral: v1FC.ValidProofOutputs[0].Value, + RenterPublicKey: renterPublicKey, + HostPublicKey: hostPublicKey, + } + fcOut := v2FC.RenterOutput.Value.Add(v2FC.HostOutput.Value).Add(cm.TipState().V2FileContractTax(v2FC)) + + txn1 := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, genesisBlock.Transactions[0].SiacoinOutputID(0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + + SiacoinOutputs: []types.SiacoinOutput{{ + Value: giftSC.Sub(fcOut), + Address: addr1, + }}, + + FileContracts: []types.V2FileContract{v2FC}, + } + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey, hostPrivateKey, &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]) + } + + txn2 := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, txn1.SiacoinOutputID(txn1.ID(), 0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + // 1 for txn1, 2 for this transaction + Value: giftSC.Sub(fcOut.Mul64(3)), + Address: addr1, + }}, + + FileContracts: []types.V2FileContract{v2FC, v2FC}, + } + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey, hostPrivateKey, &txn2) + + if err := cm.AddBlocks([]types.Block{testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn2}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn2.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn2, dbTxns[0]) + } + + for i := cm.Tip().Height; i < v2FC.ExpirationHeight; i++ { + if err := cm.AddBlocks([]types.Block{testutil.MineBlock(cm.TipState(), nil, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + } + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn1.ID(), txn2.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn1, dbTxns[0]) + testutil.CheckV2Transaction(t, txn2, dbTxns[1]) + } +} + +func TestV2FileContractRevert(t *testing.T) { + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + addr1Policy := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + + renterPrivateKey := types.GeneratePrivateKey() + renterPublicKey := renterPrivateKey.PublicKey() + + hostPrivateKey := types.GeneratePrivateKey() + hostPublicKey := hostPrivateKey.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 + prevState := cm.TipState() + + v1FC := testutil.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), 100, 105, types.VoidAddress) + v1FC.Filesize = 65 + v2FC := types.V2FileContract{ + Capacity: v1FC.Filesize, + Filesize: v1FC.Filesize, + FileMerkleRoot: v1FC.FileMerkleRoot, + ProofHeight: 20, + ExpirationHeight: 30, + RenterOutput: v1FC.ValidProofOutputs[0], + HostOutput: v1FC.ValidProofOutputs[1], + MissedHostValue: v1FC.MissedProofOutputs[1].Value, + TotalCollateral: v1FC.ValidProofOutputs[0].Value, + RenterPublicKey: renterPublicKey, + HostPublicKey: hostPublicKey, + } + fcOut := v2FC.RenterOutput.Value.Add(v2FC.HostOutput.Value).Add(cm.TipState().V2FileContractTax(v2FC)) + + txn1 := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, genesisBlock.Transactions[0].SiacoinOutputID(0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + + SiacoinOutputs: []types.SiacoinOutput{{ + Value: giftSC.Sub(fcOut).Sub(fcOut), + Address: addr1, + }}, + + FileContracts: []types.V2FileContract{v2FC, v2FC}, + } + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey, hostPrivateKey, &txn1) + + b1 := testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn1}, types.VoidAddress) + if err := cm.AddBlocks([]types.Block{b1}); 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]) + } + + { + fcs, err := db.V2Contracts([]types.FileContractID{txn1.V2FileContractID(txn1.ID(), 0), txn1.V2FileContractID(txn1.ID(), 1)}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn1.FileContracts[0], fcs[0]) + testutil.CheckV2FC(t, txn1.FileContracts[1], fcs[1]) + } + + { + fcs, err := db.V2ContractRevisions(txn1.V2FileContractID(txn1.ID(), 0)) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn1.FileContracts[0], fcs[0]) + } + + // revert the block + { + state := prevState + extra := cm.Tip().Height - state.Index.Height + 1 + + var blocks []types.Block + for i := uint64(0); i < extra; i++ { + var bs consensus.V1BlockSupplement + block := testutil.MineBlock(state, nil, types.VoidAddress) + blocks = append(blocks, block) + + if err := consensus.ValidateBlock(state, block, bs); err != nil { + t.Fatal(err) + } + state, _ = consensus.ApplyBlock(state, block, bs, time.Time{}) + } + + if err := cm.AddBlocks(blocks); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + } + + { + _, err := db.Block(b1.ID()) + if err == nil { + t.Fatal("block should not exist") + } + } + + { + _, err := db.V2Contracts([]types.FileContractID{txn1.V2FileContractID(txn1.ID(), 0)}) + if err == nil { + t.Fatal("contract should not exist") + } + } + + // See if we can spend the genesis input that was spent in reverted block + // We should be able to + txn2 := txn1 + txn2.FileContracts = txn2.FileContracts[:1] + txn2.SiacoinOutputs[0].Value = txn2.SiacoinOutputs[0].Value.Add(fcOut) + txn2.SiacoinInputs[0].Parent = getSCE(t, db, genesisBlock.Transactions[0].SiacoinOutputID(0)) + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey, hostPrivateKey, &txn2) + b2 := testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn2}, types.VoidAddress) + if err := cm.AddBlocks([]types.Block{b2}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + { + _, err := db.Block(b1.ID()) + if err == nil { + t.Fatal("block should not exist") + } + } + + { + b, err := db.Block(b2.ID()) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn2, b.V2.Transactions[0]) + } + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn2.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn2, dbTxns[0]) + } + + { + fcs, err := db.V2Contracts([]types.FileContractID{txn2.V2FileContractID(txn2.ID(), 0)}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn2.FileContracts[0], fcs[0]) + } + + { + fcs, err := db.V2ContractRevisions(txn1.V2FileContractID(txn2.ID(), 0)) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn2.FileContracts[0], fcs[0]) + } +} + +func TestV2FileContractKey(t *testing.T) { + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + addr1Policy := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(pk1.PublicKey()))} + + renterPrivateKey1 := types.GeneratePrivateKey() + renterPublicKey1 := renterPrivateKey1.PublicKey() + + renterPrivateKey2 := types.GeneratePrivateKey() + renterPublicKey2 := renterPrivateKey2.PublicKey() + + hostPrivateKey := types.GeneratePrivateKey() + hostPublicKey := hostPrivateKey.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 + + v1FC := testutil.PrepareContractFormation(renterPublicKey1, hostPublicKey, types.Siacoins(1), types.Siacoins(1), 100, 105, types.VoidAddress) + v1FC.Filesize = 65 + v2FC1 := types.V2FileContract{ + Capacity: v1FC.Filesize, + Filesize: v1FC.Filesize, + FileMerkleRoot: v1FC.FileMerkleRoot, + ProofHeight: 20, + ExpirationHeight: 30, + RenterOutput: v1FC.ValidProofOutputs[0], + HostOutput: v1FC.ValidProofOutputs[1], + MissedHostValue: v1FC.MissedProofOutputs[1].Value, + TotalCollateral: v1FC.ValidProofOutputs[0].Value, + RenterPublicKey: renterPublicKey1, + HostPublicKey: hostPublicKey, + } + fcOut := v2FC1.RenterOutput.Value.Add(v2FC1.HostOutput.Value).Add(cm.TipState().V2FileContractTax(v2FC1)) + + txn1 := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, genesisBlock.Transactions[0].SiacoinOutputID(0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Value: giftSC.Sub(fcOut), + Address: addr1, + }}, + FileContracts: []types.V2FileContract{v2FC1}, + } + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey1, hostPrivateKey, &txn1) + + b1 := testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn1}, types.VoidAddress) + if err := cm.AddBlocks([]types.Block{b1}); 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]) + } + + { + fcs, err := db.V2Contracts([]types.FileContractID{txn1.V2FileContractID(txn1.ID(), 0)}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn1.FileContracts[0], fcs[0]) + } + + { + fcs, err := db.V2ContractRevisions(txn1.V2FileContractID(txn1.ID(), 0)) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn1.FileContracts[0], fcs[0]) + } + + v2FC2 := v2FC1 + v2FC2.RenterPublicKey = renterPublicKey2 + + txn2 := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: getSCE(t, db, txn1.SiacoinOutputID(txn1.ID(), 0)), + SatisfiedPolicy: types.SatisfiedPolicy{Policy: addr1Policy}, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Value: txn1.SiacoinOutputs[0].Value.Sub(fcOut), + Address: addr1, + }}, + FileContracts: []types.V2FileContract{v2FC2}, + } + testutil.SignV2TransactionWithContracts(cm.TipState(), pk1, renterPrivateKey2, hostPrivateKey, &txn2) + + b2 := testutil.MineV2Block(cm.TipState(), []types.V2Transaction{txn2}, types.VoidAddress) + if err := cm.AddBlocks([]types.Block{b2}); err != nil { + t.Fatal(err) + } + syncDB(t, db, cm) + + { + dbTxns, err := db.V2Transactions([]types.TransactionID{txn2.ID()}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2Transaction(t, txn2, dbTxns[0]) + } + + { + fcs, err := db.V2Contracts([]types.FileContractID{txn1.V2FileContractID(txn2.ID(), 0)}) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn2.FileContracts[0], fcs[0]) + } + + { + fcs, err := db.V2ContractRevisions(txn1.V2FileContractID(txn2.ID(), 0)) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn2.FileContracts[0], fcs[0]) + } + + { + fcs, err := db.V2ContractsKey(renterPublicKey1) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn1.FileContracts[0], fcs[0]) + } + + { + fcs, err := db.V2ContractsKey(renterPublicKey2) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn2.FileContracts[0], fcs[0]) + } + + { + fcs, err := db.V2ContractsKey(hostPublicKey) + if err != nil { + t.Fatal(err) + } + testutil.CheckV2FC(t, txn1.FileContracts[0], fcs[0]) + testutil.CheckV2FC(t, txn2.FileContracts[0], fcs[1]) + } +} diff --git a/persist/sqlite/v2contracts.go b/persist/sqlite/v2contracts.go new file mode 100644 index 00000000..ec694fcc --- /dev/null +++ b/persist/sqlite/v2contracts.go @@ -0,0 +1,125 @@ +package sqlite + +import ( + "fmt" + + "go.sia.tech/core/types" + "go.sia.tech/explored/explorer" +) + +func scanV2FileContract(s scanner) (fce explorer.V2FileContract, err error) { + var resolution types.V2FileContractResolutionType + var confirmationIndex, resolutionIndex types.ChainIndex + var confirmationTransactionID, resolutionTransactionID types.TransactionID + + fc := &fce.V2FileContractElement.V2FileContract + if err = s.Scan(decodeNull(&confirmationIndex), decodeNull(&confirmationTransactionID), decodeNull(&resolution), decodeNull(&resolutionIndex), decodeNull(&resolutionTransactionID), decode(&fce.V2FileContractElement.ID), decode(&fce.V2FileContractElement.LeafIndex), decode(&fc.Capacity), decode(&fc.Filesize), decode(&fc.FileMerkleRoot), decode(&fc.ProofHeight), decode(&fc.ExpirationHeight), decode(&fc.RenterOutput.Address), decode(&fc.RenterOutput.Value), decode(&fc.HostOutput.Address), decode(&fc.HostOutput.Value), decode(&fc.MissedHostValue), decode(&fc.TotalCollateral), decode(&fc.RenterPublicKey), decode(&fc.HostPublicKey), decode(&fc.RevisionNumber), decode(&fc.RenterSignature), decode(&fc.HostSignature)); err != nil { + return + } + + if resolution != nil { + fce.Resolution = &resolution + } + if confirmationIndex != (types.ChainIndex{}) { + fce.ConfirmationIndex = &confirmationIndex + } + if resolutionIndex != (types.ChainIndex{}) { + fce.ResolutionIndex = &resolutionIndex + } + if confirmationTransactionID != (types.TransactionID{}) { + fce.ConfirmationTransactionID = &confirmationTransactionID + } + if resolutionTransactionID != (types.TransactionID{}) { + fce.ResolutionTransactionID = &resolutionTransactionID + } + + return +} + +// V2Contracts implements explorer.Store. +func (s *Store) V2Contracts(ids []types.FileContractID) (result []explorer.V2FileContract, err error) { + err = s.transaction(func(tx *txn) error { + stmt, err := tx.Prepare(`SELECT rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature +FROM v2_last_contract_revision rev +INNER JOIN v2_file_contract_elements fc ON (rev.contract_element_id = fc.id) +WHERE rev.contract_id = ? +`) + if err != nil { + return err + } + defer stmt.Close() + + for _, id := range ids { + fc, err := scanV2FileContract(stmt.QueryRow(encode(id))) + if err != nil { + return fmt.Errorf("failed to scan file contract: %w", err) + } + result = append(result, fc) + } + + return nil + }) + + return +} + +// V2ContractRevisions implements explorer.Store. +func (s *Store) V2ContractRevisions(id types.FileContractID) (revisions []explorer.V2FileContract, err error) { + err = s.transaction(func(tx *txn) error { + query := `SELECT rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature +FROM v2_file_contract_elements fc +INNER JOIN v2_last_contract_revision rev ON (rev.contract_id = fc.contract_id) +WHERE fc.contract_id = ? +ORDER BY fc.revision_number ASC +` + rows, err := tx.Query(query, encode(id)) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + fc, err := scanV2FileContract(rows) + if err != nil { + return fmt.Errorf("failed to scan file contract: %w", err) + } + + revisions = append(revisions, fc) + } + + if len(revisions) == 0 { + return explorer.ErrContractNotFound + } + return nil + }) + return +} + +// V2ContractsKey implements explorer.Store. +func (s *Store) V2ContractsKey(key types.PublicKey) (result []explorer.V2FileContract, err error) { + err = s.transaction(func(tx *txn) error { + encoded := encode(key) + rows, err := tx.Query(`SELECT rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature +FROM v2_last_contract_revision rev +INNER JOIN v2_file_contract_elements fc ON (rev.contract_element_id = fc.id) +WHERE fc.renter_public_key = ? OR fc.host_public_key = ? +ORDER BY rev.confirmation_index ASC +`, encoded, encoded) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + fc, err := scanV2FileContract(rows) + if err != nil { + return fmt.Errorf("failed to scan file contract: %w", err) + } + result = append(result, fc) + } + + return nil + }) + + return +} diff --git a/persist/sqlite/v2transactions.go b/persist/sqlite/v2transactions.go index 935e0798..8ad45f48 100644 --- a/persist/sqlite/v2transactions.go +++ b/persist/sqlite/v2transactions.go @@ -74,6 +74,8 @@ func getV2Transactions(tx *txn, ids []types.TransactionID) ([]explorer.V2Transac return nil, fmt.Errorf("getV2Transactions: failed to get siafund inputs: %w", err) } else if err := fillV2TransactionSiafundOutputs(tx, dbIDs, txns); err != nil { return nil, fmt.Errorf("getV2Transactions: failed to get siafund outputs: %w", err) + } else if err := fillV2TransactionFileContracts(tx, dbIDs, txns); err != nil { + return nil, fmt.Errorf("getV2Transactions: failed to get file contracts: %w", err) } // add host announcements if we have any @@ -149,7 +151,7 @@ func fillV2TransactionAttestations(tx *txn, dbIDs []int64, txns []explorer.V2Tra return nil } -// fillV2TransactionSiacoinInputs fills in the siacoin outputs for each +// fillV2TransactionSiacoinInputs fills in the siacoin inputs for each // transaction. func fillV2TransactionSiacoinInputs(tx *txn, dbIDs []int64, txns []explorer.V2Transaction) error { stmt, err := tx.Prepare(`SELECT ts.satisfied_policy, sc.output_id, sc.leaf_index, sc.maturity_height, sc.address, sc.value @@ -229,7 +231,7 @@ ORDER BY ts.transaction_order ASC`) return nil } -// fillV2TransactionSiafundInputs fills in the siacoin outputs for each +// fillV2TransactionSiafundInputs fills in the siacoin inputs for each // transaction. func fillV2TransactionSiafundInputs(tx *txn, dbIDs []int64, txns []explorer.V2Transaction) error { stmt, err := tx.Prepare(`SELECT ts.satisfied_policy, ts.claim_address, sf.output_id, sf.leaf_index, sf.address, sf.value @@ -267,7 +269,7 @@ ORDER BY ts.transaction_order ASC`) return nil } -// fillV2TransactionSiafundOutputs fills in the siacoin outputs for each +// fillV2TransactionSiafundOutputs fills in the siafund outputs for each // transaction. func fillV2TransactionSiafundOutputs(tx *txn, dbIDs []int64, txns []explorer.V2Transaction) error { stmt, err := tx.Prepare(`SELECT sf.output_id, sf.leaf_index, sf.spent_index, sf.claim_start, sf.address, sf.value @@ -309,6 +311,66 @@ ORDER BY ts.transaction_order ASC`) return nil } +// fillV2TransactionFileContracts fills in the file contracts for each +// transaction. +func fillV2TransactionFileContracts(tx *txn, dbIDs []int64, txns []explorer.V2Transaction) error { + stmt, err := tx.Prepare(`SELECT fc.transaction_id, rev.confirmation_index, rev.confirmation_transaction_id, rev.resolution, rev.resolution_index, rev.resolution_transaction_id, fc.contract_id, fc.leaf_index, fc.capacity, fc.filesize, fc.file_merkle_root, fc.proof_height, fc.expiration_height, fc.renter_output_address, fc.renter_output_value, fc.host_output_address, fc.host_output_value, fc.missed_host_value, fc.total_collateral, fc.renter_public_key, fc.host_public_key, fc.revision_number, fc.renter_signature, fc.host_signature +FROM v2_file_contract_elements fc +INNER JOIN v2_transaction_file_contracts ts ON (ts.contract_id = fc.id) +INNER JOIN v2_last_contract_revision rev ON (rev.contract_id = fc.contract_id) +WHERE ts.transaction_id = ? +ORDER BY ts.transaction_order ASC`) + if err != nil { + return fmt.Errorf("failed to prepare file contracts statement: %w", err) + } + defer stmt.Close() + + for i, dbID := range dbIDs { + err := func() error { + rows, err := stmt.Query(dbID) + if err != nil { + return fmt.Errorf("failed to query file contracts: %w", err) + } + defer rows.Close() + + for rows.Next() { + var resolution types.V2FileContractResolutionType + var confirmationIndex, resolutionIndex types.ChainIndex + var confirmationTransactionID, resolutionTransactionID types.TransactionID + + var fce explorer.V2FileContract + fc := &fce.V2FileContractElement.V2FileContract + if err := rows.Scan(decode(&fce.TransactionID), decodeNull(&confirmationIndex), decodeNull(&confirmationTransactionID), decodeNull(&resolution), decodeNull(&resolutionIndex), decodeNull(&resolutionTransactionID), decode(&fce.V2FileContractElement.ID), decode(&fce.V2FileContractElement.LeafIndex), decode(&fc.Capacity), decode(&fc.Filesize), decode(&fc.FileMerkleRoot), decode(&fc.ProofHeight), decode(&fc.ExpirationHeight), decode(&fc.RenterOutput.Address), decode(&fc.RenterOutput.Value), decode(&fc.HostOutput.Address), decode(&fc.HostOutput.Value), decode(&fc.MissedHostValue), decode(&fc.TotalCollateral), decode(&fc.RenterPublicKey), decode(&fc.HostPublicKey), decode(&fc.RevisionNumber), decode(&fc.RenterSignature), decode(&fc.HostSignature)); err != nil { + return fmt.Errorf("failed to scan file contract: %w", err) + } + + if resolution != nil { + fce.Resolution = &resolution + } + if confirmationIndex != (types.ChainIndex{}) { + fce.ConfirmationIndex = &confirmationIndex + } + if resolutionIndex != (types.ChainIndex{}) { + fce.ResolutionIndex = &resolutionIndex + } + if confirmationTransactionID != (types.TransactionID{}) { + fce.ConfirmationTransactionID = &confirmationTransactionID + } + if resolutionTransactionID != (types.TransactionID{}) { + fce.ResolutionTransactionID = &resolutionTransactionID + } + + txns[i].FileContracts = append(txns[i].FileContracts, fce) + } + return nil + }() + if err != nil { + return err + } + } + return nil +} + // V2Transactions implements explorer.Store. func (s *Store) V2Transactions(ids []types.TransactionID) (results []explorer.V2Transaction, err error) { err = s.transaction(func(tx *txn) error {