diff --git a/chain/chain_test.go b/chain/chain_test.go index e60141d..f53bb98 100644 --- a/chain/chain_test.go +++ b/chain/chain_test.go @@ -129,3 +129,117 @@ func TestChain_TopBlock_Good(t *testing.T) { t.Errorf("top height: got %d, want 2", topMeta.Height) } } + +func TestChain_PutGetTransaction_Good(t *testing.T) { + c := newTestChain(t) + + tx := &types.Transaction{ + Version: 1, + Vin: []types.TxInput{ + types.TxInputToKey{ + Amount: 1000000000000, + KeyImage: types.KeyImage{0x01}, + EtcDetails: wire.EncodeVarint(0), + }, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{ + Amount: 900000000000, + Target: types.TxOutToKey{Key: types.PublicKey{0x02}}, + }, + }, + Extra: wire.EncodeVarint(0), + Attachment: wire.EncodeVarint(0), + } + meta := &TxMeta{ + KeeperBlock: 5, + GlobalOutputIndexes: []uint64{42}, + } + + txHash := types.Hash{0xde, 0xad} + if err := c.PutTransaction(txHash, tx, meta); err != nil { + t.Fatalf("PutTransaction: %v", err) + } + + if !c.HasTransaction(txHash) { + t.Error("HasTransaction: got false, want true") + } + + gotTx, gotMeta, err := c.GetTransaction(txHash) + if err != nil { + t.Fatalf("GetTransaction: %v", err) + } + if gotTx.Version != 1 { + t.Errorf("version: got %d, want 1", gotTx.Version) + } + if gotMeta.KeeperBlock != 5 { + t.Errorf("keeper_block: got %d, want 5", gotMeta.KeeperBlock) + } +} + +func TestChain_KeyImage_Good(t *testing.T) { + c := newTestChain(t) + + ki := types.KeyImage{0xaa, 0xbb} + + spent, err := c.IsSpent(ki) + if err != nil { + t.Fatalf("IsSpent: %v", err) + } + if spent { + t.Error("IsSpent: got true before marking") + } + + if err := c.MarkSpent(ki, 10); err != nil { + t.Fatalf("MarkSpent: %v", err) + } + + spent, err = c.IsSpent(ki) + if err != nil { + t.Fatalf("IsSpent: %v", err) + } + if !spent { + t.Error("IsSpent: got false after marking") + } +} + +func TestChain_OutputIndex_Good(t *testing.T) { + c := newTestChain(t) + + txID := types.Hash{0x01} + + gidx0, err := c.PutOutput(1000000000000, txID, 0) + if err != nil { + t.Fatalf("PutOutput(0): %v", err) + } + if gidx0 != 0 { + t.Errorf("gindex: got %d, want 0", gidx0) + } + + gidx1, err := c.PutOutput(1000000000000, txID, 1) + if err != nil { + t.Fatalf("PutOutput(1): %v", err) + } + if gidx1 != 1 { + t.Errorf("gindex: got %d, want 1", gidx1) + } + + count, err := c.OutputCount(1000000000000) + if err != nil { + t.Fatalf("OutputCount: %v", err) + } + if count != 2 { + t.Errorf("count: got %d, want 2", count) + } + + gotTxID, gotOutNo, err := c.GetOutput(1000000000000, 0) + if err != nil { + t.Fatalf("GetOutput: %v", err) + } + if gotTxID != txID { + t.Errorf("tx_id mismatch") + } + if gotOutNo != 0 { + t.Errorf("out_no: got %d, want 0", gotOutNo) + } +} diff --git a/chain/index.go b/chain/index.go new file mode 100644 index 0000000..848cc7f --- /dev/null +++ b/chain/index.go @@ -0,0 +1,99 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package chain + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + + store "forge.lthn.ai/core/go-store" + "forge.lthn.ai/core/go-blockchain/types" +) + +// MarkSpent records a key image as spent at the given block height. +func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error { + if err := c.store.Set(groupSpentKeys, ki.String(), strconv.FormatUint(height, 10)); err != nil { + return fmt.Errorf("chain: mark spent %s: %w", ki, err) + } + return nil +} + +// IsSpent checks whether a key image has been spent. +func (c *Chain) IsSpent(ki types.KeyImage) (bool, error) { + _, err := c.store.Get(groupSpentKeys, ki.String()) + if errors.Is(err, store.ErrNotFound) { + return false, nil + } + if err != nil { + return false, fmt.Errorf("chain: check spent %s: %w", ki, err) + } + return true, nil +} + +// outputGroup returns the go-store group for outputs of the given amount. +func outputGroup(amount uint64) string { + return groupOutputsPfx + strconv.FormatUint(amount, 10) +} + +// PutOutput appends an output to the global index for the given amount. +// Returns the assigned global index. +func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64, error) { + grp := outputGroup(amount) + count, err := c.store.Count(grp) + if err != nil { + return 0, fmt.Errorf("chain: output count: %w", err) + } + gindex := uint64(count) + + entry := outputEntry{ + TxID: txID.String(), + OutNo: outNo, + } + val, err := json.Marshal(entry) + if err != nil { + return 0, fmt.Errorf("chain: marshal output: %w", err) + } + + key := strconv.FormatUint(gindex, 10) + if err := c.store.Set(grp, key, string(val)); err != nil { + return 0, fmt.Errorf("chain: store output: %w", err) + } + return gindex, nil +} + +// GetOutput retrieves an output by amount and global index. +func (c *Chain) GetOutput(amount uint64, gindex uint64) (types.Hash, uint32, error) { + grp := outputGroup(amount) + key := strconv.FormatUint(gindex, 10) + val, err := c.store.Get(grp, key) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return types.Hash{}, 0, fmt.Errorf("chain: output %d:%d not found", amount, gindex) + } + return types.Hash{}, 0, fmt.Errorf("chain: get output: %w", err) + } + + var entry outputEntry + if err := json.Unmarshal([]byte(val), &entry); err != nil { + return types.Hash{}, 0, fmt.Errorf("chain: unmarshal output: %w", err) + } + hash, err := types.HashFromHex(entry.TxID) + if err != nil { + return types.Hash{}, 0, fmt.Errorf("chain: parse output tx_id: %w", err) + } + return hash, entry.OutNo, nil +} + +// OutputCount returns the number of outputs indexed for the given amount. +func (c *Chain) OutputCount(amount uint64) (uint64, error) { + n, err := c.store.Count(outputGroup(amount)) + if err != nil { + return 0, fmt.Errorf("chain: output count: %w", err) + } + return uint64(n), nil +} diff --git a/chain/store.go b/chain/store.go index 2a08ed8..fc43219 100644 --- a/chain/store.go +++ b/chain/store.go @@ -97,6 +97,68 @@ func (c *Chain) GetBlockByHash(hash types.Hash) (*types.Block, *BlockMeta, error return c.GetBlockByHeight(height) } +// txRecord is the JSON value stored in the transactions group. +type txRecord struct { + Meta TxMeta `json:"meta"` + Blob string `json:"blob"` // hex-encoded wire format +} + +// PutTransaction stores a transaction with metadata. +func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxMeta) error { + var buf bytes.Buffer + enc := wire.NewEncoder(&buf) + wire.EncodeTransaction(enc, tx) + if err := enc.Err(); err != nil { + return fmt.Errorf("chain: encode tx %s: %w", hash, err) + } + + rec := txRecord{ + Meta: *meta, + Blob: hex.EncodeToString(buf.Bytes()), + } + val, err := json.Marshal(rec) + if err != nil { + return fmt.Errorf("chain: marshal tx %s: %w", hash, err) + } + + if err := c.store.Set(groupTx, hash.String(), string(val)); err != nil { + return fmt.Errorf("chain: store tx %s: %w", hash, err) + } + return nil +} + +// GetTransaction retrieves a transaction by hash. +func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, error) { + val, err := c.store.Get(groupTx, hash.String()) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, nil, fmt.Errorf("chain: tx %s not found", hash) + } + return nil, nil, fmt.Errorf("chain: get tx %s: %w", hash, err) + } + + var rec txRecord + if err := json.Unmarshal([]byte(val), &rec); err != nil { + return nil, nil, fmt.Errorf("chain: unmarshal tx: %w", err) + } + blob, err := hex.DecodeString(rec.Blob) + if err != nil { + return nil, nil, fmt.Errorf("chain: decode tx hex: %w", err) + } + dec := wire.NewDecoder(bytes.NewReader(blob)) + tx := wire.DecodeTransaction(dec) + if err := dec.Err(); err != nil { + return nil, nil, fmt.Errorf("chain: decode tx wire: %w", err) + } + return &tx, &rec.Meta, nil +} + +// HasTransaction checks whether a transaction exists in the store. +func (c *Chain) HasTransaction(hash types.Hash) bool { + _, err := c.store.Get(groupTx, hash.String()) + return err == nil +} + func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) { var rec blockRecord if err := json.Unmarshal([]byte(val), &rec); err != nil {