From 4bac1f6c046b309a262162d53c43e7feb8d94c23 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 20:28:54 +0000 Subject: [PATCH] feat(chain): add GetRingOutputs callback for signature verification Implements consensus.RingOutputsFn by looking up output public keys from the chain's global output index and transaction store. Wired into the sync loop so VerifySignatures=true uses real crypto verification. Co-Authored-By: Charon --- chain/ring.go | 43 +++++++++++ chain/ring_test.go | 178 +++++++++++++++++++++++++++++++++++++++++++++ chain/sync.go | 4 +- 3 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 chain/ring.go create mode 100644 chain/ring_test.go diff --git a/chain/ring.go b/chain/ring.go new file mode 100644 index 0000000..13330ae --- /dev/null +++ b/chain/ring.go @@ -0,0 +1,43 @@ +// 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 ( + "fmt" + + "forge.lthn.ai/core/go-blockchain/types" +) + +// GetRingOutputs fetches the public keys for the given global output indices +// at the specified amount. This implements the consensus.RingOutputsFn +// signature for use during signature verification. +func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicKey, error) { + pubs := make([]types.PublicKey, len(offsets)) + for i, gidx := range offsets { + txHash, outNo, err := c.GetOutput(amount, gidx) + if err != nil { + return nil, fmt.Errorf("ring output %d (amount=%d, gidx=%d): %w", i, amount, gidx, err) + } + + tx, _, err := c.GetTransaction(txHash) + if err != nil { + return nil, fmt.Errorf("ring output %d: tx %s: %w", i, txHash, err) + } + + if int(outNo) >= len(tx.Vout) { + return nil, fmt.Errorf("ring output %d: tx %s has %d outputs, want index %d", + i, txHash, len(tx.Vout), outNo) + } + + switch out := tx.Vout[outNo].(type) { + case types.TxOutputBare: + pubs[i] = out.Target.Key + default: + return nil, fmt.Errorf("ring output %d: unsupported output type %T", i, out) + } + } + return pubs, nil +} diff --git a/chain/ring_test.go b/chain/ring_test.go new file mode 100644 index 0000000..e9238a6 --- /dev/null +++ b/chain/ring_test.go @@ -0,0 +1,178 @@ +// 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 ( + "testing" + + "forge.lthn.ai/core/go-blockchain/types" + "forge.lthn.ai/core/go-blockchain/wire" +) + +func TestGetRingOutputs_Good(t *testing.T) { + c := newTestChain(t) + + pubKey := types.PublicKey{1, 2, 3} + tx := types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{types.TxInputGenesis{Height: 0}}, + Vout: []types.TxOutput{ + types.TxOutputBare{ + Amount: 1000, + Target: types.TxOutToKey{Key: pubKey, MixAttr: 0}, + }, + }, + Extra: wire.EncodeVarint(0), + Attachment: wire.EncodeVarint(0), + } + txHash := wire.TransactionHash(&tx) + + err := c.PutTransaction(txHash, &tx, &TxMeta{KeeperBlock: 0, GlobalOutputIndexes: []uint64{0}}) + if err != nil { + t.Fatalf("PutTransaction: %v", err) + } + + gidx, err := c.PutOutput(1000, txHash, 0) + if err != nil { + t.Fatalf("PutOutput: %v", err) + } + if gidx != 0 { + t.Fatalf("gidx: got %d, want 0", gidx) + } + + pubs, err := c.GetRingOutputs(1000, []uint64{0}) + if err != nil { + t.Fatalf("GetRingOutputs: %v", err) + } + if len(pubs) != 1 { + t.Fatalf("pubs length: got %d, want 1", len(pubs)) + } + if pubs[0] != pubKey { + t.Errorf("pubs[0]: got %x, want %x", pubs[0], pubKey) + } +} + +func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) { + c := newTestChain(t) + + key1 := types.PublicKey{0x11} + key2 := types.PublicKey{0x22} + + tx1 := types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{types.TxInputGenesis{Height: 0}}, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: 500, Target: types.TxOutToKey{Key: key1}}, + }, + Extra: wire.EncodeVarint(0), + Attachment: wire.EncodeVarint(0), + } + tx1Hash := wire.TransactionHash(&tx1) + + tx2 := types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{types.TxInputGenesis{Height: 1}}, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: 500, Target: types.TxOutToKey{Key: key2}}, + }, + Extra: wire.EncodeVarint(0), + Attachment: wire.EncodeVarint(0), + } + tx2Hash := wire.TransactionHash(&tx2) + + if err := c.PutTransaction(tx1Hash, &tx1, &TxMeta{KeeperBlock: 0, GlobalOutputIndexes: []uint64{0}}); err != nil { + t.Fatalf("PutTransaction(tx1): %v", err) + } + if err := c.PutTransaction(tx2Hash, &tx2, &TxMeta{KeeperBlock: 1, GlobalOutputIndexes: []uint64{1}}); err != nil { + t.Fatalf("PutTransaction(tx2): %v", err) + } + + if _, err := c.PutOutput(500, tx1Hash, 0); err != nil { + t.Fatalf("PutOutput(tx1): %v", err) + } + if _, err := c.PutOutput(500, tx2Hash, 0); err != nil { + t.Fatalf("PutOutput(tx2): %v", err) + } + + pubs, err := c.GetRingOutputs(500, []uint64{0, 1}) + if err != nil { + t.Fatalf("GetRingOutputs: %v", err) + } + if len(pubs) != 2 { + t.Fatalf("pubs length: got %d, want 2", len(pubs)) + } + if pubs[0] != key1 { + t.Errorf("pubs[0]: got %x, want %x", pubs[0], key1) + } + if pubs[1] != key2 { + t.Errorf("pubs[1]: got %x, want %x", pubs[1], key2) + } +} + +func TestGetRingOutputs_Bad_OutputNotFound(t *testing.T) { + c := newTestChain(t) + + _, err := c.GetRingOutputs(1000, []uint64{99}) + if err == nil { + t.Fatal("GetRingOutputs: expected error for missing output, got nil") + } +} + +func TestGetRingOutputs_Bad_TxNotFound(t *testing.T) { + c := newTestChain(t) + + // Index an output pointing to a transaction that does not exist in the store. + fakeTxHash := types.Hash{0xde, 0xad} + if _, err := c.PutOutput(1000, fakeTxHash, 0); err != nil { + t.Fatalf("PutOutput: %v", err) + } + + _, err := c.GetRingOutputs(1000, []uint64{0}) + if err == nil { + t.Fatal("GetRingOutputs: expected error for missing tx, got nil") + } +} + +func TestGetRingOutputs_Bad_OutputIndexOutOfRange(t *testing.T) { + c := newTestChain(t) + + tx := types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{types.TxInputGenesis{Height: 0}}, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: 1000, Target: types.TxOutToKey{Key: types.PublicKey{0x01}}}, + }, + Extra: wire.EncodeVarint(0), + Attachment: wire.EncodeVarint(0), + } + txHash := wire.TransactionHash(&tx) + + if err := c.PutTransaction(txHash, &tx, &TxMeta{KeeperBlock: 0, GlobalOutputIndexes: []uint64{0}}); err != nil { + t.Fatalf("PutTransaction: %v", err) + } + + // Index with outNo=5, which is beyond the transaction's single output. + if _, err := c.PutOutput(1000, txHash, 5); err != nil { + t.Fatalf("PutOutput: %v", err) + } + + _, err := c.GetRingOutputs(1000, []uint64{0}) + if err == nil { + t.Fatal("GetRingOutputs: expected error for out-of-range index, got nil") + } +} + +func TestGetRingOutputs_Good_EmptyOffsets(t *testing.T) { + c := newTestChain(t) + + pubs, err := c.GetRingOutputs(1000, []uint64{}) + if err != nil { + t.Fatalf("GetRingOutputs: %v", err) + } + if len(pubs) != 0 { + t.Errorf("pubs length: got %d, want 0", len(pubs)) + } +} diff --git a/chain/sync.go b/chain/sync.go index 4307f9d..390e596 100644 --- a/chain/sync.go +++ b/chain/sync.go @@ -167,9 +167,9 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error { return fmt.Errorf("validate tx %s: %w", txInfo.ID, err) } - // Optionally verify signatures (structural check only — nil getRingOutputs). + // Optionally verify signatures using the chain's output index. if opts.VerifySignatures { - if err := consensus.VerifyTransactionSignatures(&tx, opts.Forks, bd.Height, nil); err != nil { + if err := consensus.VerifyTransactionSignatures(&tx, opts.Forks, bd.Height, c.GetRingOutputs); err != nil { return fmt.Errorf("verify tx signatures %s: %w", txInfo.ID, err) } }