diff --git a/explorer/types.go b/explorer/types.go index 4f859320..44a99e94 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -169,6 +169,7 @@ type Transaction struct { // A V2Transaction is a v2 transaction that uses the wrapped types above. type V2Transaction struct { ID types.TransactionID `json:"id"` + Attestations []types.Attestation `json:"attestations,omitempty"` ArbitraryData []byte `json:"arbitraryData,omitempty"` NewFoundationAddress *types.Address `json:"newFoundationAddress,omitempty"` diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 446d9498..5a86a5b6 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -273,6 +273,17 @@ CREATE TABLE v2_block_transactions ( CREATE INDEX v2_block_transactions_block_id_index ON v2_block_transactions(block_id); CREATE INDEX v2_block_transactions_transaction_id_block_id ON v2_block_transactions(transaction_id, block_id); +CREATE TABLE v2_transaction_attestations ( + transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL, + transaction_order INTEGER NOT NULL, + public_key BLOB NOT NULL, + key TEXT NOT NULL, + value BLOB NOT NULL, + signature BLOB NOT NULL, + UNIQUE(transaction_id, transaction_order) +); +CREATE INDEX v2_transaction_attestations_transaction_id_index ON v2_transaction_attestations(transaction_id); + CREATE TABLE state_tree ( row INTEGER NOT NULL, column INTEGER NOT NULL, diff --git a/persist/sqlite/v2consensus.go b/persist/sqlite/v2consensus.go index 32561683..3d07b382 100644 --- a/persist/sqlite/v2consensus.go +++ b/persist/sqlite/v2consensus.go @@ -61,6 +61,21 @@ func addV2Transactions(tx *txn, bid types.BlockID, txns []types.V2Transaction) ( return txnDBIds, nil } +func addV2Attestations(tx *txn, id 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 { + return fmt.Errorf("addV2Attestations: failed to prepare statement: %w", err) + } + defer stmt.Close() + + for i, attestation := range txn.Attestations { + if _, err := stmt.Exec(id, i, encode(attestation.PublicKey), attestation.Key, attestation.Value, encode(attestation.Signature)); err != nil { + return fmt.Errorf("addV2Attestations: failed to execute statement: %w", err) + } + } + 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 { for _, txn := range txns { dbID, ok := v2TxnDBIds[txn.ID()] @@ -72,6 +87,10 @@ func addV2TransactionFields(tx *txn, txns []types.V2Transaction, scDBIds map[typ if dbID.exist { continue } + + if err := addV2Attestations(tx, dbID.id, txn); err != nil { + return fmt.Errorf("addV2TransactionFields: failed to add attestations: %w", err) + } } return nil diff --git a/persist/sqlite/v2consensus_test.go b/persist/sqlite/v2consensus_test.go index 8e848b71..1873a309 100644 --- a/persist/sqlite/v2consensus_test.go +++ b/persist/sqlite/v2consensus_test.go @@ -5,6 +5,7 @@ import ( "go.sia.tech/core/consensus" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" "go.sia.tech/explored/explorer" "go.sia.tech/explored/internal/testutil" ) @@ -175,3 +176,47 @@ func TestV2FoundationAddress(t *testing.T) { testutil.CheckV2Transaction(t, txn1, dbTxns[0]) } } + +func TestV2Attestations(t *testing.T) { + pk1 := types.GeneratePrivateKey() + pk2 := types.GeneratePrivateKey() + + _, _, cm, db := newStore(t, true, func(network *consensus.Network, genesisBlock types.Block) { + network.HardforkV2.AllowHeight = 1 + network.HardforkV2.RequireHeight = 2 + }) + cs := cm.TipState() + + ha1 := chain.HostAnnouncement{ + PublicKey: pk1.PublicKey(), + NetAddress: "127.0.0.1:4444", + } + ha2 := chain.HostAnnouncement{ + PublicKey: pk2.PublicKey(), + NetAddress: "127.0.0.1:8888", + } + + otherAttestation := types.Attestation{ + PublicKey: pk1.PublicKey(), + Key: "hello", + Value: []byte("world"), + } + otherAttestation.Signature = pk1.SignHash(cs.AttestationSigHash(otherAttestation)) + + txn1 := types.V2Transaction{ + Attestations: []types.Attestation{ha1.ToAttestation(cs, pk1), otherAttestation, ha2.ToAttestation(cs, pk2)}, + } + + if err := cm.AddBlocks([]types.Block{testutil.MineV2Block(cs, []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]) + } +} diff --git a/persist/sqlite/v2transactions.go b/persist/sqlite/v2transactions.go index 0c401571..ee42cf9e 100644 --- a/persist/sqlite/v2transactions.go +++ b/persist/sqlite/v2transactions.go @@ -4,6 +4,7 @@ import ( "fmt" "go.sia.tech/core/types" + "go.sia.tech/coreutils/chain" "go.sia.tech/explored/explorer" ) @@ -60,11 +61,22 @@ WHERE block_id = ? ORDER BY block_order ASC`, encode(blockID)) // getV2Transactions fetches v2 transactions in the correct order using // prepared statements. func getV2Transactions(tx *txn, ids []types.TransactionID) ([]explorer.V2Transaction, error) { - _, txns, err := getV2TransactionBase(tx, ids) + dbIDs, txns, err := getV2TransactionBase(tx, ids) if err != nil { return nil, fmt.Errorf("getV2Transactions: failed to get base transactions: %w", err) + } else if err := fillV2TransactionAttestations(tx, dbIDs, txns); err != nil { + return nil, fmt.Errorf("getV2Transactions: failed to get attestations: %w", err) } + // add host announcements if we have any + for i := range txns { + for _, attestation := range txns[i].Attestations { + var ha chain.HostAnnouncement + if ha.FromAttestation(attestation) { + txns[i].HostAnnouncements = append(txns[i].HostAnnouncements, ha) + } + } + } return txns, nil } @@ -96,6 +108,39 @@ func getV2TransactionBase(tx *txn, txnIDs []types.TransactionID) ([]int64, []exp return dbIDs, txns, nil } +// fillV2TransactionAttestations fills in the attestations for each +// transaction. +func fillV2TransactionAttestations(tx *txn, dbIDs []int64, txns []explorer.V2Transaction) error { + stmt, err := tx.Prepare(`SELECT public_key, key, value, signature FROM v2_transaction_attestations WHERE transaction_id = ? ORDER BY transaction_order`) + if err != nil { + return fmt.Errorf("failed to prepare attestations 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 attestations: %w", err) + } + defer rows.Close() + + for rows.Next() { + var attestation types.Attestation + if err := rows.Scan(decode(&attestation.PublicKey), &attestation.Key, &attestation.Value, decode(&attestation.Signature)); err != nil { + return fmt.Errorf("failed to scan attestation: %w", err) + } + txns[i].Attestations = append(txns[i].Attestations, attestation) + } + 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 {