feat(chain): transaction, key image, and output index operations
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
d5976d33c3
commit
89f5f0ebdf
3 changed files with 275 additions and 0 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
99
chain/index.go
Normal file
99
chain/index.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue