Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add host contracts history #39

Merged
merged 11 commits into from
Jun 7, 2024
14 changes: 10 additions & 4 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,25 @@ func (c *Client) Transactions(ids []types.TransactionID) (resp []explorer.Transa

// AddressUTXOs returns the specified address' unspent outputs.
func (c *Client) AddressUTXOs(address types.Address, offset, limit uint64) (resp AddressUTXOsResponse, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/addresses/%s/utxos?offset=%d&limit=%d", address, offset, limit), &resp)
err = c.c.GET(fmt.Sprintf("/explorer/addresses/utxos/%s?offset=%d&limit=%d", address, offset, limit), &resp)
return
}

// AddressEvents returns the specified address' events.
func (c *Client) AddressEvents(address types.Address, offset, limit uint64) (resp []explorer.Event, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/addresses/%s/events?offset=%d&limit=%d", address, offset, limit), &resp)
err = c.c.GET(fmt.Sprintf("/explorer/addresses/events/%s?offset=%d&limit=%d", address, offset, limit), &resp)
return
}

// AddressBalance returns the specified address' balance.
func (c *Client) AddressBalance(address types.Address) (resp AddressBalanceResponse, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/addresses/%s/balance", address), &resp)
err = c.c.GET(fmt.Sprintf("/explorer/addresses/balance/%s", address), &resp)
return
}

// Contract returns the file contract with the specified ID.
func (c *Client) Contract(id types.FileContractID) (resp explorer.FileContract, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/contracts/%s", id), &resp)
err = c.c.GET(fmt.Sprintf("/explorer/contracts/id/%s", id), &resp)
chris124567 marked this conversation as resolved.
Show resolved Hide resolved
return
}

Expand All @@ -116,6 +116,12 @@ func (c *Client) Contracts(ids []types.FileContractID) (resp []explorer.FileCont
return
}

// ContractsKey returns the contracts for a particular ed25519 key.
func (c *Client) ContractsKey(key types.PublicKey) (resp []explorer.FileContract, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/contracts/key/%s", key), &resp)
return
}

// Metrics returns the most recent metrics about Sia.
func (c *Client) Metrics() (resp explorer.Metrics, err error) {
err = c.c.GET("/explorer/metrics", &resp)
Expand Down
27 changes: 23 additions & 4 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type (
UnspentSiafundOutputs(address types.Address, offset, limit uint64) ([]explorer.SiafundOutput, error)
AddressEvents(address types.Address, offset, limit uint64) (events []explorer.Event, err error)
Contracts(ids []types.FileContractID) (result []explorer.FileContract, err error)
ContractsKey(key types.PublicKey) (result []explorer.FileContract, err error)
}
)

Expand Down Expand Up @@ -305,6 +306,23 @@ func (s *server) explorerContractIDHandler(jc jape.Context) {
jc.Encode(fcs[0])
}

func (s *server) explorerContractKeyHandler(jc jape.Context) {
errNotFound := errors.New("no contract found")

var key types.PublicKey
if jc.DecodeParam("key", &key) != nil {
return
}
fcs, err := s.e.ContractsKey(key)
if jc.Check("failed to get contracts", err) != nil {
return
} else if len(fcs) == 0 {
jc.Error(errNotFound, http.StatusNotFound)
return
}
jc.Encode(fcs)
}

func (s *server) explorerContractsHandler(jc jape.Context) {
var ids []types.FileContractID
if jc.Decode(&ids) != nil {
Expand Down Expand Up @@ -343,10 +361,11 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler {
"GET /explorer/metrics/:id": srv.explorerMetricsIDHandler,
"GET /explorer/transactions/:id": srv.explorerTransactionsIDHandler,
"POST /explorer/transactions": srv.explorerTransactionsHandler,
"GET /explorer/addresses/:address/utxos": srv.explorerAddressessAddressUtxosHandler,
"GET /explorer/addresses/:address/events": srv.explorerAddressessAddressEventsHandler,
"GET /explorer/addresses/:address/balance": srv.explorerAddressessAddressBalanceHandler,
"GET /explorer/contracts/:id": srv.explorerContractIDHandler,
"GET /explorer/addresses/utxos/:address": srv.explorerAddressessAddressUtxosHandler,
chris124567 marked this conversation as resolved.
Show resolved Hide resolved
"GET /explorer/addresses/events/:address": srv.explorerAddressessAddressEventsHandler,
"GET /explorer/addresses/balance/:address": srv.explorerAddressessAddressBalanceHandler,
"GET /explorer/contracts/id/:id": srv.explorerContractIDHandler,
"GET /explorer/contracts/key/:key": srv.explorerContractKeyHandler,
"POST /explorer/contracts": srv.explorerContractsHandler,
})
}
6 changes: 6 additions & 0 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Store interface {
AddressEvents(address types.Address, offset, limit uint64) (events []Event, err error)
Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error)
Contracts(ids []types.FileContractID) (result []FileContract, err error)
ContractsKey(key types.PublicKey) (result []FileContract, err error)
}

// Explorer implements a Sia explorer.
Expand Down Expand Up @@ -168,3 +169,8 @@ func (e *Explorer) Balance(address types.Address) (sc types.Currency, immatureSC
func (e *Explorer) Contracts(ids []types.FileContractID) (result []FileContract, err error) {
return e.s.Contracts(ids)
}

// ContractsKey returns the contracts for a particular ed25519 key.
func (e *Explorer) ContractsKey(key types.PublicKey) (result []FileContract, err error) {
return e.s.ContractsKey(key)
}
48 changes: 43 additions & 5 deletions persist/sqlite/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,18 +741,50 @@ func addFileContractElements(tx *txn, b types.Block, fces []explorer.FileContrac
}
defer stmt.Close()

revisionStmt, err := tx.Prepare(`INSERT INTO last_contract_revision(contract_id, contract_element_id)
VALUES (?, ?)
revisionStmt, err := tx.Prepare(`INSERT INTO last_contract_revision(contract_id, contract_element_id, ed25519_renter_key, ed25519_host_key)
VALUES (?, ?, ?, ?)
ON CONFLICT (contract_id)
DO UPDATE SET contract_element_id = ?`)
DO UPDATE SET contract_element_id = ?, ed25519_renter_key = COALESCE(?, ed25519_renter_key), ed25519_host_key = COALESCE(?, ed25519_host_key)`)
if err != nil {
return nil, fmt.Errorf("addFileContractElements: failed to prepare last_contract_revision statement: %w", err)
}
defer revisionStmt.Close()

fcKeys := make(map[explorer.DBFileContract][2]types.PublicKey)
// populate fcKeys using revision UnlockConditions fields
for _, txn := range b.Transactions {
for _, fcr := range txn.FileContractRevisions {
fc := fcr.FileContract
uc := fcr.UnlockConditions
dbFC := explorer.DBFileContract{ID: fcr.ParentID, RevisionNumber: fc.RevisionNumber}

// check for 2 ed25519 keys
ok := true
var result [2]types.PublicKey
for i := 0; i < 2; i++ {
// fewer than 2 keys
if i >= len(uc.PublicKeys) {
ok = false
break
}

if uc.PublicKeys[i].Algorithm == types.SpecifierEd25519 {
result[i] = types.PublicKey(uc.PublicKeys[i].Key)
} else {
// not an ed25519 key
ok = false
}
}
if ok {
fcKeys[dbFC] = result
}
}
}

fcDBIds := make(map[explorer.DBFileContract]int64)
addFC := func(fcID types.FileContractID, leafIndex uint64, fc types.FileContract, resolved, valid, lastRevision bool) error {
var dbID int64
dbFC := explorer.DBFileContract{ID: fcID, RevisionNumber: fc.RevisionNumber}
err := stmt.QueryRow(encode(b.ID()), encode(fcID), encode(leafIndex), fc.Filesize, encode(fc.FileMerkleRoot), fc.WindowStart, fc.WindowEnd, encode(fc.Payout), encode(fc.UnlockHash), encode(fc.RevisionNumber), resolved, valid, encode(leafIndex)).Scan(&dbID)
if err != nil {
return fmt.Errorf("failed to execute file_contract_elements statement: %w", err)
Expand All @@ -761,12 +793,18 @@ func addFileContractElements(tx *txn, b types.Block, fces []explorer.FileContrac
// 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 {
var renterKey, hostKey []byte
if keys, ok := fcKeys[dbFC]; ok {
renterKey = encode(keys[0]).([]byte)
hostKey = encode(keys[1]).([]byte)
}

if _, err := revisionStmt.Exec(encode(fcID), dbID, renterKey, hostKey, dbID, renterKey, hostKey); err != nil {
return fmt.Errorf("failed to update last revision number: %w", err)
}
}

fcDBIds[explorer.DBFileContract{ID: fcID, RevisionNumber: fc.RevisionNumber}] = dbID
fcDBIds[dbFC] = dbID
return nil
}

Expand Down
60 changes: 60 additions & 0 deletions persist/sqlite/consensus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,21 @@ func TestFileContract(t *testing.T) {
}
syncDB(t, db, cm)

{
renterContracts, err := db.ContractsKey(renterPublicKey)
if err != nil {
t.Fatal(err)
}
hostContracts, err := db.ContractsKey(hostPublicKey)
if err != nil {
t.Fatal(err)
}
check(t, "renter contracts and host contracts", len(renterContracts), len(hostContracts))
check(t, "len(contracts)", 1, len(renterContracts))
checkFC(false, true, fc, renterContracts[0])
checkFC(false, true, fc, hostContracts[0])
}

checkMetrics(t, db, explorer.Metrics{
Height: 2,
Difficulty: cm.TipState().Difficulty,
Expand Down Expand Up @@ -930,6 +945,21 @@ func TestFileContract(t *testing.T) {
syncDB(t, db, cm)
}

{
renterContracts, err := db.ContractsKey(renterPublicKey)
if err != nil {
t.Fatal(err)
}
hostContracts, err := db.ContractsKey(hostPublicKey)
if err != nil {
t.Fatal(err)
}
check(t, "renter contracts and host contracts", len(renterContracts), len(hostContracts))
check(t, "len(contracts)", 1, len(renterContracts))
checkFC(true, false, fc, renterContracts[0])
checkFC(true, false, fc, hostContracts[0])
}

checkMetrics(t, db, explorer.Metrics{
Height: cm.Tip().Height,
Difficulty: cm.TipState().Difficulty,
Expand Down Expand Up @@ -1069,6 +1099,21 @@ func TestEphemeralFileContract(t *testing.T) {
}
syncDB(t, db, cm)

{
renterContracts, err := db.ContractsKey(renterPublicKey)
if err != nil {
t.Fatal(err)
}
hostContracts, err := db.ContractsKey(hostPublicKey)
if err != nil {
t.Fatal(err)
}
check(t, "renter contracts and host contracts", len(renterContracts), len(hostContracts))
check(t, "len(contracts)", 1, len(renterContracts))
checkFC(true, false, true, revisedFC1, renterContracts[0])
checkFC(true, false, true, revisedFC1, hostContracts[0])
}

// Explorer.Contracts should return latest revision
{
dbFCs, err := db.Contracts([]types.FileContractID{fcID})
Expand Down Expand Up @@ -1142,6 +1187,21 @@ func TestEphemeralFileContract(t *testing.T) {
checkFC(true, false, true, revisedFC3, dbFCs[0])
}

{
renterContracts, err := db.ContractsKey(renterPublicKey)
if err != nil {
t.Fatal(err)
}
hostContracts, err := db.ContractsKey(hostPublicKey)
if err != nil {
t.Fatal(err)
}
check(t, "renter contracts and host contracts", len(renterContracts), len(hostContracts))
check(t, "len(contracts)", 1, len(renterContracts))
checkFC(true, false, true, revisedFC3, renterContracts[0])
checkFC(true, false, true, revisedFC3, hostContracts[0])
}

{
txns, err := db.Transactions([]types.TransactionID{reviseTxn2.ID()})
if err != nil {
Expand Down
59 changes: 51 additions & 8 deletions persist/sqlite/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import (
"go.sia.tech/explored/explorer"
)

// Contracts implements explorer.Store.
func (s *Store) Contracts(ids []types.FileContractID) (result []explorer.FileContract, err error) {
encodedIDs := func(ids []types.FileContractID) []any {
result := make([]any, len(ids))
for i, id := range ids {
result[i] = encode(id)
}
return result
func encodedIDs(ids []types.FileContractID) []any {
result := make([]any, len(ids))
for i, id := range ids {
result[i] = encode(id)
}
return result
}

// Contracts implements explorer.Store.
func (s *Store) Contracts(ids []types.FileContractID) (result []explorer.FileContract, err error) {
err = s.transaction(func(tx *txn) error {
query := `SELECT fc1.id, fc1.contract_id, fc1.leaf_index, fc1.resolved, fc1.valid, fc1.filesize, fc1.file_merkle_root, fc1.window_start, fc1.window_end, fc1.payout, fc1.unlock_hash, fc1.revision_number
FROM file_contract_elements fc1
Expand Down Expand Up @@ -57,3 +57,46 @@ func (s *Store) Contracts(ids []types.FileContractID) (result []explorer.FileCon

return
}

// ContractsKey implements explorer.Store.
func (s *Store) ContractsKey(key types.PublicKey) (result []explorer.FileContract, err error) {
err = s.transaction(func(tx *txn) error {
query := `SELECT fc1.id, fc1.contract_id, fc1.leaf_index, fc1.resolved, fc1.valid, fc1.filesize, fc1.file_merkle_root, fc1.window_start, fc1.window_end, fc1.payout, fc1.unlock_hash, fc1.revision_number
FROM file_contract_elements fc1
INNER JOIN last_contract_revision rev ON (rev.contract_element_id = fc1.id)
WHERE rev.ed25519_renter_key = ? OR rev.ed25519_host_key = ?`
rows, err := tx.Query(query, encode(key), encode(key))
if err != nil {
return err
}
defer rows.Close()

var contractIDs []int64
idContract := make(map[int64]explorer.FileContract)
for rows.Next() {
var contractID int64
var fc explorer.FileContract
if err := rows.Scan(&contractID, decode(&fc.StateElement.ID), decode(&fc.StateElement.LeafIndex), &fc.Resolved, &fc.Valid, &fc.Filesize, decode(&fc.FileMerkleRoot), &fc.WindowStart, &fc.WindowEnd, decode(&fc.Payout), decode(&fc.UnlockHash), decode(&fc.RevisionNumber)); err != nil {
return fmt.Errorf("failed to scan transaction: %w", err)
}

idContract[contractID] = fc
contractIDs = append(contractIDs, contractID)
}

proofOutputs, err := fileContractOutputs(tx, contractIDs)
if err != nil {
return fmt.Errorf("failed to get file contract outputs: %w", err)
}
for contractID, output := range proofOutputs {
fc := idContract[contractID]
fc.ValidProofOutputs = output.valid
fc.MissedProofOutputs = output.missed
result = append(result, fc)
}

return nil
})

return
}
2 changes: 2 additions & 0 deletions persist/sqlite/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ CREATE INDEX file_contract_elements_contract_id_index ON file_contract_elements(

CREATE TABLE last_contract_revision (
contract_id BLOB PRIMARY KEY NOT NULL,
ed25519_renter_key BLOB,
ed25519_host_key BLOB,
contract_element_id INTEGER UNIQUE REFERENCES file_contract_elements(id) ON DELETE CASCADE NOT NULL
);

Expand Down
Loading