Skip to content

Commit

Permalink
Merge pull request #109 from SiaFoundation/add-v2
Browse files Browse the repository at this point in the history
Add basic v2 transactions and blocks
  • Loading branch information
n8maninger authored Oct 15, 2024
2 parents 3ebe449 + 837f753 commit 4d34627
Show file tree
Hide file tree
Showing 14 changed files with 672 additions and 12 deletions.
19 changes: 19 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ func (c *Client) TransactionChainIndices(id types.TransactionID, offset, limit u
return
}

// V2Transaction returns the v2 transaction with the specified ID.
func (c *Client) V2Transaction(id types.TransactionID) (resp explorer.V2Transaction, err error) {
err = c.c.GET(fmt.Sprintf("/v2/transactions/%s", id), &resp)
return
}

// V2Transactions returns the v2 transactions with the specified IDs.
func (c *Client) V2Transactions(ids []types.TransactionID) (resp []explorer.V2Transaction, err error) {
err = c.c.POST("/v2/transactions", ids, &resp)
return
}

// V2TransactionChainIndices returns chain indices a v2 transaction was
// included in.
func (c *Client) V2TransactionChainIndices(id types.TransactionID, offset, limit uint64) (resp []types.ChainIndex, err error) {
err = c.c.GET(fmt.Sprintf("/v2/transactions/%s/indices?offset=%d&limit=%d", id, offset, limit), &resp)
return
}

// AddressSiacoinUTXOs returns the specified address' unspent outputs.
func (c *Client) AddressSiacoinUTXOs(address types.Address, offset, limit uint64) (resp []explorer.SiacoinOutput, err error) {
err = c.c.GET(fmt.Sprintf("/addresses/%s/utxos/siacoin?offset=%d&limit=%d", address, offset, limit), &resp)
Expand Down
66 changes: 63 additions & 3 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type (
HostMetrics() (explorer.HostMetrics, error)
Transactions(ids []types.TransactionID) ([]explorer.Transaction, error)
TransactionChainIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error)
V2Transactions(ids []types.TransactionID) ([]explorer.V2Transaction, error)
V2TransactionChainIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error)
Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error)
SiacoinElements(ids []types.SiacoinOutputID) (result []explorer.SiacoinOutput, err error)
SiafundElements(ids []types.SiafundOutputID) (result []explorer.SiafundOutput, err error)
Expand Down Expand Up @@ -320,6 +322,60 @@ func (s *server) transactionsBatchHandler(jc jape.Context) {
jc.Encode(txns)
}

func (s *server) v2TransactionsIDHandler(jc jape.Context) {
var id types.TransactionID
if jc.DecodeParam("id", &id) != nil {
return
}
txns, err := s.e.V2Transactions([]types.TransactionID{id})
if jc.Check("failed to get transaction", err) != nil {
return
} else if len(txns) == 0 {
jc.Error(ErrTransactionNotFound, http.StatusNotFound)
return
}
jc.Encode(txns[0])
}

func (s *server) v2TransactionsIDIndicesHandler(jc jape.Context) {
var id types.TransactionID
if jc.DecodeParam("id", &id) != nil {
return
}

limit := uint64(100)
offset := uint64(0)
if jc.DecodeForm("limit", &limit) != nil || jc.DecodeForm("offset", &offset) != nil {
return
}

if limit > 500 {
limit = 500
}

indices, err := s.e.V2TransactionChainIndices(id, offset, limit)
if jc.Check("failed to get transaction indices", err) != nil {
return
}
jc.Encode(indices)
}

func (s *server) v2TransactionsBatchHandler(jc jape.Context) {
var ids []types.TransactionID
if jc.Decode(&ids) != nil {
return
} else if len(ids) > maxIDs {
jc.Error(ErrTooManyIDs, http.StatusBadRequest)
return
}

txns, err := s.e.V2Transactions(ids)
if jc.Check("failed to get transactions", err) != nil {
return
}
jc.Encode(txns)
}

func (s *server) addressessAddressUtxosSiacoinHandler(jc jape.Context) {
var address types.Address
if jc.DecodeParam("address", &address) != nil {
Expand Down Expand Up @@ -554,9 +610,13 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler {

"GET /blocks/:id": srv.blocksIDHandler,

"GET /transactions/:id": srv.transactionsIDHandler,
"POST /transactions": srv.transactionsBatchHandler,
"GET /transactions/:id/indices": srv.transactionsIDIndicesHandler,
"GET /transactions/:id": srv.transactionsIDHandler,
"POST /transactions": srv.transactionsBatchHandler,
"GET /transactions/:id/indices": srv.transactionsIDIndicesHandler,

"GET /v2/transactions/:id": srv.v2TransactionsIDHandler,
"POST /v2/transactions": srv.v2TransactionsBatchHandler,
"GET /v2/transactions/:id/indices": srv.v2TransactionsIDIndicesHandler,

"GET /addresses/:address/utxos/siacoin": srv.addressessAddressUtxosSiacoinHandler,
"GET /addresses/:address/utxos/siafund": srv.addressessAddressUtxosSiafundHandler,
Expand Down
13 changes: 12 additions & 1 deletion explorer/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
// event type constants
const (
EventTypeTransaction = "transaction"
EventTypeV2Transaction = "v2transaction"
EventTypeMinerPayout = "miner payout"
EventTypeContractPayout = "contract payout"
EventTypeSiafundClaim = "siafund claim"
Expand Down Expand Up @@ -39,6 +40,9 @@ type Event struct {
// EventType implements Event.
func (*EventTransaction) EventType() string { return EventTypeTransaction }

// EventType implements Event.
func (*EventV2Transaction) EventType() string { return EventTypeV2Transaction }

// EventType implements Event.
func (*EventMinerPayout) EventType() string { return EventTypeMinerPayout }

Expand Down Expand Up @@ -80,6 +84,13 @@ type EventTransaction struct {
Fee types.Currency `json:"fee"`
}

// An EventV2Transaction represents a v2 transaction that affects the wallet.
type EventV2Transaction struct {
Transaction V2Transaction `json:"transaction"`
HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements"`
Fee types.Currency `json:"fee"`
}

// An EventMinerPayout represents a miner payout from a block.
type EventMinerPayout struct {
SiacoinOutput types.SiacoinElement `json:"siacoinOutput"`
Expand Down Expand Up @@ -206,7 +217,7 @@ func AppliedEvents(cs consensus.State, b types.Block, cu ChainUpdate) []Event {
for _, txn := range b.V2Transactions() {
relevant := relevantV2Txn(txn)

var e EventTransaction
var e EventV2Transaction
for _, a := range txn.Attestations {
var ha chain.HostAnnouncement
if ha.FromAttestation(a) {
Expand Down
16 changes: 15 additions & 1 deletion explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ type Store interface {
HostMetrics() (HostMetrics, error)
Transactions(ids []types.TransactionID) ([]Transaction, error)
TransactionChainIndices(txid types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error)
V2Transactions(ids []types.TransactionID) ([]V2Transaction, error)
V2TransactionChainIndices(txid types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error)
UnspentSiacoinOutputs(address types.Address, offset, limit uint64) ([]SiacoinOutput, error)
UnspentSiafundOutputs(address types.Address, offset, limit uint64) ([]SiafundOutput, error)
AddressEvents(address types.Address, offset, limit uint64) (events []Event, err error)
Expand Down Expand Up @@ -211,6 +213,18 @@ func (e *Explorer) TransactionChainIndices(id types.TransactionID, offset, limit
return e.s.TransactionChainIndices(id, offset, limit)
}

// V2Transactions returns the v2 transactions with the specified IDs.
func (e *Explorer) V2Transactions(ids []types.TransactionID) ([]V2Transaction, error) {
return e.s.V2Transactions(ids)
}

// V2TransactionChainIndices returns the chain indices of the blocks the
// transaction was included in. If the transaction has not been included in
// any blocks, the result will be nil,nil.
func (e *Explorer) V2TransactionChainIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) {
return e.s.V2TransactionChainIndices(id, offset, limit)
}

// UnspentSiacoinOutputs returns the unspent siacoin outputs owned by the
// specified address.
func (e *Explorer) UnspentSiacoinOutputs(address types.Address, offset, limit uint64) ([]SiacoinOutput, error) {
Expand Down Expand Up @@ -275,7 +289,7 @@ func (e *Explorer) Search(id types.Hash256) (SearchType, error) {
}

_, err = e.Block(types.BlockID(id))
if err != nil && err != sql.ErrNoRows {
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return SearchTypeInvalid, err
} else if err == nil {
return SearchTypeBlock, nil
Expand Down
18 changes: 18 additions & 0 deletions explorer/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ type Transaction struct {
HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements,omitempty"`
}

// A V2Transaction is a v2 transaction that uses the wrapped types above.
type V2Transaction struct {
ID types.TransactionID `json:"id"`
ArbitraryData []byte `json:"arbitraryData,omitempty"`

HostAnnouncements []chain.HostAnnouncement `json:"hostAnnouncements,omitempty"`
}

// V2BlockData is a struct containing the fields from types.V2BlockData and our
// modified explorer.V2Transaction type.
type V2BlockData struct {
Height uint64 `json:"height"`
Commitment types.Hash256 `json:"commitment"`
Transactions []V2Transaction `json:"transactions"`
}

// A Block is a block containing wrapped transactions and siacoin
// outputs for the miner payouts.
type Block struct {
Expand All @@ -175,6 +191,8 @@ type Block struct {
Timestamp time.Time `json:"timestamp"`
MinerPayouts []SiacoinOutput `json:"minerPayouts"`
Transactions []Transaction `json:"transactions"`

V2 *V2BlockData `json:"v2,omitempty"`
}

// Metrics contains various statistics relevant to the health of the Sia network.
Expand Down
20 changes: 20 additions & 0 deletions internal/testutil/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ func MineBlock(state consensus.State, txns []types.Transaction, minerAddr types.
return b
}

// 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 {
b := types.Block{
ParentID: state.Index.ID,
Timestamp: types.CurrentTimestamp(),
MinerPayouts: []types.SiacoinOutput{{Address: minerAddr, Value: state.BlockReward()}},

V2: &types.V2BlockData{
Transactions: txns,
Height: state.Index.Height + 1,
},
}
b.V2.Commitment = state.Commitment(state.TransactionsCommitment(b.Transactions, b.V2Transactions()), b.MinerPayouts[0].Address)
for b.ID().CmpWork(state.ChildTarget) < 0 {
b.Nonce += state.NonceFactor()
}
return b
}

// SignTransactionWithContracts signs a transaction using the specified private
// keys, including contract revisions.
func SignTransactionWithContracts(cs consensus.State, pk, renterPK, hostPK types.PrivateKey, txn *types.Transaction) {
Expand Down
32 changes: 32 additions & 0 deletions internal/testutil/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func CheckTransaction(t *testing.T, expectTxn types.Transaction, gotTxn explorer
Equal(t, "siacoin outputs", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs))
Equal(t, "siafund inputs", len(expectTxn.SiafundInputs), len(gotTxn.SiafundInputs))
Equal(t, "siafund outputs", len(expectTxn.SiafundOutputs), len(gotTxn.SiafundOutputs))
Equal(t, "arbitrary data", len(expectTxn.ArbitraryData), len(gotTxn.ArbitraryData))
Equal(t, "miner fees", len(expectTxn.MinerFees), len(gotTxn.MinerFees))
Equal(t, "signatures", len(expectTxn.Signatures), len(gotTxn.Signatures))

for i := range expectTxn.SiacoinInputs {
expectSci := expectTxn.SiacoinInputs[i]
Expand Down Expand Up @@ -100,6 +103,35 @@ func CheckTransaction(t *testing.T, expectTxn types.Transaction, gotTxn explorer
}
}

// CheckV2Transaction checks the inputs and outputs of the retrieved transaction
// with the source transaction.
func CheckV2Transaction(t *testing.T, expectTxn types.V2Transaction, gotTxn explorer.V2Transaction) {
t.Helper()

Equal(t, "arbitrary data", len(expectTxn.ArbitraryData), len(gotTxn.ArbitraryData))

for i := range expectTxn.ArbitraryData {
Equal(t, "arbitrary data value", expectTxn.ArbitraryData[i], gotTxn.ArbitraryData[i])
}
}

// CheckV2ChainIndices checks that the chain indices that a v2 transaction was
// in from the explorer match the expected chain indices.
func CheckV2ChainIndices(t *testing.T, db explorer.Store, txnID types.TransactionID, expected []types.ChainIndex) {
t.Helper()

indices, err := db.V2TransactionChainIndices(txnID, 0, 100)
switch {
case err != nil:
t.Fatal(err)
case len(indices) != len(expected):
t.Fatalf("expected %d indices, got %d", len(expected), len(indices))
}
for i := range indices {
Equal(t, "index", expected[i], indices[i])
}
}

// CheckFC checks the retrieved file contract with the source file contract in
// addition to checking the resolved and valid fields.
func CheckFC(t *testing.T, revision, resolved, valid bool, expected types.FileContract, got explorer.FileContract) {
Expand Down
17 changes: 17 additions & 0 deletions persist/sqlite/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ func scanEvent(tx *txn, s scanner) (ev explorer.Event, eventID int64, err error)
eventTx.HostAnnouncements = append(eventTx.HostAnnouncements, announcement)
}
ev.Data = &eventTx
case explorer.EventTypeV2Transaction:
var txnID int64
var eventTx explorer.EventV2Transaction
err = tx.QueryRow(`SELECT transaction_id, fee FROM v2_transaction_events WHERE event_id = ?`, eventID).Scan(&txnID, decode(&eventTx.Fee))
if err != nil {
return explorer.Event{}, 0, fmt.Errorf("failed to fetch v2 transaction ID: %w", err)
}
txns, err := getV2Transactions(tx, map[int64]transactionID{txnID: {id: types.TransactionID(ev.ID)}})
if err != nil || len(txns) == 0 {
return explorer.Event{}, 0, fmt.Errorf("failed to fetch v2 transaction: %w", err)
}

rows, err := tx.Query(`SELECT public_key, net_address FROM v2_host_announcements WHERE transaction_id = ? ORDER BY transaction_order ASC`, txnID)
if err != nil {
return explorer.Event{}, 0, fmt.Errorf("failed to get host announcements: %w", err)
}
defer rows.Close()
case explorer.EventTypeContractPayout:
var m explorer.EventContractPayout
err = tx.QueryRow(`SELECT sce.output_id, sce.leaf_index, sce.maturity_height, sce.address, sce.value, fce.contract_id, fce.leaf_index, fce.filesize, fce.file_merkle_root, fce.window_start, fce.window_end, fce.payout, fce.unlock_hash, fce.revision_number, ev.missed
Expand Down
24 changes: 21 additions & 3 deletions persist/sqlite/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,34 @@ import (
// Block implements explorer.Store.
func (s *Store) Block(id types.BlockID) (result explorer.Block, err error) {
err = s.transaction(func(tx *txn) error {
err = tx.QueryRow(`SELECT parent_id, nonce, timestamp, height FROM blocks WHERE id=?`, encode(id)).Scan(decode(&result.ParentID), decode(&result.Nonce), decode(&result.Timestamp), &result.Height)
var v2Height uint64
var v2Commitment types.Hash256
err := tx.QueryRow(`SELECT parent_id, nonce, timestamp, height, v2_height, v2_commitment FROM blocks WHERE id = ?`, encode(id)).Scan(decode(&result.ParentID), decode(&result.Nonce), decode(&result.Timestamp), &result.Height, decodeNull(&v2Height), decodeNull(&v2Commitment))
if err != nil {
return err
return fmt.Errorf("failed to get block: %w", err)
}

result.MinerPayouts, err = blockMinerPayouts(tx, id)
if err != nil {
return fmt.Errorf("failed to get miner payouts: %w", err)
}

if (v2Height != 0 && v2Commitment != types.Hash256{}) {
result.V2 = new(explorer.V2BlockData)
result.V2.Height = v2Height
result.V2.Commitment = v2Commitment

// get block transaction IDs
transactionIDs, err := blockV2TransactionIDs(tx, id)
if err != nil {
return fmt.Errorf("failed to get block transaction IDs: %w", err)
}

result.V2.Transactions, err = getV2Transactions(tx, transactionIDs)
if err != nil {
return fmt.Errorf("failed to get transactions: %w", err)
}
}

// get block transaction IDs
transactionIDs, err := blockTransactionIDs(tx, id)
if err != nil {
Expand Down
Loading

0 comments on commit 4d34627

Please sign in to comment.