Skip to content

Commit

Permalink
Merge pull request #39 from SiaFoundation/add-host-contracts-history
Browse files Browse the repository at this point in the history
Add host contracts history
  • Loading branch information
n8maninger authored Jun 7, 2024
2 parents bc290d0 + 1252eba commit 853e117
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 13 deletions.
6 changes: 6 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/pubkey/%s/contracts", 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
19 changes: 19 additions & 0 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 @@ -347,6 +365,7 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler {
"GET /explorer/addresses/:address/events": srv.explorerAddressessAddressEventsHandler,
"GET /explorer/addresses/:address/balance": srv.explorerAddressessAddressBalanceHandler,
"GET /explorer/contracts/:id": srv.explorerContractIDHandler,
"GET /explorer/pubkey/:key/contracts": 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

0 comments on commit 853e117

Please sign in to comment.