Compare commits

..

No commits in common. "dev" and "main" have entirely different histories.
dev ... main

61 changed files with 610 additions and 3500 deletions

View file

@ -19,16 +19,11 @@ type Chain struct {
}
// New creates a Chain backed by the given store.
//
// s, _ := store.New("~/.lethean/chain/chain.db")
// blockchain := chain.New(s)
func New(s *store.Store) *Chain {
return &Chain{store: s}
}
// Height returns the number of stored blocks (0 if empty).
//
// h, err := blockchain.Height()
func (c *Chain) Height() (uint64, error) {
n, err := c.store.Count(groupBlocks)
if err != nil {
@ -39,8 +34,6 @@ func (c *Chain) Height() (uint64, error) {
// TopBlock returns the highest stored block and its metadata.
// Returns an error if the chain is empty.
//
// blk, meta, err := blockchain.TopBlock()
func (c *Chain) TopBlock() (*types.Block, *BlockMeta, error) {
h, err := c.Height()
if err != nil {

View file

@ -76,16 +76,12 @@ func (c *Chain) nextDifficultyWith(height uint64, forks []config.HardFork, baseT
// NextDifficulty computes the expected PoW difficulty for the block at the
// given height. Pre-HF6 the target is 120s; post-HF6 it doubles to 240s.
//
// diff, err := blockchain.NextDifficulty(nextHeight, config.MainnetForks)
func (c *Chain) NextDifficulty(height uint64, forks []config.HardFork) (uint64, error) {
return c.nextDifficultyWith(height, forks, config.DifficultyPowTarget, config.DifficultyPowTargetHF6)
}
// NextPoSDifficulty computes the expected PoS difficulty for the block at the
// given height. Pre-HF6 the target is 120s; post-HF6 it doubles to 240s.
//
// diff, err := blockchain.NextPoSDifficulty(nextHeight, config.MainnetForks)
func (c *Chain) NextPoSDifficulty(height uint64, forks []config.HardFork) (uint64, error) {
return c.nextDifficultyWith(height, forks, config.DifficultyPosTarget, config.DifficultyPosTargetHF6)
}

View file

@ -18,8 +18,6 @@ import (
)
// MarkSpent records a key image as spent at the given block height.
//
// err := blockchain.MarkSpent(input.KeyImage, blockHeight)
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 coreerr.E("Chain.MarkSpent", fmt.Sprintf("chain: mark spent %s", ki), err)
@ -28,8 +26,6 @@ func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error {
}
// IsSpent checks whether a key image has been spent.
//
// spent, err := blockchain.IsSpent(keyImage)
func (c *Chain) IsSpent(ki types.KeyImage) (bool, error) {
_, err := c.store.Get(groupSpentKeys, ki.String())
if errors.Is(err, store.ErrNotFound) {
@ -96,8 +92,6 @@ func (c *Chain) GetOutput(amount uint64, gindex uint64) (types.Hash, uint32, err
}
// OutputCount returns the number of outputs indexed for the given amount.
//
// count, err := blockchain.OutputCount(1_000_000_000_000)
func (c *Chain) OutputCount(amount uint64) (uint64, error) {
n, err := c.store.Count(outputGroup(amount))
if err != nil {

View file

@ -6,7 +6,9 @@
package chain
import (
corelog "dappco.re/go/core/log"
"log"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/p2p"
levinpkg "dappco.re/go/core/p2p/node/levin"
@ -26,10 +28,6 @@ func NewLevinP2PConn(conn *levinpkg.Connection, peerHeight uint64, localSync p2p
return &LevinP2PConn{conn: conn, peerHeight: peerHeight, localSync: localSync}
}
// PeerHeight returns the remote peer's advertised chain height,
// obtained during the Levin handshake.
//
// height := conn.PeerHeight()
func (c *LevinP2PConn) PeerHeight() uint64 { return c.peerHeight }
// handleMessage processes non-target messages received while waiting for
@ -42,44 +40,40 @@ func (c *LevinP2PConn) handleMessage(hdr levinpkg.Header, data []byte) error {
resp := p2p.TimedSyncRequest{PayloadData: c.localSync}
payload, err := resp.Encode()
if err != nil {
return corelog.E("LevinP2PConn.handleMessage", "encode timed_sync response", err)
return coreerr.E("LevinP2PConn.handleMessage", "encode timed_sync response", err)
}
if err := c.conn.WriteResponse(p2p.CommandTimedSync, payload, levinpkg.ReturnOK); err != nil {
return corelog.E("LevinP2PConn.handleMessage", "write timed_sync response", err)
return coreerr.E("LevinP2PConn.handleMessage", "write timed_sync response", err)
}
corelog.Info("p2p responded to timed_sync")
log.Printf("p2p: responded to timed_sync")
return nil
}
// Silently skip other messages (new_block notifications, etc.)
return nil
}
// RequestChain sends NOTIFY_REQUEST_CHAIN with our sparse block ID list
// and returns the start height and block IDs from the peer's response.
//
// startHeight, blockIDs, err := conn.RequestChain(historyBytes)
func (c *LevinP2PConn) RequestChain(blockIDs [][]byte) (uint64, [][]byte, error) {
req := p2p.RequestChain{BlockIDs: blockIDs}
payload, err := req.Encode()
if err != nil {
return 0, nil, corelog.E("LevinP2PConn.RequestChain", "encode request_chain", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "encode request_chain", err)
}
// Send as notification (expectResponse=false) per CryptoNote protocol.
if err := c.conn.WritePacket(p2p.CommandRequestChain, payload, false); err != nil {
return 0, nil, corelog.E("LevinP2PConn.RequestChain", "write request_chain", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "write request_chain", err)
}
// Read until we get RESPONSE_CHAIN_ENTRY.
for {
hdr, data, err := c.conn.ReadPacket()
if err != nil {
return 0, nil, corelog.E("LevinP2PConn.RequestChain", "read response_chain", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "read response_chain", err)
}
if hdr.Command == p2p.CommandResponseChain {
var resp p2p.ResponseChainEntry
if err := resp.Decode(data); err != nil {
return 0, nil, corelog.E("LevinP2PConn.RequestChain", "decode response_chain", err)
return 0, nil, coreerr.E("LevinP2PConn.RequestChain", "decode response_chain", err)
}
return resp.StartHeight, resp.BlockIDs, nil
}
@ -89,31 +83,27 @@ func (c *LevinP2PConn) RequestChain(blockIDs [][]byte) (uint64, [][]byte, error)
}
}
// RequestObjects sends NOTIFY_REQUEST_GET_OBJECTS for the given block
// hashes and returns the raw block and transaction blobs.
//
// entries, err := conn.RequestObjects(batchHashes)
func (c *LevinP2PConn) RequestObjects(blockHashes [][]byte) ([]BlockBlobEntry, error) {
req := p2p.RequestGetObjects{Blocks: blockHashes}
payload, err := req.Encode()
if err != nil {
return nil, corelog.E("LevinP2PConn.RequestObjects", "encode request_get_objects", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "encode request_get_objects", err)
}
if err := c.conn.WritePacket(p2p.CommandRequestObjects, payload, false); err != nil {
return nil, corelog.E("LevinP2PConn.RequestObjects", "write request_get_objects", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "write request_get_objects", err)
}
// Read until we get RESPONSE_GET_OBJECTS.
for {
hdr, data, err := c.conn.ReadPacket()
if err != nil {
return nil, corelog.E("LevinP2PConn.RequestObjects", "read response_get_objects", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "read response_get_objects", err)
}
if hdr.Command == p2p.CommandResponseObjects {
var resp p2p.ResponseGetObjects
if err := resp.Decode(data); err != nil {
return nil, corelog.E("LevinP2PConn.RequestObjects", "decode response_get_objects", err)
return nil, coreerr.E("LevinP2PConn.RequestObjects", "decode response_get_objects", err)
}
entries := make([]BlockBlobEntry, len(resp.Blocks))
for i, b := range resp.Blocks {

View file

@ -8,8 +8,9 @@ package chain
import (
"context"
"fmt"
"log"
corelog "dappco.re/go/core/log"
coreerr "dappco.re/go/core/log"
)
// P2PConnection abstracts the P2P communication needed for block sync.
@ -45,7 +46,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
localHeight, err := c.Height()
if err != nil {
return corelog.E("Chain.P2PSync", "p2p sync: get height", err)
return coreerr.E("Chain.P2PSync", "p2p sync: get height", err)
}
peerHeight := conn.PeerHeight()
@ -56,7 +57,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
// Build sparse chain history.
history, err := c.SparseChainHistory()
if err != nil {
return corelog.E("Chain.P2PSync", "p2p sync: build history", err)
return coreerr.E("Chain.P2PSync", "p2p sync: build history", err)
}
// Convert Hash to []byte for P2P.
@ -70,14 +71,14 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
// Request chain entry.
startHeight, blockIDs, err := conn.RequestChain(historyBytes)
if err != nil {
return corelog.E("Chain.P2PSync", "p2p sync: request chain", err)
return coreerr.E("Chain.P2PSync", "p2p sync: request chain", err)
}
if len(blockIDs) == 0 {
return nil // nothing to sync
}
corelog.Info("p2p sync chain entry", "start_height", startHeight, "block_ids", len(blockIDs))
log.Printf("p2p sync: chain entry from height %d, %d block IDs", startHeight, len(blockIDs))
// The daemon returns the fork-point block as the first entry.
// Skip blocks we already have.
@ -107,24 +108,24 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
entries, err := conn.RequestObjects(batch)
if err != nil {
return corelog.E("Chain.P2PSync", "p2p sync: request objects", err)
return coreerr.E("Chain.P2PSync", "p2p sync: request objects", err)
}
currentHeight := fetchStart + uint64(i)
for j, entry := range entries {
blockHeight := currentHeight + uint64(j)
if blockHeight > 0 && blockHeight%100 == 0 {
corelog.Info("p2p sync processing block", "height", blockHeight)
log.Printf("p2p sync: processing block %d", blockHeight)
}
blockDiff, err := c.NextDifficulty(blockHeight, opts.Forks)
if err != nil {
return corelog.E("Chain.P2PSync", fmt.Sprintf("p2p sync: compute difficulty for block %d", blockHeight), err)
return coreerr.E("Chain.P2PSync", fmt.Sprintf("p2p sync: compute difficulty for block %d", blockHeight), err)
}
if err := c.processBlockBlobs(entry.Block, entry.Txs,
blockHeight, blockDiff, opts); err != nil {
return corelog.E("Chain.P2PSync", fmt.Sprintf("p2p sync: process block %d", blockHeight), err)
return coreerr.E("Chain.P2PSync", fmt.Sprintf("p2p sync: process block %d", blockHeight), err)
}
}
}

View file

@ -15,12 +15,10 @@ import (
)
// GetRingOutputs fetches the public keys for the given global output indices
// at the specified spending height and amount. This implements the
// consensus.RingOutputsFn signature for use during signature verification.
//
// keys, err := blockchain.GetRingOutputs(blockHeight, inputAmount, []uint64{0, 5, 12, 30})
func (c *Chain) GetRingOutputs(height, amount uint64, offsets []uint64) ([]types.PublicKey, error) {
publicKeys := make([]types.PublicKey, len(offsets))
// 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 {
@ -38,44 +36,16 @@ func (c *Chain) GetRingOutputs(height, amount uint64, offsets []uint64) ([]types
switch out := tx.Vout[outNo].(type) {
case types.TxOutputBare:
spendKey, err := ringOutputSpendKey(height, out.Target)
if err != nil {
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: %v", i, err), nil)
toKey, ok := out.Target.(types.TxOutToKey)
if !ok {
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: unsupported target type %T", i, out.Target), nil)
}
publicKeys[i] = spendKey
pubs[i] = toKey.Key
default:
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: unsupported output type %T", i, out), nil)
}
}
return publicKeys, nil
}
// ringOutputSpendKey extracts the spend key for a transparent output target.
//
// TxOutMultisig does not carry enough context here to select the exact spend
// path, so we return the first listed key as a deterministic fallback.
// TxOutHTLC selects redeem vs refund based on whether the spending height is
// before or after the contract expiration. The refund path only opens after
// the expiration height has passed.
func ringOutputSpendKey(height uint64, target types.TxOutTarget) (types.PublicKey, error) {
if key, ok := (types.TxOutputBare{Target: target}).SpendKey(); ok {
return key, nil
}
switch t := target.(type) {
case types.TxOutMultisig:
if len(t.Keys) == 0 {
return types.PublicKey{}, coreerr.E("ringOutputSpendKey", "multisig target has no keys", nil)
}
return t.Keys[0], nil
case types.TxOutHTLC:
if height > t.Expiration {
return t.PKRefund, nil
}
return t.PKRedeem, nil
default:
return types.PublicKey{}, coreerr.E("ringOutputSpendKey", fmt.Sprintf("unsupported target type %T", target), nil)
}
return pubs, nil
}
// GetZCRingOutputs fetches ZC ring members (stealth address, amount commitment,
@ -83,8 +53,6 @@ func ringOutputSpendKey(height uint64, target types.TxOutTarget) (types.PublicKe
// consensus.ZCRingOutputsFn signature for post-HF4 CLSAG GGX verification.
//
// ZC outputs are indexed at amount=0 (confidential amounts).
//
// members, err := blockchain.GetZCRingOutputs([]uint64{100, 200, 300})
func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, error) {
members := make([]consensus.ZCRingMember, len(offsets))
for i, gidx := range offsets {

View file

@ -43,7 +43,7 @@ func TestGetRingOutputs_Good(t *testing.T) {
t.Fatalf("gidx: got %d, want 0", gidx)
}
pubs, err := c.GetRingOutputs(100, 1000, []uint64{0})
pubs, err := c.GetRingOutputs(1000, []uint64{0})
if err != nil {
t.Fatalf("GetRingOutputs: %v", err)
}
@ -55,134 +55,6 @@ func TestGetRingOutputs_Good(t *testing.T) {
}
}
func TestGetRingOutputs_Good_Multisig(t *testing.T) {
c := newTestChain(t)
first := types.PublicKey{0x11, 0x22, 0x33}
second := types.PublicKey{0x44, 0x55, 0x66}
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
Vout: []types.TxOutput{
types.TxOutputBare{
Amount: 1000,
Target: types.TxOutMultisig{
MinimumSigs: 2,
Keys: []types.PublicKey{first, second},
},
},
},
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)
}
if _, err := c.PutOutput(1000, txHash, 0); err != nil {
t.Fatalf("PutOutput: %v", err)
}
pubs, err := c.GetRingOutputs(100, 1000, []uint64{0})
if err != nil {
t.Fatalf("GetRingOutputs: %v", err)
}
if pubs[0] != first {
t.Errorf("pubs[0]: got %x, want %x", pubs[0], first)
}
}
func TestGetRingOutputs_Good_HTLC(t *testing.T) {
c := newTestChain(t)
redeem := types.PublicKey{0xAA, 0xBB, 0xCC}
refund := types.PublicKey{0xDD, 0xEE, 0xFF}
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
Vout: []types.TxOutput{
types.TxOutputBare{
Amount: 1000,
Target: types.TxOutHTLC{
HTLCHash: types.Hash{0x01},
Flags: 0,
Expiration: 200,
PKRedeem: redeem,
PKRefund: refund,
},
},
},
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)
}
if _, err := c.PutOutput(1000, txHash, 0); err != nil {
t.Fatalf("PutOutput: %v", err)
}
pubs, err := c.GetRingOutputs(100, 1000, []uint64{0})
if err != nil {
t.Fatalf("GetRingOutputs: %v", err)
}
if pubs[0] != redeem {
t.Errorf("pubs[0]: got %x, want %x", pubs[0], redeem)
}
pubs, err = c.GetRingOutputs(250, 1000, []uint64{0})
if err != nil {
t.Fatalf("GetRingOutputs refund path: %v", err)
}
if pubs[0] != refund {
t.Errorf("pubs[0] refund path: got %x, want %x", pubs[0], refund)
}
}
func TestGetRingOutputs_Good_HTLCExpirationBoundary(t *testing.T) {
c := newTestChain(t)
redeem := types.PublicKey{0xAA, 0xBB, 0xCC}
refund := types.PublicKey{0xDD, 0xEE, 0xFF}
tx := types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
Vout: []types.TxOutput{
types.TxOutputBare{
Amount: 1000,
Target: types.TxOutHTLC{
HTLCHash: types.Hash{0x01},
Flags: 0,
Expiration: 200,
PKRedeem: redeem,
PKRefund: refund,
},
},
},
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)
}
if _, err := c.PutOutput(1000, txHash, 0); err != nil {
t.Fatalf("PutOutput: %v", err)
}
pubs, err := c.GetRingOutputs(200, 1000, []uint64{0})
if err != nil {
t.Fatalf("GetRingOutputs boundary path: %v", err)
}
if pubs[0] != redeem {
t.Errorf("pubs[0] boundary path: got %x, want %x", pubs[0], redeem)
}
}
func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
c := newTestChain(t)
@ -225,7 +97,7 @@ func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
t.Fatalf("PutOutput(tx2): %v", err)
}
pubs, err := c.GetRingOutputs(500, 500, []uint64{0, 1})
pubs, err := c.GetRingOutputs(500, []uint64{0, 1})
if err != nil {
t.Fatalf("GetRingOutputs: %v", err)
}
@ -243,7 +115,7 @@ func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
func TestGetRingOutputs_Bad_OutputNotFound(t *testing.T) {
c := newTestChain(t)
_, err := c.GetRingOutputs(1000, 1000, []uint64{99})
_, err := c.GetRingOutputs(1000, []uint64{99})
if err == nil {
t.Fatal("GetRingOutputs: expected error for missing output, got nil")
}
@ -258,7 +130,7 @@ func TestGetRingOutputs_Bad_TxNotFound(t *testing.T) {
t.Fatalf("PutOutput: %v", err)
}
_, err := c.GetRingOutputs(1000, 1000, []uint64{0})
_, err := c.GetRingOutputs(1000, []uint64{0})
if err == nil {
t.Fatal("GetRingOutputs: expected error for missing tx, got nil")
}
@ -287,7 +159,7 @@ func TestGetRingOutputs_Bad_OutputIndexOutOfRange(t *testing.T) {
t.Fatalf("PutOutput: %v", err)
}
_, err := c.GetRingOutputs(1000, 1000, []uint64{0})
_, err := c.GetRingOutputs(1000, []uint64{0})
if err == nil {
t.Fatal("GetRingOutputs: expected error for out-of-range index, got nil")
}
@ -296,7 +168,7 @@ func TestGetRingOutputs_Bad_OutputIndexOutOfRange(t *testing.T) {
func TestGetRingOutputs_Good_EmptyOffsets(t *testing.T) {
c := newTestChain(t)
pubs, err := c.GetRingOutputs(1000, 1000, []uint64{})
pubs, err := c.GetRingOutputs(1000, []uint64{})
if err != nil {
t.Fatalf("GetRingOutputs: %v", err)
}

View file

@ -41,8 +41,6 @@ type blockRecord struct {
}
// PutBlock stores a block and updates the block_index.
//
// err := blockchain.PutBlock(&blk, &chain.BlockMeta{Hash: blockHash, Height: 100})
func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
var buf bytes.Buffer
enc := wire.NewEncoder(&buf)
@ -74,8 +72,6 @@ func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
}
// GetBlockByHeight retrieves a block by its height.
//
// blk, meta, err := blockchain.GetBlockByHeight(1000)
func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error) {
val, err := c.store.Get(groupBlocks, heightKey(height))
if err != nil {
@ -88,8 +84,6 @@ func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error
}
// GetBlockByHash retrieves a block by its hash.
//
// blk, meta, err := blockchain.GetBlockByHash(blockHash)
func (c *Chain) GetBlockByHash(hash types.Hash) (*types.Block, *BlockMeta, error) {
heightStr, err := c.store.Get(groupBlockIndex, hash.String())
if err != nil {
@ -112,8 +106,6 @@ type txRecord struct {
}
// PutTransaction stores a transaction with metadata.
//
// err := blockchain.PutTransaction(txHash, &tx, &chain.TxMeta{KeeperBlock: height})
func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxMeta) error {
var buf bytes.Buffer
enc := wire.NewEncoder(&buf)
@ -138,8 +130,6 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM
}
// GetTransaction retrieves a transaction by hash.
//
// tx, meta, err := blockchain.GetTransaction(txHash)
func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, error) {
val, err := c.store.Get(groupTx, hash.String())
if err != nil {
@ -166,8 +156,6 @@ func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, er
}
// HasTransaction checks whether a transaction exists in the store.
//
// if blockchain.HasTransaction(txHash) { /* already stored */ }
func (c *Chain) HasTransaction(hash types.Hash) bool {
_, err := c.store.Get(groupTx, hash.String())
return err == nil

View file

@ -11,10 +11,11 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"regexp"
"strconv"
corelog "dappco.re/go/core/log"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/consensus"
@ -51,12 +52,12 @@ func DefaultSyncOptions() SyncOptions {
func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions) error {
localHeight, err := c.Height()
if err != nil {
return corelog.E("Chain.Sync", "sync: get local height", err)
return coreerr.E("Chain.Sync", "sync: get local height", err)
}
remoteHeight, err := client.GetHeight()
if err != nil {
return corelog.E("Chain.Sync", "sync: get remote height", err)
return coreerr.E("Chain.Sync", "sync: get remote height", err)
}
for localHeight < remoteHeight {
@ -71,22 +72,22 @@ func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions)
blocks, err := client.GetBlocksDetails(localHeight, batch)
if err != nil {
return corelog.E("Chain.Sync", fmt.Sprintf("sync: fetch blocks at %d", localHeight), err)
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: fetch blocks at %d", localHeight), err)
}
if err := resolveBlockBlobs(blocks, client); err != nil {
return corelog.E("Chain.Sync", fmt.Sprintf("sync: resolve blobs at %d", localHeight), err)
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: resolve blobs at %d", localHeight), err)
}
for _, bd := range blocks {
if err := c.processBlock(bd, opts); err != nil {
return corelog.E("Chain.Sync", fmt.Sprintf("sync: process block %d", bd.Height), err)
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: process block %d", bd.Height), err)
}
}
localHeight, err = c.Height()
if err != nil {
return corelog.E("Chain.Sync", "sync: get height after batch", err)
return coreerr.E("Chain.Sync", "sync: get height after batch", err)
}
}
@ -95,12 +96,12 @@ func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions)
func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
if bd.Height > 0 && bd.Height%100 == 0 {
corelog.Info("sync processing block", "height", bd.Height)
log.Printf("sync: processing block %d", bd.Height)
}
blockBlob, err := hex.DecodeString(bd.Blob)
if err != nil {
return corelog.E("Chain.processBlock", "decode block hex", err)
return coreerr.E("Chain.processBlock", "decode block hex", err)
}
// Build a set of the block's regular tx hashes for lookup.
@ -110,7 +111,7 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
dec := wire.NewDecoder(bytes.NewReader(blockBlob))
blk := wire.DecodeBlock(dec)
if err := dec.Err(); err != nil {
return corelog.E("Chain.processBlock", "decode block for tx hashes", err)
return coreerr.E("Chain.processBlock", "decode block for tx hashes", err)
}
regularTxs := make(map[string]struct{}, len(blk.TxHashes))
@ -125,7 +126,7 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
}
txBlobBytes, err := hex.DecodeString(txInfo.Blob)
if err != nil {
return corelog.E("Chain.processBlock", fmt.Sprintf("decode tx hex %s", txInfo.ID), err)
return coreerr.E("Chain.processBlock", fmt.Sprintf("decode tx hex %s", txInfo.ID), err)
}
txBlobs = append(txBlobs, txBlobBytes)
}
@ -136,10 +137,10 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
computedHash := wire.BlockHash(&blk)
daemonHash, err := types.HashFromHex(bd.ID)
if err != nil {
return corelog.E("Chain.processBlock", "parse daemon block hash", err)
return coreerr.E("Chain.processBlock", "parse daemon block hash", err)
}
if computedHash != daemonHash {
return corelog.E("Chain.processBlock", fmt.Sprintf("block hash mismatch: computed %s, daemon says %s", computedHash, daemonHash), nil)
return coreerr.E("Chain.processBlock", fmt.Sprintf("block hash mismatch: computed %s, daemon says %s", computedHash, daemonHash), nil)
}
return c.processBlockBlobs(blockBlob, txBlobs, bd.Height, diff, opts)
@ -154,7 +155,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
dec := wire.NewDecoder(bytes.NewReader(blockBlob))
blk := wire.DecodeBlock(dec)
if err := dec.Err(); err != nil {
return corelog.E("Chain.processBlockBlobs", "decode block wire", err)
return coreerr.E("Chain.processBlockBlobs", "decode block wire", err)
}
// Compute the block hash.
@ -164,21 +165,21 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
if height == 0 {
genesisHash, err := types.HashFromHex(GenesisHash)
if err != nil {
return corelog.E("Chain.processBlockBlobs", "parse genesis hash", err)
return coreerr.E("Chain.processBlockBlobs", "parse genesis hash", err)
}
if blockHash != genesisHash {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("genesis hash %s does not match expected %s", blockHash, GenesisHash), nil)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("genesis hash %s does not match expected %s", blockHash, GenesisHash), nil)
}
}
// Validate header.
if err := c.ValidateHeader(&blk, height, opts.Forks); err != nil {
if err := c.ValidateHeader(&blk, height); err != nil {
return err
}
// Validate miner transaction structure.
if err := consensus.ValidateMinerTx(&blk.MinerTx, height, opts.Forks); err != nil {
return corelog.E("Chain.processBlockBlobs", "validate miner tx", err)
return coreerr.E("Chain.processBlockBlobs", "validate miner tx", err)
}
// Calculate cumulative difficulty.
@ -186,7 +187,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
if height > 0 {
_, prevMeta, err := c.TopBlock()
if err != nil {
return corelog.E("Chain.processBlockBlobs", "get prev block meta", err)
return coreerr.E("Chain.processBlockBlobs", "get prev block meta", err)
}
cumulDiff = prevMeta.CumulativeDiff + difficulty
} else {
@ -197,13 +198,13 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
minerTxHash := wire.TransactionHash(&blk.MinerTx)
minerGindexes, err := c.indexOutputs(minerTxHash, &blk.MinerTx)
if err != nil {
return corelog.E("Chain.processBlockBlobs", "index miner tx outputs", err)
return coreerr.E("Chain.processBlockBlobs", "index miner tx outputs", err)
}
if err := c.PutTransaction(minerTxHash, &blk.MinerTx, &TxMeta{
KeeperBlock: height,
GlobalOutputIndexes: minerGindexes,
}); err != nil {
return corelog.E("Chain.processBlockBlobs", "store miner tx", err)
return coreerr.E("Chain.processBlockBlobs", "store miner tx", err)
}
// Process regular transactions from txBlobs.
@ -211,27 +212,27 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
txDec := wire.NewDecoder(bytes.NewReader(txBlobData))
tx := wire.DecodeTransaction(txDec)
if err := txDec.Err(); err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("decode tx wire [%d]", i), err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("decode tx wire [%d]", i), err)
}
txHash := wire.TransactionHash(&tx)
// Validate transaction semantics, including the HF5 freeze window.
if err := consensus.ValidateTransactionInBlock(&tx, txBlobData, opts.Forks, height); err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("validate tx %s", txHash), err)
// Validate transaction semantics.
if err := consensus.ValidateTransaction(&tx, txBlobData, opts.Forks, height); err != nil {
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("validate tx %s", txHash), err)
}
// Optionally verify signatures using the chain's output index.
if opts.VerifySignatures {
if err := consensus.VerifyTransactionSignatures(&tx, opts.Forks, height, c.GetRingOutputs, c.GetZCRingOutputs); err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("verify tx signatures %s", txHash), err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("verify tx signatures %s", txHash), err)
}
}
// Index outputs.
gindexes, err := c.indexOutputs(txHash, &tx)
if err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("index tx outputs %s", txHash), err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("index tx outputs %s", txHash), err)
}
// Mark key images as spent.
@ -239,15 +240,11 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
switch inp := vin.(type) {
case types.TxInputToKey:
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
}
case types.TxInputHTLC:
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
}
case types.TxInputZC:
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
}
}
}
@ -257,7 +254,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
KeeperBlock: height,
GlobalOutputIndexes: gindexes,
}); err != nil {
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("store tx %s", txHash), err)
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("store tx %s", txHash), err)
}
}
@ -332,13 +329,13 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
// Batch-fetch tx blobs.
txHexes, missed, err := client.GetTransactions(allHashes)
if err != nil {
return corelog.E("resolveBlockBlobs", "fetch tx blobs", err)
return coreerr.E("resolveBlockBlobs", "fetch tx blobs", err)
}
if len(missed) > 0 {
return corelog.E("resolveBlockBlobs", fmt.Sprintf("daemon missed %d tx(es): %v", len(missed), missed), nil)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("daemon missed %d tx(es): %v", len(missed), missed), nil)
}
if len(txHexes) != len(allHashes) {
return corelog.E("resolveBlockBlobs", fmt.Sprintf("expected %d tx blobs, got %d", len(allHashes), len(txHexes)), nil)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("expected %d tx blobs, got %d", len(allHashes), len(txHexes)), nil)
}
// Index fetched blobs by hash.
@ -366,16 +363,16 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
// Parse header from object_in_json.
hdr, err := parseBlockHeader(bd.ObjectInJSON)
if err != nil {
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse header", bd.Height), err)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse header", bd.Height), err)
}
// Miner tx blob is transactions_details[0].
if len(bd.Transactions) == 0 {
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d has no transactions_details", bd.Height), nil)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d has no transactions_details", bd.Height), nil)
}
minerTxBlob, err := hex.DecodeString(bd.Transactions[0].Blob)
if err != nil {
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d: decode miner tx hex", bd.Height), err)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: decode miner tx hex", bd.Height), err)
}
// Collect regular tx hashes.
@ -383,7 +380,7 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
for _, txInfo := range bd.Transactions[1:] {
h, err := types.HashFromHex(txInfo.ID)
if err != nil {
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse tx hash %s", bd.Height, txInfo.ID), err)
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse tx hash %s", bd.Height, txInfo.ID), err)
}
txHashes = append(txHashes, h)
}
@ -413,17 +410,17 @@ var aggregatedRE = regexp.MustCompile(`"AGGREGATED"\s*:\s*\{([^}]+)\}`)
func parseBlockHeader(objectInJSON string) (*types.BlockHeader, error) {
m := aggregatedRE.FindStringSubmatch(objectInJSON)
if m == nil {
return nil, corelog.E("parseBlockHeader", "AGGREGATED section not found in object_in_json", nil)
return nil, coreerr.E("parseBlockHeader", "AGGREGATED section not found in object_in_json", nil)
}
var hj blockHeaderJSON
if err := json.Unmarshal([]byte("{"+m[1]+"}"), &hj); err != nil {
return nil, corelog.E("parseBlockHeader", "unmarshal AGGREGATED", err)
return nil, coreerr.E("parseBlockHeader", "unmarshal AGGREGATED", err)
}
prevID, err := types.HashFromHex(hj.PrevID)
if err != nil {
return nil, corelog.E("parseBlockHeader", "parse prev_id", err)
return nil, coreerr.E("parseBlockHeader", "parse prev_id", err)
}
return &types.BlockHeader{

View file

@ -12,7 +12,6 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -736,87 +735,6 @@ func TestSync_Bad_InvalidBlockBlob(t *testing.T) {
}
}
func TestSync_Bad_PreHardforkFreeze(t *testing.T) {
genesisBlob, genesisHash := makeGenesisBlockBlob()
genesisBytes, err := hex.DecodeString(genesisBlob)
if err != nil {
t.Fatalf("decode genesis blob: %v", err)
}
regularTx := types.Transaction{
Version: 1,
Vin: []types.TxInput{
types.TxInputToKey{
Amount: 1000000000000,
KeyOffsets: []types.TxOutRef{{
Tag: types.RefTypeGlobalIndex,
GlobalIndex: 0,
}},
KeyImage: types.KeyImage{0xaa, 0xbb, 0xcc},
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),
}
var txBuf bytes.Buffer
txEnc := wire.NewEncoder(&txBuf)
wire.EncodeTransaction(txEnc, &regularTx)
regularTxBlob := txBuf.Bytes()
regularTxHash := wire.TransactionHash(&regularTx)
minerTx1 := testCoinbaseTx(1)
block1 := types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
Nonce: 42,
PrevID: genesisHash,
Timestamp: 1770897720,
},
MinerTx: minerTx1,
TxHashes: []types.Hash{regularTxHash},
}
var blk1Buf bytes.Buffer
blk1Enc := wire.NewEncoder(&blk1Buf)
wire.EncodeBlock(blk1Enc, &block1)
block1Blob := blk1Buf.Bytes()
orig := GenesisHash
GenesisHash = genesisHash.String()
t.Cleanup(func() { GenesisHash = orig })
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
opts := SyncOptions{
Forks: []config.HardFork{
{Version: config.HF0Initial, Height: 0, Mandatory: true},
{Version: config.HF5, Height: 2, Mandatory: true},
},
}
if err := c.processBlockBlobs(genesisBytes, nil, 0, 1, opts); err != nil {
t.Fatalf("process genesis: %v", err)
}
err = c.processBlockBlobs(block1Blob, [][]byte{regularTxBlob}, 1, 100, opts)
if err == nil {
t.Fatal("expected freeze rejection, got nil")
}
if !strings.Contains(err.Error(), "freeze") {
t.Fatalf("expected freeze error, got %v", err)
}
}
// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
func testCoinbaseTxV2(height uint64) types.Transaction {
return types.Transaction{
@ -1019,148 +937,3 @@ func TestSync_Good_ZCInputKeyImageMarkedSpent(t *testing.T) {
t.Error("IsSpent(zc_key_image): got false, want true")
}
}
func TestSync_Good_HTLCInputKeyImageMarkedSpent(t *testing.T) {
genesisBlob, genesisHash := makeGenesisBlockBlob()
htlcKeyImage := types.KeyImage{0x44, 0x55, 0x66}
htlcTx := types.Transaction{
Version: 1,
Vin: []types.TxInput{
types.TxInputHTLC{
HTLCOrigin: "contract-1",
Amount: 1000000000000,
KeyOffsets: []types.TxOutRef{{
Tag: types.RefTypeGlobalIndex,
GlobalIndex: 0,
}},
KeyImage: htlcKeyImage,
EtcDetails: wire.EncodeVarint(0),
},
},
Vout: []types.TxOutput{
types.TxOutputBare{
Amount: 900000000000,
Target: types.TxOutToKey{Key: types.PublicKey{0x21}},
},
},
Extra: wire.EncodeVarint(0),
Attachment: wire.EncodeVarint(0),
}
var txBuf bytes.Buffer
txEnc := wire.NewEncoder(&txBuf)
wire.EncodeTransaction(txEnc, &htlcTx)
htlcTxBlob := hex.EncodeToString(txBuf.Bytes())
htlcTxHash := wire.TransactionHash(&htlcTx)
minerTx1 := testCoinbaseTx(1)
block1 := types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
Nonce: 42,
PrevID: genesisHash,
Timestamp: 1770897720,
},
MinerTx: minerTx1,
TxHashes: []types.Hash{htlcTxHash},
}
var blk1Buf bytes.Buffer
blk1Enc := wire.NewEncoder(&blk1Buf)
wire.EncodeBlock(blk1Enc, &block1)
block1Blob := hex.EncodeToString(blk1Buf.Bytes())
block1Hash := wire.BlockHash(&block1)
orig := GenesisHash
GenesisHash = genesisHash.String()
t.Cleanup(func() { GenesisHash = orig })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/getheight" {
json.NewEncoder(w).Encode(map[string]any{
"height": 2,
"status": "OK",
})
return
}
var req struct {
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
json.NewDecoder(r.Body).Decode(&req)
switch req.Method {
case "get_blocks_details":
blocks := []map[string]any{
{
"height": uint64(0),
"timestamp": uint64(1770897600),
"base_reward": uint64(1000000000000),
"id": genesisHash.String(),
"difficulty": "1",
"type": uint64(1),
"blob": genesisBlob,
"transactions_details": []any{},
},
{
"height": uint64(1),
"timestamp": uint64(1770897720),
"base_reward": uint64(1000000),
"id": block1Hash.String(),
"difficulty": "100",
"type": uint64(1),
"blob": block1Blob,
"transactions_details": []map[string]any{
{
"id": htlcTxHash.String(),
"blob": htlcTxBlob,
"fee": uint64(100000000000),
},
},
},
}
result := map[string]any{
"blocks": blocks,
"status": "OK",
}
resultBytes, _ := json.Marshal(result)
json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": "0",
"result": json.RawMessage(resultBytes),
})
}
}))
defer srv.Close()
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
client := rpc.NewClient(srv.URL)
opts := SyncOptions{
VerifySignatures: false,
Forks: []config.HardFork{
{Version: config.HF1, Height: 0, Mandatory: true},
{Version: config.HF2, Height: 0, Mandatory: true},
},
}
err := c.Sync(context.Background(), client, opts)
if err != nil {
t.Fatalf("Sync: %v", err)
}
spent, err := c.IsSpent(htlcKeyImage)
if err != nil {
t.Fatalf("IsSpent: %v", err)
}
if !spent {
t.Error("IsSpent(htlc_key_image): got false, want true")
}
}

View file

@ -12,14 +12,13 @@ import (
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/consensus"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
)
// ValidateHeader checks a block header before storage.
// expectedHeight is the height at which this block would be stored.
func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64, forks []config.HardFork) error {
func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
currentHeight, err := c.Height()
if err != nil {
return coreerr.E("Chain.ValidateHeader", "validate: get height", err)
@ -35,9 +34,6 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64, forks []co
if !b.PrevID.IsZero() {
return coreerr.E("Chain.ValidateHeader", "validate: genesis block has non-zero prev_id", nil)
}
if err := consensus.CheckBlockVersion(b.MajorVersion, forks, expectedHeight); err != nil {
return coreerr.E("Chain.ValidateHeader", "validate: block version", err)
}
return nil
}
@ -50,11 +46,6 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64, forks []co
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: prev_id %s does not match top block %s", b.PrevID, topMeta.Hash), nil)
}
// Block major version check.
if err := consensus.CheckBlockVersion(b.MajorVersion, forks, expectedHeight); err != nil {
return coreerr.E("Chain.ValidateHeader", "validate: block version", err)
}
// Block size check.
var buf bytes.Buffer
enc := wire.NewEncoder(&buf)

View file

@ -8,9 +8,8 @@ package chain
import (
"testing"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
store "dappco.re/go/core/store"
"dappco.re/go/core/blockchain/types"
)
func TestValidateHeader_Good_Genesis(t *testing.T) {
@ -18,116 +17,6 @@ func TestValidateHeader_Good_Genesis(t *testing.T) {
defer s.Close()
c := New(s)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 0,
Timestamp: 1770897600,
},
MinerTx: testCoinbaseTx(0),
}
err := c.ValidateHeader(blk, 0, config.MainnetForks)
if err != nil {
t.Fatalf("ValidateHeader genesis: %v", err)
}
}
func TestValidateHeader_Good_Sequential(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
// Store block 0.
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
hash0 := types.Hash{0x01}
c.PutBlock(blk0, &BlockMeta{Hash: hash0, Height: 0, Timestamp: 1770897600})
// Validate block 1.
blk1 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 0,
Timestamp: 1770897720,
PrevID: hash0,
},
MinerTx: testCoinbaseTx(1),
}
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
if err != nil {
t.Fatalf("ValidateHeader block 1: %v", err)
}
}
func TestValidateHeader_Bad_WrongPrevID(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0})
blk1 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 0,
Timestamp: 1770897720,
PrevID: types.Hash{0xFF}, // wrong
},
MinerTx: testCoinbaseTx(1),
}
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
if err == nil {
t.Fatal("expected error for wrong prev_id")
}
}
func TestValidateHeader_Bad_WrongHeight(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
blk := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
// Chain is empty (height 0), but we pass expectedHeight=5.
err := c.ValidateHeader(blk, 5, config.MainnetForks)
if err == nil {
t.Fatal("expected error for wrong height")
}
}
func TestValidateHeader_Bad_GenesisNonZeroPrev(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 0,
PrevID: types.Hash{0xFF}, // genesis must have zero prev_id
},
MinerTx: testCoinbaseTx(0),
}
err := c.ValidateHeader(blk, 0, config.MainnetForks)
if err == nil {
t.Fatal("expected error for genesis with non-zero prev_id")
}
}
func TestValidateHeader_Bad_WrongVersion(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
@ -136,8 +25,99 @@ func TestValidateHeader_Bad_WrongVersion(t *testing.T) {
MinerTx: testCoinbaseTx(0),
}
err := c.ValidateHeader(blk, 0, config.MainnetForks)
if err == nil {
t.Fatal("expected error for wrong block version")
err := c.ValidateHeader(blk, 0)
if err != nil {
t.Fatalf("ValidateHeader genesis: %v", err)
}
}
func TestValidateHeader_Good_Sequential(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
// Store block 0.
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
hash0 := types.Hash{0x01}
c.PutBlock(blk0, &BlockMeta{Hash: hash0, Height: 0, Timestamp: 1770897600})
// Validate block 1.
blk1 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
Timestamp: 1770897720,
PrevID: hash0,
},
MinerTx: testCoinbaseTx(1),
}
err := c.ValidateHeader(blk1, 1)
if err != nil {
t.Fatalf("ValidateHeader block 1: %v", err)
}
}
func TestValidateHeader_Bad_WrongPrevID(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0})
blk1 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
Timestamp: 1770897720,
PrevID: types.Hash{0xFF}, // wrong
},
MinerTx: testCoinbaseTx(1),
}
err := c.ValidateHeader(blk1, 1)
if err == nil {
t.Fatal("expected error for wrong prev_id")
}
}
func TestValidateHeader_Bad_WrongHeight(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
blk := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
// Chain is empty (height 0), but we pass expectedHeight=5.
err := c.ValidateHeader(blk, 5)
if err == nil {
t.Fatal("expected error for wrong height")
}
}
func TestValidateHeader_Bad_GenesisNonZeroPrev(t *testing.T) {
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
PrevID: types.Hash{0xFF}, // genesis must have zero prev_id
},
MinerTx: testCoinbaseTx(0),
}
err := c.ValidateHeader(blk, 0)
if err == nil {
t.Fatal("expected error for genesis with non-zero prev_id")
}
}

View file

@ -6,8 +6,6 @@
package blockchain
import (
"fmt"
"net"
"os"
"path/filepath"
@ -30,9 +28,9 @@ const defaultChainSeed = "seeds.lthn.io:36942"
// command path documents the node features directly.
func AddChainCommands(root *cobra.Command) {
var (
chainDataDir string
seedPeerAddress string
useTestnet bool
dataDir string
seed string
testnet bool
)
chainCmd := &cobra.Command{
@ -41,26 +39,26 @@ func AddChainCommands(root *cobra.Command) {
Long: "Manage the Lethean blockchain — sync, explore, and mine.",
}
chainCmd.PersistentFlags().StringVar(&chainDataDir, "data-dir", defaultChainDataDirPath(), "blockchain data directory")
chainCmd.PersistentFlags().StringVar(&seedPeerAddress, "seed", defaultChainSeed, "seed peer address (host:port)")
chainCmd.PersistentFlags().BoolVar(&useTestnet, "testnet", false, "use testnet")
chainCmd.PersistentFlags().StringVar(&dataDir, "data-dir", defaultChainDataDirPath(), "blockchain data directory")
chainCmd.PersistentFlags().StringVar(&seed, "seed", defaultChainSeed, "seed peer address (host:port)")
chainCmd.PersistentFlags().BoolVar(&testnet, "testnet", false, "use testnet")
chainCmd.AddCommand(
newChainExplorerCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
newChainSyncCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
newChainExplorerCommand(&dataDir, &seed, &testnet),
newChainSyncCommand(&dataDir, &seed, &testnet),
)
root.AddCommand(chainCmd)
}
func chainConfigForSeed(useTestnet bool, seedPeerAddress string) (config.ChainConfig, []config.HardFork, string) {
if useTestnet {
if seedPeerAddress == defaultChainSeed {
seedPeerAddress = "localhost:46942"
func chainConfigForSeed(testnet bool, seed string) (config.ChainConfig, []config.HardFork, string) {
if testnet {
if seed == defaultChainSeed {
seed = "localhost:46942"
}
return config.Testnet, config.TestnetForks, seedPeerAddress
return config.Testnet, config.TestnetForks, seed
}
return config.Mainnet, config.MainnetForks, seedPeerAddress
return config.Mainnet, config.MainnetForks, seed
}
func defaultChainDataDirPath() string {
@ -77,16 +75,3 @@ func ensureChainDataDirExists(dataDir string) error {
}
return nil
}
func validateChainOptions(chainDataDir, seedPeerAddress string) error {
if chainDataDir == "" {
return coreerr.E("validateChainOptions", "data dir is required", nil)
}
if seedPeerAddress == "" {
return coreerr.E("validateChainOptions", "seed is required", nil)
}
if _, _, err := net.SplitHostPort(seedPeerAddress); err != nil {
return coreerr.E("validateChainOptions", fmt.Sprintf("seed %q must be host:port", seedPeerAddress), err)
}
return nil
}

View file

@ -46,68 +46,3 @@ func TestAddChainCommands_Good_PersistentFlags(t *testing.T) {
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("seed"))
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("testnet"))
}
func TestValidateChainOptions_Good(t *testing.T) {
err := validateChainOptions("/tmp/lethean", "seed.example:36942")
require.NoError(t, err)
}
func TestValidateChainOptions_Bad(t *testing.T) {
tests := []struct {
name string
dataDir string
seed string
want string
}{
{name: "missing data dir", dataDir: "", seed: "seed.example:36942", want: "data dir is required"},
{name: "missing seed", dataDir: "/tmp/lethean", seed: "", want: "seed is required"},
{name: "malformed seed", dataDir: "/tmp/lethean", seed: "seed.example", want: "must be host:port"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateChainOptions(tt.dataDir, tt.seed)
require.Error(t, err)
assert.Contains(t, err.Error(), tt.want)
})
}
}
func TestChainSyncCommand_BadMutuallyExclusiveFlags(t *testing.T) {
dataDir := t.TempDir()
seed := "seed.example:36942"
testnet := false
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
cmd.SetArgs([]string{"--daemon", "--stop"})
err := cmd.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot be combined")
}
func TestChainSyncCommand_BadArgsRejected(t *testing.T) {
dataDir := t.TempDir()
seed := "seed.example:36942"
testnet := false
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
cmd.SetArgs([]string{"extra"})
err := cmd.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown command")
}
func TestChainExplorerCommand_BadSeedRejected(t *testing.T) {
dataDir := t.TempDir()
seed := "bad-seed"
testnet := false
cmd := newChainExplorerCommand(&dataDir, &seed, &testnet)
cmd.SetArgs(nil)
err := cmd.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), "must be host:port")
}

View file

@ -71,8 +71,6 @@ var TestnetForks = []HardFork{
//
// A fork with Height=0 is active from genesis (height 0).
// A fork with Height=N is active at heights > N.
//
// version := config.VersionAtHeight(config.MainnetForks, 15000) // returns HF2
func VersionAtHeight(forks []HardFork, height uint64) uint8 {
var version uint8
for _, hf := range forks {
@ -87,8 +85,6 @@ func VersionAtHeight(forks []HardFork, height uint64) uint8 {
// IsHardForkActive reports whether the specified hardfork version is active
// at the given block height.
//
// if config.IsHardForkActive(config.MainnetForks, config.HF4Zarcanum, height) { /* Zarcanum rules apply */ }
func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool {
for _, hf := range forks {
if hf.Version == version {
@ -101,8 +97,6 @@ func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool {
// HardforkActivationHeight returns the activation height for the given
// hardfork version. The fork becomes active at heights strictly greater
// than the returned value. Returns (0, false) if the version is not found.
//
// height, ok := config.HardforkActivationHeight(config.TestnetForks, config.HF5)
func HardforkActivationHeight(forks []HardFork, version uint8) (uint64, bool) {
for _, hf := range forks {
if hf.Version == version {

View file

@ -1,20 +0,0 @@
// 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 consensus
import "dappco.re/go/core/blockchain/crypto"
// VerifyBalanceProof verifies a generic double-Schnorr proof against the
// provided public points.
//
// The caller is responsible for constructing the balance context point(s)
// from transaction inputs, outputs, fees, and any asset-operation terms.
// This helper only performs the cryptographic check.
//
// ok := consensus.VerifyBalanceProof(txHash, false, pointA, pointB, proofBytes)
func VerifyBalanceProof(hash [32]byte, aIsX bool, a [32]byte, b [32]byte, proof []byte) bool {
return crypto.VerifyDoubleSchnorr(hash, aIsX, a, b, proof)
}

View file

@ -23,8 +23,6 @@ func IsPoS(flags uint8) bool {
// CheckTimestamp validates a block's timestamp against future limits and
// the median of recent timestamps.
//
// consensus.CheckTimestamp(blk.Timestamp, blk.Flags, uint64(time.Now().Unix()), recentTimestamps)
func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, recentTimestamps []uint64) error {
// Future time limit.
limit := config.BlockFutureTimeLimit
@ -63,38 +61,10 @@ func medianTimestamp(timestamps []uint64) uint64 {
return sorted[n/2]
}
func expectedMinerTxVersion(forks []config.HardFork, height uint64) uint64 {
switch {
case config.IsHardForkActive(forks, config.HF5, height):
return types.VersionPostHF5
case config.IsHardForkActive(forks, config.HF4Zarcanum, height):
return types.VersionPostHF4
case config.IsHardForkActive(forks, config.HF1, height):
return types.VersionPreHF4
default:
return types.VersionInitial
}
}
// ValidateMinerTx checks the structure of a coinbase (miner) transaction.
// For PoW blocks: exactly 1 input (TxInputGenesis). For PoS blocks: exactly
// 2 inputs (TxInputGenesis + stake input).
//
// consensus.ValidateMinerTx(&blk.MinerTx, height, config.MainnetForks)
func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFork) error {
expectedVersion := expectedMinerTxVersion(forks, height)
if tx.Version != expectedVersion {
return coreerr.E("ValidateMinerTx", fmt.Sprintf("version %d invalid at height %d (expected %d)",
tx.Version, height, expectedVersion), ErrMinerTxVersion)
}
if tx.Version >= types.VersionPostHF5 {
activeHardForkVersion := config.VersionAtHeight(forks, height)
if tx.HardforkID != activeHardForkVersion {
return coreerr.E("ValidateMinerTx", fmt.Sprintf("hardfork id %d does not match active fork %d at height %d",
tx.HardforkID, activeHardForkVersion, height), ErrMinerTxVersion)
}
}
if len(tx.Vin) == 0 {
return coreerr.E("ValidateMinerTx", "no inputs", ErrMinerTxInputs)
}
@ -116,13 +86,12 @@ func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFo
switch tx.Vin[1].(type) {
case types.TxInputToKey:
// Pre-HF4 PoS.
case types.TxInputZC:
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
if !hardForkFourActive {
default:
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
if !hf4Active {
return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs)
}
default:
return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs)
// Post-HF4: accept ZC inputs.
}
} else {
return coreerr.E("ValidateMinerTx", fmt.Sprintf("%d inputs (expected 1 or 2)", len(tx.Vin)), ErrMinerTxInputs)
@ -133,11 +102,6 @@ func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFo
// ValidateBlockReward checks that the miner transaction outputs do not
// exceed the expected reward (base reward + fees for pre-HF4).
//
// Post-HF4 miner transactions may use Zarcanum outputs, so the validator
// sums both transparent amounts and the encoded Zarcanum amount field.
//
// consensus.ValidateBlockReward(&blk.MinerTx, height, blockSize, medianSize, totalFees, config.MainnetForks)
func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSize, totalFees uint64, forks []config.HardFork) error {
base := BaseReward(height)
reward, err := BlockReward(base, blockSize, medianSize)
@ -145,17 +109,14 @@ func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSi
return err
}
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
expected := MinerReward(reward, totalFees, hardForkFourActive)
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
expected := MinerReward(reward, totalFees, hf4Active)
// Sum miner tx outputs.
var outputSum uint64
for _, vout := range minerTx.Vout {
switch out := vout.(type) {
case types.TxOutputBare:
outputSum += out.Amount
case types.TxOutputZarcanum:
outputSum += out.EncryptedAmount
if bare, ok := vout.(types.TxOutputBare); ok {
outputSum += bare.Amount
}
}
@ -188,34 +149,24 @@ func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 {
// checkBlockVersion validates that the block's major version matches
// what is expected at the given height in the fork schedule.
func checkBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
func checkBlockVersion(blk *types.Block, forks []config.HardFork, height uint64) error {
expected := expectedBlockMajorVersion(forks, height)
if majorVersion != expected {
return coreerr.E("CheckBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
majorVersion, expected, height), ErrBlockMajorVersion)
if blk.MajorVersion != expected {
return coreerr.E("checkBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
blk.MajorVersion, expected, height), ErrBlockMajorVersion)
}
return nil
}
// CheckBlockVersion validates that the block's major version matches the
// expected version for the supplied height and fork schedule.
//
// consensus.CheckBlockVersion(blk.MajorVersion, config.MainnetForks, height)
func CheckBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
return checkBlockVersion(majorVersion, forks, height)
}
// ValidateBlock performs full consensus validation on a block. It checks
// the block version, timestamp, miner transaction structure, and reward.
// Transaction semantic validation for regular transactions should be done
// separately via ValidateTransaction for each tx in the block.
//
// consensus.ValidateBlock(&blk, height, blockSize, medianSize, totalFees, adjustedTime, recentTimestamps, config.MainnetForks)
func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, adjustedTime uint64,
recentTimestamps []uint64, forks []config.HardFork) error {
// Block major version check.
if err := checkBlockVersion(blk.MajorVersion, forks, height); err != nil {
if err := checkBlockVersion(blk, forks, height); err != nil {
return err
}
@ -248,8 +199,6 @@ func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, a
//
// Returns false if the fork version is not found or if the activation height
// is too low for a meaningful freeze window.
//
// if consensus.IsPreHardforkFreeze(config.TestnetForks, config.HF5, height) { /* reject non-coinbase txs */ }
func IsPreHardforkFreeze(forks []config.HardFork, version uint8, height uint64) bool {
activationHeight, ok := config.HardforkActivationHeight(forks, version)
if !ok {
@ -274,8 +223,6 @@ func IsPreHardforkFreeze(forks []config.HardFork, version uint8, height uint64)
// pre-hardfork freeze check. This wraps ValidateTransaction with an
// additional check: during the freeze window before HF5, non-coinbase
// transactions are rejected.
//
// consensus.ValidateTransactionInBlock(&tx, txBlob, config.MainnetForks, blockHeight)
func ValidateTransactionInBlock(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error {
// Pre-hardfork freeze: reject non-coinbase transactions in the freeze window.
if !isCoinbase(tx) && IsPreHardforkFreeze(forks, config.HF5, height) {

View file

@ -74,15 +74,6 @@ func validMinerTx(height uint64) *types.Transaction {
}
}
func validMinerTxForForks(height uint64, forks []config.HardFork) *types.Transaction {
tx := validMinerTx(height)
tx.Version = expectedMinerTxVersion(forks, height)
if tx.Version >= types.VersionPostHF5 {
tx.HardforkID = config.VersionAtHeight(forks, height)
}
return tx
}
func TestValidateMinerTx_Good(t *testing.T) {
tx := validMinerTx(100)
err := ValidateMinerTx(tx, 100, config.MainnetForks)
@ -96,14 +87,14 @@ func TestValidateMinerTx_Bad_WrongHeight(t *testing.T) {
}
func TestValidateMinerTx_Bad_NoInputs(t *testing.T) {
tx := &types.Transaction{Version: types.VersionPreHF4}
tx := &types.Transaction{Version: types.VersionInitial}
err := ValidateMinerTx(tx, 100, config.MainnetForks)
assert.ErrorIs(t, err, ErrMinerTxInputs)
}
func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Version: types.VersionInitial,
Vin: []types.TxInput{types.TxInputToKey{Amount: 1}},
}
err := ValidateMinerTx(tx, 100, config.MainnetForks)
@ -112,7 +103,7 @@ func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
func TestValidateMinerTx_Good_PoS(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Version: types.VersionInitial,
Vin: []types.TxInput{
types.TxInputGenesis{Height: 100},
types.TxInputToKey{Amount: 1}, // PoS stake input
@ -126,148 +117,6 @@ func TestValidateMinerTx_Good_PoS(t *testing.T) {
require.NoError(t, err)
}
func TestValidateMinerTx_Good_PoS_ZCAfterHF4(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{
types.TxInputGenesis{Height: 101},
types.TxInputZC{KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
err := ValidateMinerTx(tx, 101, config.TestnetForks)
require.NoError(t, err)
}
func TestValidateMinerTx_Bad_PoS_UnsupportedStakeInput(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{
types.TxInputGenesis{Height: 101},
types.TxInputHTLC{Amount: 1, KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
err := ValidateMinerTx(tx, 101, config.TestnetForks)
assert.ErrorIs(t, err, ErrMinerTxInputs)
}
func TestValidateMinerTx_Version_Good(t *testing.T) {
tests := []struct {
name string
forks []config.HardFork
tx *types.Transaction
height uint64
}{
{
name: "mainnet_pre_hf1_v0",
forks: config.MainnetForks,
height: 100,
tx: validMinerTx(100),
},
{
name: "mainnet_post_hf1_pre_hf4_v1",
forks: config.MainnetForks,
height: 10081,
tx: &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 10081}},
Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
},
},
{
name: "testnet_post_hf4_v2",
forks: config.TestnetForks,
height: 101,
tx: &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 101}},
Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
},
},
{
name: "testnet_post_hf5_v3",
forks: config.TestnetForks,
height: 201,
tx: &types.Transaction{
Version: types.VersionPostHF5,
HardforkID: config.HF5,
Vin: []types.TxInput{types.TxInputGenesis{Height: 201}},
Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateMinerTx(tt.tx, tt.height, tt.forks)
require.NoError(t, err)
})
}
}
func TestValidateMinerTx_Version_Bad(t *testing.T) {
tests := []struct {
name string
forks []config.HardFork
height uint64
tx *types.Transaction
}{
{
name: "mainnet_pre_hf1_v1",
forks: config.MainnetForks,
height: 100,
tx: &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 100}},
Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
},
},
{
name: "mainnet_post_hf1_pre_hf4_v0",
forks: config.MainnetForks,
height: 10081,
tx: &types.Transaction{
Version: types.VersionInitial,
Vin: []types.TxInput{types.TxInputGenesis{Height: 10081}},
Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
},
},
{
name: "testnet_post_hf4_v1",
forks: config.TestnetForks,
height: 101,
tx: &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 101}},
Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
},
},
{
name: "testnet_post_hf5_wrong_hardfork_id",
forks: config.TestnetForks,
height: 201,
tx: &types.Transaction{
Version: types.VersionPostHF5,
HardforkID: config.HF4Zarcanum,
Vin: []types.TxInput{types.TxInputGenesis{Height: 201}},
Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateMinerTx(tt.tx, tt.height, tt.forks)
assert.ErrorIs(t, err, ErrMinerTxVersion)
})
}
}
func TestValidateBlockReward_Good(t *testing.T) {
height := uint64(100)
tx := validMinerTx(height)
@ -278,7 +127,7 @@ func TestValidateBlockReward_Good(t *testing.T) {
func TestValidateBlockReward_Bad_TooMuch(t *testing.T) {
height := uint64(100)
tx := &types.Transaction{
Version: types.VersionPreHF4,
Version: types.VersionInitial,
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: config.BlockReward + 1, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
@ -292,7 +141,7 @@ func TestValidateBlockReward_Good_WithFees(t *testing.T) {
height := uint64(100)
fees := uint64(50_000_000_000)
tx := &types.Transaction{
Version: types.VersionPreHF4,
Version: types.VersionInitial,
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: config.BlockReward + fees, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
@ -302,53 +151,6 @@ func TestValidateBlockReward_Good_WithFees(t *testing.T) {
require.NoError(t, err)
}
func TestValidateBlockReward_Good_ZarcanumOutputs(t *testing.T) {
height := uint64(100)
tx := &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
Vout: []types.TxOutput{
types.TxOutputZarcanum{
StealthAddress: types.PublicKey{1},
ConcealingPoint: types.PublicKey{2},
AmountCommitment: types.PublicKey{3},
BlindedAssetID: types.PublicKey{4},
EncryptedAmount: config.BlockReward / 2,
},
types.TxOutputZarcanum{
StealthAddress: types.PublicKey{5},
ConcealingPoint: types.PublicKey{6},
AmountCommitment: types.PublicKey{7},
BlindedAssetID: types.PublicKey{8},
EncryptedAmount: config.BlockReward / 2,
},
},
}
err := ValidateBlockReward(tx, height, 1000, config.BlockGrantedFullRewardZone, 0, config.MainnetForks)
require.NoError(t, err)
}
func TestValidateBlockReward_Bad_ZarcanumOutputs(t *testing.T) {
height := uint64(100)
tx := &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
Vout: []types.TxOutput{
types.TxOutputZarcanum{
StealthAddress: types.PublicKey{1},
ConcealingPoint: types.PublicKey{2},
AmountCommitment: types.PublicKey{3},
BlindedAssetID: types.PublicKey{4},
EncryptedAmount: config.BlockReward + 1,
},
},
}
err := ValidateBlockReward(tx, height, 1000, config.BlockGrantedFullRewardZone, 0, config.MainnetForks)
assert.ErrorIs(t, err, ErrRewardMismatch)
}
func TestValidateBlock_Good(t *testing.T) {
now := uint64(time.Now().Unix())
height := uint64(100)
@ -492,7 +294,7 @@ func TestCheckBlockVersion_Good(t *testing.T) {
Flags: 0,
},
}
err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
err := checkBlockVersion(blk, tt.forks, tt.height)
require.NoError(t, err)
})
}
@ -521,7 +323,7 @@ func TestCheckBlockVersion_Bad(t *testing.T) {
Flags: 0,
},
}
err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
err := checkBlockVersion(blk, tt.forks, tt.height)
assert.ErrorIs(t, err, ErrBlockVersion)
})
}
@ -534,17 +336,17 @@ func TestCheckBlockVersion_Ugly(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 255, Timestamp: now},
}
err := checkBlockVersion(blk.MajorVersion, config.MainnetForks, 0)
err := checkBlockVersion(blk, config.MainnetForks, 0)
assert.ErrorIs(t, err, ErrBlockVersion)
err = checkBlockVersion(blk.MajorVersion, config.MainnetForks, 10081)
err = checkBlockVersion(blk, config.MainnetForks, 10081)
assert.ErrorIs(t, err, ErrBlockVersion)
// Version 0 at the exact HF1 boundary (height 10080 -- fork not yet active).
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: config.BlockMajorVersionInitial, Timestamp: now},
}
err = checkBlockVersion(blk0.MajorVersion, config.MainnetForks, 10080)
err = checkBlockVersion(blk0, config.MainnetForks, 10080)
require.NoError(t, err)
}
@ -574,7 +376,7 @@ func TestValidateBlock_MajorVersion_Good(t *testing.T) {
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTxForForks(tt.height, tt.forks),
MinerTx: *validMinerTx(tt.height),
}
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
require.NoError(t, err)
@ -608,7 +410,7 @@ func TestValidateBlock_MajorVersion_Bad(t *testing.T) {
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTxForForks(tt.height, tt.forks),
MinerTx: *validMinerTx(tt.height),
}
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
assert.ErrorIs(t, err, ErrBlockMajorVersion)

View file

@ -14,7 +14,6 @@
// - Cryptographic: PoW hash verification (RandomX via CGo),
// ring signature verification, proof verification.
//
// All validation functions take a hardfork schedule ([]config.HardFork)
// and a block height for hardfork-aware gating. The package has no
// dependency on chain/ or any storage layer.
// All functions take *config.ChainConfig and a block height for
// hardfork-aware validation. The package has no dependency on chain/.
package consensus

View file

@ -29,16 +29,15 @@ var (
ErrNegativeFee = errors.New("consensus: outputs exceed inputs")
// Block errors.
ErrBlockTooLarge = errors.New("consensus: block exceeds max size")
ErrBlockMajorVersion = errors.New("consensus: invalid block major version for height")
ErrTimestampFuture = errors.New("consensus: block timestamp too far in future")
ErrTimestampOld = errors.New("consensus: block timestamp below median")
ErrMinerTxInputs = errors.New("consensus: invalid miner transaction inputs")
ErrMinerTxHeight = errors.New("consensus: miner transaction height mismatch")
ErrMinerTxVersion = errors.New("consensus: invalid miner transaction version for current hardfork")
ErrMinerTxUnlock = errors.New("consensus: miner transaction unlock time invalid")
ErrRewardMismatch = errors.New("consensus: block reward mismatch")
ErrMinerTxProofs = errors.New("consensus: miner transaction proof count invalid")
ErrBlockTooLarge = errors.New("consensus: block exceeds max size")
ErrBlockMajorVersion = errors.New("consensus: invalid block major version for height")
ErrTimestampFuture = errors.New("consensus: block timestamp too far in future")
ErrTimestampOld = errors.New("consensus: block timestamp below median")
ErrMinerTxInputs = errors.New("consensus: invalid miner transaction inputs")
ErrMinerTxHeight = errors.New("consensus: miner transaction height mismatch")
ErrMinerTxUnlock = errors.New("consensus: miner transaction unlock time invalid")
ErrRewardMismatch = errors.New("consensus: block reward mismatch")
ErrMinerTxProofs = errors.New("consensus: miner transaction proof count invalid")
// ErrBlockVersion is an alias for ErrBlockMajorVersion, used by
// checkBlockVersion when the block major version does not match

View file

@ -17,8 +17,6 @@ import (
// TxFee calculates the transaction fee for pre-HF4 (v0/v1) transactions.
// Coinbase transactions return 0. For standard transactions, fee equals
// the difference between total input amounts and total output amounts.
//
// fee, err := consensus.TxFee(&tx)
func TxFee(tx *types.Transaction) (uint64, error) {
if isCoinbase(tx) {
return 0, nil

View file

@ -19,8 +19,6 @@ var maxTarget = new(big.Int).Lsh(big.NewInt(1), 256)
// CheckDifficulty returns true if hash meets the given difficulty target.
// The hash (interpreted as a 256-bit little-endian number) must be less
// than maxTarget / difficulty.
//
// if consensus.CheckDifficulty(powHash, currentDifficulty) { /* valid PoW solution */ }
func CheckDifficulty(hash types.Hash, difficulty uint64) bool {
if difficulty == 0 {
return true
@ -41,8 +39,6 @@ func CheckDifficulty(hash types.Hash, difficulty uint64) bool {
// CheckPoWHash computes the RandomX hash of a block header hash + nonce
// and checks it against the difficulty target.
//
// valid, err := consensus.CheckPoWHash(headerHash, nonce, difficulty)
func CheckPoWHash(headerHash types.Hash, nonce, difficulty uint64) (bool, error) {
// Build input: header_hash (32 bytes) || nonce (8 bytes LE).
var input [40]byte

View file

@ -17,8 +17,6 @@ import (
// BaseReward returns the base block reward at the given height.
// Height 0 (genesis) returns the premine amount. All other heights
// return the fixed block reward (1 LTHN).
//
// reward := consensus.BaseReward(15000) // 1_000_000_000_000 (1 LTHN)
func BaseReward(height uint64) uint64 {
if height == 0 {
return config.Premine
@ -35,8 +33,6 @@ func BaseReward(height uint64) uint64 {
// reward = baseReward * (2*median - size) * size / median²
//
// Uses math/bits.Mul64 for 128-bit intermediate products to avoid overflow.
//
// reward, err := consensus.BlockReward(consensus.BaseReward(height), blockSize, medianSize)
func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
effectiveMedian := medianSize
if effectiveMedian < config.BlockGrantedFullRewardZone {
@ -76,9 +72,6 @@ func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
// MinerReward calculates the total miner payout. Pre-HF4, transaction
// fees are added to the base reward. Post-HF4 (postHF4=true), fees are
// burned and the miner receives only the base reward.
//
// payout := consensus.MinerReward(reward, totalFees, false) // pre-HF4: reward + fees
// payout := consensus.MinerReward(reward, totalFees, true) // post-HF4: reward only (fees burned)
func MinerReward(baseReward, totalFees uint64, postHF4 bool) uint64 {
if postHF4 {
return baseReward

View file

@ -12,34 +12,15 @@ import (
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
)
type transactionForkState struct {
activeHardForkVersion uint8
hardForkOneActive bool
hardForkFourActive bool
hardForkFiveActive bool
}
func newTransactionForkState(forks []config.HardFork, height uint64) transactionForkState {
return transactionForkState{
activeHardForkVersion: config.VersionAtHeight(forks, height),
hardForkOneActive: config.IsHardForkActive(forks, config.HF1, height),
hardForkFourActive: config.IsHardForkActive(forks, config.HF4Zarcanum, height),
hardForkFiveActive: config.IsHardForkActive(forks, config.HF5, height),
}
}
// ValidateTransaction performs semantic validation on a regular (non-coinbase)
// transaction. Checks are ordered to match the C++ validate_tx_semantic().
//
// consensus.ValidateTransaction(&tx, txBlob, config.MainnetForks, blockHeight)
func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error {
state := newTransactionForkState(forks, height)
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
// 0. Transaction version.
if err := checkTxVersion(tx, state, height); err != nil {
if err := checkTxVersion(tx, forks, height); err != nil {
return err
}
@ -56,18 +37,15 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
return coreerr.E("ValidateTransaction", fmt.Sprintf("%d", len(tx.Vin)), ErrTooManyInputs)
}
hf1Active := config.IsHardForkActive(forks, config.HF1, height)
// 3. Input types — TxInputGenesis not allowed in regular transactions.
if err := checkInputTypes(tx, state); err != nil {
if err := checkInputTypes(tx, hf1Active, hf4Active); err != nil {
return err
}
// 4. Output validation.
if err := checkOutputs(tx, state); err != nil {
return err
}
// 4a. HF5 asset operation validation inside extra.
if err := checkAssetOperations(tx.Extra, state.hardForkFiveActive); err != nil {
if err := checkOutputs(tx, hf1Active, hf4Active); err != nil {
return err
}
@ -85,7 +63,7 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
}
// 7. Balance check (pre-HF4 only — post-HF4 uses commitment proofs).
if !state.hardForkFourActive {
if !hf4Active {
if _, err := TxFee(tx); err != nil {
return err
}
@ -97,37 +75,27 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
// checkTxVersion validates that the transaction version is appropriate for the
// current hardfork era.
//
// Pre-HF4: regular transactions must use version 1.
// HF4 era: regular transactions must use version 2.
// HF5+: transaction version must be exactly version 3 and the embedded
// hardfork_id must match the active hardfork version.
func checkTxVersion(tx *types.Transaction, state transactionForkState, height uint64) error {
var expectedVersion uint64
switch {
case state.hardForkFiveActive:
expectedVersion = types.VersionPostHF5
case state.hardForkFourActive:
expectedVersion = types.VersionPostHF4
default:
expectedVersion = types.VersionPreHF4
}
// After HF5: transaction version must be >= VersionPostHF5 (3).
// Before HF5: transaction version 3 is rejected (too early).
func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint64) error {
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
if tx.Version != expectedVersion {
if hf5Active && tx.Version < types.VersionPostHF5 {
return coreerr.E("checkTxVersion",
fmt.Sprintf("version %d invalid at height %d (expected %d)", tx.Version, height, expectedVersion),
fmt.Sprintf("version %d too low after HF5 at height %d", tx.Version, height),
ErrTxVersionInvalid)
}
if tx.Version >= types.VersionPostHF5 && tx.HardforkID != state.activeHardForkVersion {
if !hf5Active && tx.Version >= types.VersionPostHF5 {
return coreerr.E("checkTxVersion",
fmt.Sprintf("hardfork id %d does not match active fork %d at height %d", tx.HardforkID, state.activeHardForkVersion, height),
fmt.Sprintf("version %d not allowed before HF5 at height %d", tx.Version, height),
ErrTxVersionInvalid)
}
return nil
}
func checkInputTypes(tx *types.Transaction, state transactionForkState) error {
func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error {
for _, vin := range tx.Vin {
switch vin.(type) {
case types.TxInputToKey:
@ -136,26 +104,25 @@ func checkInputTypes(tx *types.Transaction, state transactionForkState) error {
return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType)
case types.TxInputHTLC, types.TxInputMultisig:
// HTLC and multisig inputs require at least HF1.
if !state.hardForkOneActive {
if !hf1Active {
return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF1", vin.InputType()), ErrInvalidInputType)
}
case types.TxInputZC:
if !state.hardForkFourActive {
default:
// Future types (ZC) — accept if HF4+.
if !hf4Active {
return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF4", vin.InputType()), ErrInvalidInputType)
}
default:
return coreerr.E("checkInputTypes", fmt.Sprintf("unsupported input type %T", vin), ErrInvalidInputType)
}
}
return nil
}
func checkOutputs(tx *types.Transaction, state transactionForkState) error {
func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
if len(tx.Vout) == 0 {
return ErrNoOutputs
}
if state.hardForkFourActive && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
if hf4Active && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
return coreerr.E("checkOutputs", fmt.Sprintf("%d (min %d)", len(tx.Vout), config.TxMinAllowedOutputs), ErrTooFewOutputs)
}
@ -169,24 +136,15 @@ func checkOutputs(tx *types.Transaction, state transactionForkState) error {
if o.Amount == 0 {
return coreerr.E("checkOutputs", fmt.Sprintf("output %d has zero amount", i), ErrInvalidOutput)
}
// Only known transparent output targets are accepted.
// HTLC and Multisig output targets require at least HF1.
switch o.Target.(type) {
case types.TxOutToKey:
case types.TxOutHTLC, types.TxOutMultisig:
if !state.hardForkOneActive {
if !hf1Active {
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: HTLC/multisig target pre-HF1", i), ErrInvalidOutput)
}
case nil:
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: missing target", i), ErrInvalidOutput)
default:
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: unsupported target %T", i, o.Target), ErrInvalidOutput)
}
case types.TxOutputZarcanum:
if !state.hardForkFourActive {
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: Zarcanum output pre-HF4", i), ErrInvalidOutput)
}
default:
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: unsupported output type %T", i, vout), ErrInvalidOutput)
// Validated by proof verification.
}
}
@ -212,33 +170,3 @@ func checkKeyImages(tx *types.Transaction) error {
}
return nil
}
func checkAssetOperations(extra []byte, hardForkFiveActive bool) error {
if len(extra) == 0 {
return nil
}
elements, err := wire.DecodeVariantVector(extra)
if err != nil {
return coreerr.E("checkAssetOperations", "parse extra", ErrInvalidExtra)
}
for i, elem := range elements {
if elem.Tag != types.AssetDescriptorOperationTag {
continue
}
if !hardForkFiveActive {
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]: asset descriptor operation pre-HF5", i), ErrInvalidExtra)
}
op, err := wire.DecodeAssetDescriptorOperation(elem.Data)
if err != nil {
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]: decode asset descriptor operation", i), ErrInvalidExtra)
}
if err := op.Validate(); err != nil {
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]", i), err)
}
}
return nil
}

View file

@ -8,12 +8,10 @@
package consensus
import (
"bytes"
"testing"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -34,10 +32,6 @@ func validV1Tx() *types.Transaction {
}
}
type unsupportedTxOutTarget struct{}
func (unsupportedTxOutTarget) TargetType() uint8 { return 250 }
func TestValidateTransaction_Good(t *testing.T) {
tx := validV1Tx()
blob := make([]byte, 100) // small blob
@ -244,178 +238,6 @@ func TestCheckOutputs_MultisigTargetPostHF1_Good(t *testing.T) {
require.NoError(t, err)
}
func TestCheckInputTypes_ZCPreHF4_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputZC{KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
types.TxOutputBare{Amount: 1, Target: types.TxOutToKey{Key: types.PublicKey{2}}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
assert.ErrorIs(t, err, ErrInvalidInputType)
}
func TestCheckOutputs_ZarcanumPreHF4_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputZarcanum{StealthAddress: types.PublicKey{1}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
assert.ErrorIs(t, err, ErrInvalidOutput)
}
func TestCheckOutputs_ZarcanumPostHF4_Good(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{
types.TxInputZC{KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputZarcanum{StealthAddress: types.PublicKey{1}},
types.TxOutputZarcanum{StealthAddress: types.PublicKey{2}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.TestnetForks, 150)
require.NoError(t, err)
}
func TestCheckOutputs_MissingTarget_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: nil},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 20000)
assert.ErrorIs(t, err, ErrInvalidOutput)
}
func TestCheckOutputs_UnsupportedTarget_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: unsupportedTxOutTarget{}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 20000)
assert.ErrorIs(t, err, ErrInvalidOutput)
}
func assetDescriptorExtraBlob(ticker string, ownerZero bool) []byte {
var buf bytes.Buffer
enc := wire.NewEncoder(&buf)
enc.WriteVarint(1)
enc.WriteUint8(types.AssetDescriptorOperationTag)
assetOp := bytes.Buffer{}
opEnc := wire.NewEncoder(&assetOp)
opEnc.WriteUint8(1) // version
opEnc.WriteUint8(types.AssetOpRegister)
opEnc.WriteUint8(0) // no asset id
opEnc.WriteUint8(1) // descriptor present
opEnc.WriteVarint(uint64(len(ticker)))
opEnc.WriteBytes([]byte(ticker))
opEnc.WriteVarint(7)
opEnc.WriteBytes([]byte("Lethean"))
opEnc.WriteUint64LE(1000000)
opEnc.WriteUint64LE(0)
opEnc.WriteUint8(12)
opEnc.WriteVarint(0)
if ownerZero {
opEnc.WriteBytes(make([]byte, 32))
} else {
opEnc.WriteBytes(bytes.Repeat([]byte{0xAA}, 32))
}
opEnc.WriteVarint(0)
opEnc.WriteUint64LE(0)
opEnc.WriteUint64LE(0)
opEnc.WriteVarint(0)
enc.WriteBytes(assetOp.Bytes())
return buf.Bytes()
}
func TestValidateTransaction_AssetDescriptorOperation_Good(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPostHF5,
Vin: []types.TxInput{
types.TxInputZC{
KeyImage: types.KeyImage{1},
},
},
Vout: []types.TxOutput{
types.TxOutputBare{
Amount: 90,
Target: types.TxOutToKey{Key: types.PublicKey{1}},
},
types.TxOutputBare{
Amount: 1,
Target: types.TxOutToKey{Key: types.PublicKey{2}},
},
},
Extra: assetDescriptorExtraBlob("LTHN", false),
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.TestnetForks, 250)
require.NoError(t, err)
}
func TestValidateTransaction_AssetDescriptorOperationPreHF5_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
Extra: assetDescriptorExtraBlob("LTHN", false),
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
assert.ErrorIs(t, err, ErrInvalidExtra)
}
func TestValidateTransaction_AssetDescriptorOperationInvalid_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPostHF5,
Vin: []types.TxInput{
types.TxInputZC{
KeyImage: types.KeyImage{1},
},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
types.TxOutputBare{Amount: 1, Target: types.TxOutToKey{Key: types.PublicKey{2}}},
},
Extra: assetDescriptorExtraBlob("TOO-LONG", true),
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.TestnetForks, 250)
assert.ErrorIs(t, err, ErrInvalidExtra)
}
// --- Key image tests for HTLC (Task 8) ---
func TestCheckKeyImages_HTLCDuplicate_Bad(t *testing.T) {

View file

@ -64,7 +64,7 @@ func TestCheckTxVersion_Good(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkTxVersion(tt.tx, newTransactionForkState(tt.forks, tt.height), tt.height)
err := checkTxVersion(tt.tx, tt.forks, tt.height)
if err != nil {
t.Errorf("checkTxVersion returned unexpected error: %v", err)
}
@ -79,37 +79,15 @@ func TestCheckTxVersion_Bad(t *testing.T) {
forks []config.HardFork
height uint64
}{
// v0 regular transaction before HF4 — must still be v1.
{"v0_before_hf4", func() *types.Transaction {
tx := validV1Tx()
tx.Version = types.VersionInitial
return tx
}(), config.MainnetForks, 5000},
// v1 transaction after HF4 — must be v2.
{"v1_after_hf4", validV1Tx(), config.TestnetForks, 150},
// v2 transaction after HF5 — must be v3.
{"v2_after_hf5", validV2Tx(), config.TestnetForks, 250},
// v3 transaction after HF4 but before HF5 — too early.
{"v3_after_hf4_before_hf5", validV3Tx(), config.TestnetForks, 150},
// v3 transaction after HF5 with wrong hardfork id.
{"v3_after_hf5_wrong_hardfork", func() *types.Transaction {
tx := validV3Tx()
tx.HardforkID = 4
return tx
}(), config.TestnetForks, 250},
// v3 transaction before HF5 — too early.
{"v3_before_hf5", validV3Tx(), config.TestnetForks, 150},
// future version must be rejected.
{"v4_after_hf5", func() *types.Transaction {
tx := validV3Tx()
tx.Version = types.VersionPostHF5 + 1
return tx
}(), config.TestnetForks, 250},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkTxVersion(tt.tx, newTransactionForkState(tt.forks, tt.height), tt.height)
err := checkTxVersion(tt.tx, tt.forks, tt.height)
if err == nil {
t.Error("expected ErrTxVersionInvalid, got nil")
}
@ -118,30 +96,16 @@ func TestCheckTxVersion_Bad(t *testing.T) {
}
func TestCheckTxVersion_Ugly(t *testing.T) {
// v2 at exact HF4 activation boundary (height 101 on testnet, HF4.Height=100).
txHF4 := validV2Tx()
err := checkTxVersion(txHF4, newTransactionForkState(config.TestnetForks, 101), 101)
if err != nil {
t.Errorf("v2 at HF4 activation boundary should be valid: %v", err)
}
// v1 at exact HF4 activation boundary should be rejected.
txPreHF4 := validV1Tx()
err = checkTxVersion(txPreHF4, newTransactionForkState(config.TestnetForks, 101), 101)
if err == nil {
t.Error("v1 at HF4 activation boundary should be rejected")
}
// v3 at exact HF5 activation boundary (height 201 on testnet, HF5.Height=200).
tx := validV3Tx()
err = checkTxVersion(tx, newTransactionForkState(config.TestnetForks, 201), 201)
err := checkTxVersion(tx, config.TestnetForks, 201)
if err != nil {
t.Errorf("v3 at HF5 activation boundary should be valid: %v", err)
}
// v2 at exact HF5 activation boundary — should be rejected.
tx2 := validV2Tx()
err = checkTxVersion(tx2, newTransactionForkState(config.TestnetForks, 201), 201)
err = checkTxVersion(tx2, config.TestnetForks, 201)
if err == nil {
t.Error("v2 at HF5 activation boundary should be rejected")
}

View file

@ -18,18 +18,6 @@ import (
"github.com/stretchr/testify/require"
)
func buildSingleZCSigRaw() []byte {
var buf bytes.Buffer
enc := wire.NewEncoder(&buf)
enc.WriteVarint(1)
enc.WriteUint8(types.SigTypeZC)
enc.WriteBytes(make([]byte, 64))
enc.WriteVarint(0)
enc.WriteVarint(0)
enc.WriteBytes(make([]byte, 64))
return buf.Bytes()
}
// loadTestTx loads and decodes a hex-encoded transaction from testdata.
func loadTestTx(t *testing.T, filename string) *types.Transaction {
t.Helper()
@ -159,20 +147,6 @@ func TestVerifyV2Signatures_BadSigCount(t *testing.T) {
assert.Error(t, err, "should fail with mismatched sig count")
}
func TestVerifyV2Signatures_HTLCWrongSigTag_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPostHF5,
Vin: []types.TxInput{
types.TxInputHTLC{Amount: 100, KeyImage: types.KeyImage{1}},
},
SignaturesRaw: buildSingleZCSigRaw(),
}
err := VerifyTransactionSignatures(tx, config.TestnetForks, 250, nil, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "HTLC")
}
func TestVerifyV2Signatures_TxHash(t *testing.T) {
// Verify the known tx hash matches.
tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex")

View file

@ -16,9 +16,9 @@ import (
"dappco.re/go/core/blockchain/wire"
)
// RingOutputsFn fetches the public keys for a ring at the given spending
// height, amount, and offsets. Used to decouple consensus/ from chain storage.
type RingOutputsFn func(height, amount uint64, offsets []uint64) ([]types.PublicKey, error)
// RingOutputsFn fetches the public keys for a ring at the given amount
// and offsets. Used to decouple consensus/ from chain storage.
type RingOutputsFn func(amount uint64, offsets []uint64) ([]types.PublicKey, error)
// ZCRingMember holds the three public keys per ring entry needed for
// CLSAG GGX verification (HF4+). All fields are premultiplied by 1/8
@ -41,9 +41,6 @@ type ZCRingOutputsFn func(offsets []uint64) ([]ZCRingMember, error)
// getRingOutputs is used for pre-HF4 (V1) signature verification.
// getZCRingOutputs is used for post-HF4 (V2) CLSAG GGX verification.
// Either may be nil for structural-only checks.
//
// consensus.VerifyTransactionSignatures(&tx, config.MainnetForks, height, chain.GetRingOutputs, chain.GetZCRingOutputs)
// consensus.VerifyTransactionSignatures(&tx, config.MainnetForks, height, nil, nil) // structural only
func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork,
height uint64, getRingOutputs RingOutputsFn, getZCRingOutputs ZCRingOutputsFn) error {
@ -52,17 +49,17 @@ func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork,
return nil
}
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
if !hardForkFourActive {
return verifyV1Signatures(tx, height, getRingOutputs)
if !hf4Active {
return verifyV1Signatures(tx, getRingOutputs)
}
return verifyV2Signatures(tx, getZCRingOutputs)
}
// verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions.
func verifyV1Signatures(tx *types.Transaction, height uint64, getRingOutputs RingOutputsFn) error {
func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error {
// Count ring-signing inputs (TxInputToKey and TxInputHTLC contribute
// ring signatures; TxInputMultisig does not).
var ringInputCount int
@ -111,7 +108,7 @@ func verifyV1Signatures(tx *types.Transaction, height uint64, getRingOutputs Rin
offsets[i] = ref.GlobalIndex
}
ringKeys, err := getRingOutputs(height, amount, offsets)
ringKeys, err := getRingOutputs(amount, offsets)
if err != nil {
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err)
}
@ -155,8 +152,7 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: V2 signature count %d != input count %d", len(sigEntries), len(tx.Vin)), nil)
}
// Validate that ZC inputs have ZC_sig and that ring-spending inputs use
// the ring-signature tags that match their spending model.
// Validate that ZC inputs have ZC_sig and vice versa.
for i, vin := range tx.Vin {
switch vin.(type) {
case types.TxInputZC:
@ -167,10 +163,6 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid {
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is to_key but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
}
case types.TxInputHTLC:
if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid {
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is HTLC but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
}
}
}
@ -253,9 +245,8 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
}
}
// Balance proofs are verified by the generic double-Schnorr helper in
// consensus.VerifyBalanceProof once the transaction-specific public
// points have been constructed.
// TODO: Verify balance proof (generic_double_schnorr_sig).
// Requires computing commitment_to_zero and a new bridge function.
return nil
}

View file

@ -48,7 +48,7 @@ func TestVerifyV1Signatures_Good_MockRing(t *testing.T) {
tx.Signatures = [][]types.Signature{make([]types.Signature, 1)}
tx.Signatures[0][0] = types.Signature(sigs[0])
getRing := func(height, amount uint64, offsets []uint64) ([]types.PublicKey, error) {
getRing := func(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
return []types.PublicKey{types.PublicKey(pub)}, nil
}
@ -82,7 +82,7 @@ func TestVerifyV1Signatures_Bad_WrongSig(t *testing.T) {
},
}
getRing := func(height, amount uint64, offsets []uint64) ([]types.PublicKey, error) {
getRing := func(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
return []types.PublicKey{types.PublicKey(pub)}, nil
}

View file

@ -49,6 +49,8 @@ set(CXX_SOURCES
set(RANDOMX_SOURCES
randomx/aes_hash.cpp
randomx/argon2_ref.c
randomx/argon2_ssse3.c
randomx/argon2_avx2.c
randomx/bytecode_machine.cpp
randomx/cpu.cpp
randomx/dataset.cpp
@ -56,47 +58,23 @@ set(RANDOMX_SOURCES
randomx/virtual_memory.c
randomx/vm_interpreted.cpp
randomx/allocator.cpp
randomx/assembly_generator_x86.cpp
randomx/instruction.cpp
randomx/randomx.cpp
randomx/superscalar.cpp
randomx/vm_compiled.cpp
randomx/vm_interpreted_light.cpp
randomx/argon2_core.c
randomx/blake2_generator.cpp
randomx/instructions_portable.cpp
randomx/reciprocal.c
randomx/virtual_machine.cpp
randomx/vm_compiled.cpp
randomx/vm_compiled_light.cpp
randomx/blake2/blake2b.c
randomx/jit_compiler_x86.cpp
randomx/jit_compiler_x86_static.S
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
list(APPEND RANDOMX_SOURCES
randomx/argon2_ssse3.c
randomx/argon2_avx2.c
randomx/assembly_generator_x86.cpp
randomx/jit_compiler_x86.cpp
randomx/jit_compiler_x86_static.S
)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$")
list(APPEND RANDOMX_SOURCES
randomx/jit_compiler_a64.cpp
randomx/jit_compiler_a64_static.S
)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(riscv64|rv64)$")
list(APPEND RANDOMX_SOURCES
randomx/aes_hash_rv64_vector.cpp
randomx/aes_hash_rv64_zvkned.cpp
randomx/cpu_rv64.S
randomx/jit_compiler_rv64.cpp
randomx/jit_compiler_rv64_static.S
randomx/jit_compiler_rv64_vector.cpp
randomx/jit_compiler_rv64_vector_static.S
)
else()
message(FATAL_ERROR "Unsupported RandomX architecture: ${CMAKE_SYSTEM_PROCESSOR}")
endif()
add_library(randomx STATIC ${RANDOMX_SOURCES})
target_include_directories(randomx PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/randomx
@ -107,18 +85,15 @@ set_property(TARGET randomx PROPERTY CXX_STANDARD_REQUIRED ON)
# Platform-specific flags for RandomX
enable_language(ASM)
target_compile_options(randomx PRIVATE -maes)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
target_compile_options(randomx PRIVATE -maes)
check_c_compiler_flag(-mssse3 HAVE_SSSE3)
if(HAVE_SSSE3)
set_source_files_properties(randomx/argon2_ssse3.c PROPERTIES COMPILE_FLAGS -mssse3)
endif()
check_c_compiler_flag(-mavx2 HAVE_AVX2)
if(HAVE_AVX2)
set_source_files_properties(randomx/argon2_avx2.c PROPERTIES COMPILE_FLAGS -mavx2)
endif()
check_c_compiler_flag(-mssse3 HAVE_SSSE3)
if(HAVE_SSSE3)
set_source_files_properties(randomx/argon2_ssse3.c PROPERTIES COMPILE_FLAGS -mssse3)
endif()
check_c_compiler_flag(-mavx2 HAVE_AVX2)
if(HAVE_AVX2)
set_source_files_properties(randomx/argon2_avx2.c PROPERTIES COMPILE_FLAGS -mavx2)
endif()
target_compile_options(randomx PRIVATE
@ -131,6 +106,7 @@ target_compile_options(randomx PRIVATE
# --- Find system dependencies ---
find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
# --- Static library ---
add_library(cryptonote STATIC ${C_SOURCES} ${CXX_SOURCES})
@ -140,6 +116,7 @@ target_include_directories(cryptonote PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/compat
${CMAKE_CURRENT_SOURCE_DIR}/randomx
${OPENSSL_INCLUDE_DIR}
${Boost_INCLUDE_DIRS}
)
target_link_libraries(cryptonote PRIVATE

View file

@ -104,91 +104,37 @@ bool deserialise_bpp(const uint8_t *buf, size_t len, crypto::bpp_signature &sig)
return off == len; // must consume all bytes
}
bool read_bppe_at(const uint8_t *buf, size_t len, size_t *offset,
crypto::bppe_signature &sig) {
if (!read_pubkey_vec(buf, len, offset, sig.L)) return false;
if (!read_pubkey_vec(buf, len, offset, sig.R)) return false;
if (!read_pubkey(buf, len, offset, sig.A0)) return false;
if (!read_pubkey(buf, len, offset, sig.A)) return false;
if (!read_pubkey(buf, len, offset, sig.B)) return false;
if (!read_scalar(buf, len, offset, sig.r)) return false;
if (!read_scalar(buf, len, offset, sig.s)) return false;
if (!read_scalar(buf, len, offset, sig.delta_1)) return false;
if (!read_scalar(buf, len, offset, sig.delta_2)) return false;
return true;
}
// Deserialise a bppe_signature from wire bytes (Bulletproofs++ Enhanced, 2 deltas).
// Layout: varint(len(L)) + L[]*32 + varint(len(R)) + R[]*32
// + A0(32) + A(32) + B(32) + r(32) + s(32) + delta_1(32) + delta_2(32)
bool deserialise_bppe(const uint8_t *buf, size_t len, crypto::bppe_signature &sig) {
size_t off = 0;
if (!read_bppe_at(buf, len, &off, sig)) return false;
if (!read_pubkey_vec(buf, len, &off, sig.L)) return false;
if (!read_pubkey_vec(buf, len, &off, sig.R)) return false;
if (!read_pubkey(buf, len, &off, sig.A0)) return false;
if (!read_pubkey(buf, len, &off, sig.A)) return false;
if (!read_pubkey(buf, len, &off, sig.B)) return false;
if (!read_scalar(buf, len, &off, sig.r)) return false;
if (!read_scalar(buf, len, &off, sig.s)) return false;
if (!read_scalar(buf, len, &off, sig.delta_1)) return false;
if (!read_scalar(buf, len, &off, sig.delta_2)) return false;
return off == len; // must consume all bytes
}
bool read_bge_at(const uint8_t *buf, size_t len, size_t *offset,
crypto::BGE_proof &proof) {
if (!read_pubkey(buf, len, offset, proof.A)) return false;
if (!read_pubkey(buf, len, offset, proof.B)) return false;
if (!read_pubkey_vec(buf, len, offset, proof.Pk)) return false;
if (!read_scalar_vec(buf, len, offset, proof.f)) return false;
if (!read_scalar(buf, len, offset, proof.y)) return false;
if (!read_scalar(buf, len, offset, proof.z)) return false;
return true;
}
// Deserialise a BGE_proof from wire bytes.
// Layout: A(32) + B(32) + varint(len(Pk)) + Pk[]*32
// + varint(len(f)) + f[]*32 + y(32) + z(32)
bool deserialise_bge(const uint8_t *buf, size_t len, crypto::BGE_proof &proof) {
size_t off = 0;
if (!read_bge_at(buf, len, &off, proof)) return false;
if (!read_pubkey(buf, len, &off, proof.A)) return false;
if (!read_pubkey(buf, len, &off, proof.B)) return false;
if (!read_pubkey_vec(buf, len, &off, proof.Pk)) return false;
if (!read_scalar_vec(buf, len, &off, proof.f)) return false;
if (!read_scalar(buf, len, &off, proof.y)) return false;
if (!read_scalar(buf, len, &off, proof.z)) return false;
return off == len;
}
bool read_clsag_ggxxg_at(const uint8_t *buf, size_t len, size_t *offset,
crypto::CLSAG_GGXXG_signature &sig) {
if (!read_scalar(buf, len, offset, sig.c)) return false;
if (!read_scalar_vec(buf, len, offset, sig.r_g)) return false;
if (!read_scalar_vec(buf, len, offset, sig.r_x)) return false;
if (!read_pubkey(buf, len, offset, sig.K1)) return false;
if (!read_pubkey(buf, len, offset, sig.K2)) return false;
if (!read_pubkey(buf, len, offset, sig.K3)) return false;
if (!read_pubkey(buf, len, offset, sig.K4)) return false;
return true;
}
bool deserialise_zarcanum(const uint8_t *buf, size_t len,
crypto::zarcanum_proof &proof) {
size_t off = 0;
if (!read_scalar(buf, len, &off, proof.d)) return false;
if (!read_pubkey(buf, len, &off, proof.C)) return false;
if (!read_pubkey(buf, len, &off, proof.C_prime)) return false;
if (!read_pubkey(buf, len, &off, proof.E)) return false;
if (!read_scalar(buf, len, &off, proof.c)) return false;
if (!read_scalar(buf, len, &off, proof.y0)) return false;
if (!read_scalar(buf, len, &off, proof.y1)) return false;
if (!read_scalar(buf, len, &off, proof.y2)) return false;
if (!read_scalar(buf, len, &off, proof.y3)) return false;
if (!read_scalar(buf, len, &off, proof.y4)) return false;
if (!read_bppe_at(buf, len, &off, proof.E_range_proof)) return false;
if (!read_pubkey(buf, len, &off, proof.pseudo_out_amount_commitment)) return false;
if (!read_clsag_ggxxg_at(buf, len, &off, proof.clsag_ggxxg)) return false;
return off == len;
}
bool deserialise_double_schnorr(const uint8_t *buf, size_t len,
crypto::generic_double_schnorr_sig &sig) {
if (buf == nullptr || len != 96) {
return false;
}
memcpy(sig.c.m_s, buf, 32);
memcpy(sig.y0.m_s, buf + 32, 32);
memcpy(sig.y1.m_s, buf + 64, 32);
return true;
}
} // anonymous namespace
extern "C" {
@ -693,133 +639,13 @@ int cn_bge_verify(const uint8_t context[32], const uint8_t *ring,
}
}
int cn_double_schnorr_generate(int a_is_x, const uint8_t hash[32],
const uint8_t secret_a[32],
const uint8_t secret_b[32],
uint8_t *proof, size_t proof_len) {
if (hash == nullptr || secret_a == nullptr || secret_b == nullptr || proof == nullptr) {
return 1;
}
if (proof_len != 96) {
return 1;
}
try {
crypto::hash m;
memcpy(&m, hash, 32);
crypto::scalar_t sa, sb;
memcpy(sa.m_s, secret_a, 32);
memcpy(sb.m_s, secret_b, 32);
crypto::generic_double_schnorr_sig sig;
bool ok;
if (a_is_x != 0) {
ok = crypto::generate_double_schnorr_sig<crypto::gt_X, crypto::gt_G>(
m, sa * crypto::c_point_X, sa, sb * crypto::c_point_G, sb, sig);
} else {
ok = crypto::generate_double_schnorr_sig<crypto::gt_G, crypto::gt_G>(
m, sa * crypto::c_point_G, sa, sb * crypto::c_point_G, sb, sig);
}
if (!ok) {
return 1;
}
memcpy(proof, sig.c.m_s, 32);
memcpy(proof + 32, sig.y0.m_s, 32);
memcpy(proof + 64, sig.y1.m_s, 32);
return 0;
} catch (...) {
return 1;
}
}
int cn_double_schnorr_verify(int a_is_x, const uint8_t hash[32],
const uint8_t a[32], const uint8_t b[32],
const uint8_t *proof, size_t proof_len) {
if (hash == nullptr || a == nullptr || b == nullptr || proof == nullptr) {
return 1;
}
try {
crypto::hash m;
memcpy(&m, hash, 32);
crypto::public_key b_pk;
memcpy(&b_pk, b, 32);
crypto::public_key a_pk;
memcpy(&a_pk, a, 32);
crypto::point_t a_pt(a_pk);
crypto::generic_double_schnorr_sig sig;
if (!deserialise_double_schnorr(proof, proof_len, sig)) {
return 1;
}
if (a_is_x != 0) {
return crypto::verify_double_schnorr_sig<crypto::gt_X, crypto::gt_G>(m, a_pt, b_pk, sig) ? 0 : 1;
}
return crypto::verify_double_schnorr_sig<crypto::gt_G, crypto::gt_G>(m, a_pt, b_pk, sig) ? 0 : 1;
} catch (...) {
return 1;
}
}
// ── Zarcanum PoS ────────────────────────────────────────
// Compatibility wrapper for the historical proof-only API.
// Zarcanum verification requires many parameters beyond what the current
// bridge API exposes (kernel_hash, ring, last_pow_block_id, stake_ki,
// pos_difficulty). Returns -1 until the API is extended.
int cn_zarcanum_verify(const uint8_t /*hash*/[32], const uint8_t * /*proof*/,
size_t /*proof_len*/) {
return -1;
}
int cn_zarcanum_verify_full(const uint8_t m[32], const uint8_t kernel_hash[32],
const uint8_t *ring, size_t ring_size,
const uint8_t last_pow_block_id_hashed[32],
const uint8_t stake_ki[32],
uint64_t pos_difficulty,
const uint8_t *proof, size_t proof_len) {
if (m == nullptr || kernel_hash == nullptr || ring == nullptr ||
last_pow_block_id_hashed == nullptr || stake_ki == nullptr ||
proof == nullptr || proof_len == 0 || ring_size == 0) {
return 1;
}
try {
crypto::hash msg;
crypto::hash kernel;
crypto::scalar_t last_pow;
crypto::key_image key_img;
memcpy(&msg, m, 32);
memcpy(&kernel, kernel_hash, 32);
memcpy(&last_pow, last_pow_block_id_hashed, 32);
memcpy(&key_img, stake_ki, 32);
std::vector<crypto::public_key> stealth_keys(ring_size);
std::vector<crypto::public_key> commitments(ring_size);
std::vector<crypto::public_key> asset_ids(ring_size);
std::vector<crypto::public_key> concealing_pts(ring_size);
std::vector<crypto::CLSAG_GGXXG_input_ref_t> ring_refs;
ring_refs.reserve(ring_size);
for (size_t i = 0; i < ring_size; ++i) {
memcpy(&stealth_keys[i], ring + i * 128, 32);
memcpy(&commitments[i], ring + i * 128 + 32, 32);
memcpy(&asset_ids[i], ring + i * 128 + 64, 32);
memcpy(&concealing_pts[i], ring + i * 128 + 96, 32);
ring_refs.emplace_back(stealth_keys[i], commitments[i], asset_ids[i], concealing_pts[i]);
}
crypto::zarcanum_proof sig;
if (!deserialise_zarcanum(proof, proof_len, sig)) {
return 1;
}
crypto::mp::uint128_t difficulty(pos_difficulty);
return crypto::zarcanum_verify_proof(msg, kernel, ring_refs, last_pow,
key_img, difficulty, sig) ? 0 : 1;
} catch (...) {
return 1;
}
return -1; // needs extended API — see bridge.h TODO
}
// ── RandomX PoW Hashing ──────────────────────────────────

View file

@ -125,42 +125,12 @@ int cn_bppe_verify(const uint8_t *proof, size_t proof_len,
int cn_bge_verify(const uint8_t context[32], const uint8_t *ring,
size_t ring_size, const uint8_t *proof, size_t proof_len);
// ── Generic Double Schnorr ────────────────────────────────
// Generates a generic_double_schnorr_sig from zarcanum.h.
// a_is_x selects the generator pair:
// 0 -> (G, G)
// 1 -> (X, G)
// proof must point to a 96-byte buffer.
int cn_double_schnorr_generate(int a_is_x, const uint8_t hash[32],
const uint8_t secret_a[32],
const uint8_t secret_b[32],
uint8_t *proof, size_t proof_len);
// Verifies a generic_double_schnorr_sig from zarcanum.h.
// a_is_x selects the generator pair:
// 0 -> (G, G)
// 1 -> (X, G)
// Returns 0 on success, 1 on verification failure or deserialisation error.
int cn_double_schnorr_verify(int a_is_x, const uint8_t hash[32],
const uint8_t a[32], const uint8_t b[32],
const uint8_t *proof, size_t proof_len);
// ── Zarcanum PoS ──────────────────────────────────────────
// Legacy compatibility wrapper for the historical proof-only API.
// TODO: extend API to accept kernel_hash, ring, last_pow_block_id,
// stake_ki, pos_difficulty. Currently returns -1 (not implemented).
int cn_zarcanum_verify(const uint8_t hash[32], const uint8_t *proof,
size_t proof_len);
// Full Zarcanum verification entrypoint.
// ring is a flat array of 128-byte CLSAG_GGXXG ring members:
// [stealth(32) | amount_commitment(32) | blinded_asset_id(32) | concealing(32)]
// Returns 0 on success, 1 on verification failure or deserialisation error.
int cn_zarcanum_verify_full(const uint8_t m[32], const uint8_t kernel_hash[32],
const uint8_t *ring, size_t ring_size,
const uint8_t last_pow_block_id_hashed[32],
const uint8_t stake_ki[32],
uint64_t pos_difficulty,
const uint8_t *proof, size_t proof_len);
// ── RandomX PoW Hashing ──────────────────────────────────
// key/key_size: RandomX cache key (e.g. "LetheanRandomXv1")
// input/input_size: block header hash (32 bytes) + nonce (8 bytes LE)

View file

@ -1,67 +0,0 @@
// 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
#pragma once
#include <cstddef>
#include <cstdint>
namespace boost {
namespace multiprecision {
using limb_type = std::uint64_t;
enum cpp_integer_type {
signed_magnitude,
unsigned_magnitude,
};
enum cpp_int_check_type {
unchecked,
checked,
};
enum expression_template_option {
et_off,
et_on,
};
template <unsigned MinBits = 0, unsigned MaxBits = 0,
cpp_integer_type SignType = signed_magnitude,
cpp_int_check_type Checked = unchecked,
class Allocator = void>
class cpp_int_backend {};
template <class Backend, expression_template_option ExpressionTemplates = et_off>
class number {
public:
number() = default;
number(unsigned long long) {}
class backend_type {
public:
std::size_t size() const { return 0; }
static constexpr std::size_t limb_bits = sizeof(limb_type) * 8;
limb_type *limbs() { return nullptr; }
const limb_type *limbs() const { return nullptr; }
void resize(unsigned, unsigned) {}
void normalize() {}
};
backend_type &backend() { return backend_; }
const backend_type &backend() const { return backend_; }
private:
backend_type backend_{};
};
using uint128_t = number<cpp_int_backend<128, 128, unsigned_magnitude, unchecked, void>>;
using uint256_t = number<cpp_int_backend<256, 256, unsigned_magnitude, unchecked, void>>;
using uint512_t = number<cpp_int_backend<512, 512, unsigned_magnitude, unchecked, void>>;
} // namespace multiprecision
} // namespace boost

View file

@ -578,82 +578,10 @@ func TestBGE_Bad_GarbageProof(t *testing.T) {
}
}
func TestZarcanumCompatibilityWrapper_Bad_EmptyProof(t *testing.T) {
func TestZarcanum_Stub_NotImplemented(t *testing.T) {
// Zarcanum bridge API needs extending — verify it returns false.
hash := [32]byte{0x01}
if crypto.VerifyZarcanum(hash, []byte{0x00}) {
t.Fatal("compatibility wrapper should reject malformed proof data")
}
}
func TestZarcanumWithContext_Bad_MinimalProof(t *testing.T) {
var ctx crypto.ZarcanumVerificationContext
ctx.ContextHash = [32]byte{0x01}
ctx.KernelHash = [32]byte{0x02}
ctx.LastPowBlockIDHashed = [32]byte{0x03}
ctx.StakeKeyImage = [32]byte{0x04}
ctx.PosDifficulty = 1
ctx.Ring = []crypto.ZarcanumRingMember{{
StealthAddress: [32]byte{0x11},
AmountCommitment: [32]byte{0x22},
BlindedAssetID: [32]byte{0x33},
ConcealingPoint: [32]byte{0x44},
}}
// Minimal structurally valid proof blob:
// 10 scalars/points + empty BPPE + pseudo_out_amount_commitment +
// CLSAG_GGXXG with one ring entry and zeroed scalars.
proof := make([]byte, 0, 10*32+2+32+2+32+1+128)
proof = append(proof, make([]byte, 10*32)...)
proof = append(proof, 0x00) // BPPE L length
proof = append(proof, 0x00) // BPPE R length
proof = append(proof, make([]byte, 7*32)...)
proof = append(proof, make([]byte, 32)...)
proof = append(proof, 0x01) // CLSAG_GGXXG r_g length
proof = append(proof, make([]byte, 32)...)
proof = append(proof, 0x01) // CLSAG_GGXXG r_x length
proof = append(proof, make([]byte, 32)...)
proof = append(proof, make([]byte, 128)...)
ctx.Proof = proof
if crypto.VerifyZarcanumWithContext(ctx) {
t.Fatal("minimal Zarcanum proof should fail verification")
}
}
func TestDoubleSchnorr_Bad_EmptyProof(t *testing.T) {
var hash, a, b [32]byte
if crypto.VerifyDoubleSchnorr(hash, true, a, b, nil) {
t.Fatal("empty double-Schnorr proof should fail")
}
}
func TestDoubleSchnorr_Good_Roundtrip(t *testing.T) {
hash := crypto.FastHash([]byte("double-schnorr"))
_, secretA, err := crypto.GenerateKeys()
if err != nil {
t.Fatalf("GenerateKeys(secretA): %v", err)
}
pubA, err := crypto.SecretToPublic(secretA)
if err != nil {
t.Fatalf("SecretToPublic(secretA): %v", err)
}
_, secretB, err := crypto.GenerateKeys()
if err != nil {
t.Fatalf("GenerateKeys(secretB): %v", err)
}
pubB, err := crypto.SecretToPublic(secretB)
if err != nil {
t.Fatalf("SecretToPublic(secretB): %v", err)
}
proof, err := crypto.GenerateDoubleSchnorr(hash, false, secretA, secretB)
if err != nil {
t.Fatalf("GenerateDoubleSchnorr: %v", err)
}
if !crypto.VerifyDoubleSchnorr(hash, false, pubA, pubB, proof[:]) {
t.Fatal("generated double-Schnorr proof failed verification")
t.Fatal("Zarcanum stub should return false")
}
}

View file

@ -7,59 +7,7 @@ package crypto
*/
import "C"
import (
"unsafe"
coreerr "dappco.re/go/core/log"
)
// ZarcanumRingMember is one flat ring entry for Zarcanum verification.
// All fields are stored premultiplied by 1/8, matching the on-chain form.
type ZarcanumRingMember struct {
StealthAddress [32]byte
AmountCommitment [32]byte
BlindedAssetID [32]byte
ConcealingPoint [32]byte
}
// ZarcanumVerificationContext groups the full context required by the
// upstream C++ verifier.
type ZarcanumVerificationContext struct {
ContextHash [32]byte
KernelHash [32]byte
Ring []ZarcanumRingMember
LastPowBlockIDHashed [32]byte
StakeKeyImage [32]byte
Proof []byte
PosDifficulty uint64
}
// GenerateDoubleSchnorr creates a generic_double_schnorr_sig from zarcanum.h.
// aIsX selects the generator pair:
//
// false -> (G, G)
// true -> (X, G)
func GenerateDoubleSchnorr(hash [32]byte, aIsX bool, secretA [32]byte, secretB [32]byte) ([96]byte, error) {
var proof [96]byte
var flag C.int
if aIsX {
flag = 1
}
rc := C.cn_double_schnorr_generate(
flag,
(*C.uint8_t)(unsafe.Pointer(&hash[0])),
(*C.uint8_t)(unsafe.Pointer(&secretA[0])),
(*C.uint8_t)(unsafe.Pointer(&secretB[0])),
(*C.uint8_t)(unsafe.Pointer(&proof[0])),
C.size_t(len(proof)),
)
if rc != 0 {
return proof, coreerr.E("GenerateDoubleSchnorr", "double_schnorr_generate failed", nil)
}
return proof, nil
}
import "unsafe"
// VerifyBPP verifies a Bulletproofs++ range proof (1 delta).
// Used for zc_outs_range_proof in post-HF4 transactions.
@ -126,36 +74,9 @@ func VerifyBGE(context [32]byte, ring [][32]byte, proof []byte) bool {
) == 0
}
// VerifyDoubleSchnorr verifies a generic_double_schnorr_sig from zarcanum.h.
// aIsX selects the generator pair:
//
// false -> (G, G)
// true -> (X, G)
//
// The proof blob is the 96-byte wire encoding: c(32) + y0(32) + y1(32).
func VerifyDoubleSchnorr(hash [32]byte, aIsX bool, a [32]byte, b [32]byte, proof []byte) bool {
if len(proof) != 96 {
return false
}
var flag C.int
if aIsX {
flag = 1
}
return C.cn_double_schnorr_verify(
flag,
(*C.uint8_t)(unsafe.Pointer(&hash[0])),
(*C.uint8_t)(unsafe.Pointer(&a[0])),
(*C.uint8_t)(unsafe.Pointer(&b[0])),
(*C.uint8_t)(unsafe.Pointer(&proof[0])),
C.size_t(len(proof)),
) == 0
}
// VerifyZarcanum verifies a Zarcanum PoS proof.
// This compatibility wrapper remains for the historical proof blob API.
// Use VerifyZarcanumWithContext for full verification.
// Currently returns false — bridge API needs extending to pass kernel_hash,
// ring, last_pow_block_id, stake_ki, and pos_difficulty.
func VerifyZarcanum(hash [32]byte, proof []byte) bool {
if len(proof) == 0 {
return false
@ -166,43 +87,3 @@ func VerifyZarcanum(hash [32]byte, proof []byte) bool {
C.size_t(len(proof)),
) == 0
}
// VerifyZarcanumWithContext verifies a Zarcanum PoS proof with the full
// consensus context required by the upstream verifier.
//
// Example:
//
// crypto.VerifyZarcanumWithContext(crypto.ZarcanumVerificationContext{
// ContextHash: txHash,
// KernelHash: kernelHash,
// Ring: ring,
// LastPowBlockIDHashed: lastPowHash,
// StakeKeyImage: stakeKeyImage,
// PosDifficulty: posDifficulty,
// Proof: proofBlob,
// })
func VerifyZarcanumWithContext(ctx ZarcanumVerificationContext) bool {
if len(ctx.Ring) == 0 || len(ctx.Proof) == 0 {
return false
}
flat := make([]byte, len(ctx.Ring)*128)
for i, member := range ctx.Ring {
copy(flat[i*128:], member.StealthAddress[:])
copy(flat[i*128+32:], member.AmountCommitment[:])
copy(flat[i*128+64:], member.BlindedAssetID[:])
copy(flat[i*128+96:], member.ConcealingPoint[:])
}
return C.cn_zarcanum_verify_full(
(*C.uint8_t)(unsafe.Pointer(&ctx.ContextHash[0])),
(*C.uint8_t)(unsafe.Pointer(&ctx.KernelHash[0])),
(*C.uint8_t)(unsafe.Pointer(&flat[0])),
C.size_t(len(ctx.Ring)),
(*C.uint8_t)(unsafe.Pointer(&ctx.LastPowBlockIDHashed[0])),
(*C.uint8_t)(unsafe.Pointer(&ctx.StakeKeyImage[0])),
C.uint64_t(ctx.PosDifficulty),
(*C.uint8_t)(unsafe.Pointer(&ctx.Proof[0])),
C.size_t(len(ctx.Proof)),
) == 0
}

View file

@ -16,9 +16,6 @@
//
#pragma once
#include <string>
#include <sstream>
#include <iomanip>
#include <stdexcept>
#include <boost/multiprecision/cpp_int.hpp>
#include "crypto.h"
#include "eth_signature.h"

View file

@ -61,8 +61,6 @@ var StarterDifficulty = big.NewInt(1)
//
// where each solve-time interval i is weighted by its position (1..n),
// giving more influence to recent blocks.
//
// nextDiff := difficulty.NextDifficulty(timestamps, cumulativeDiffs, 120)
func NextDifficulty(timestamps []uint64, cumulativeDiffs []*big.Int, target uint64) *big.Int {
// Need at least 2 entries to compute one solve-time interval.
if len(timestamps) < 2 || len(cumulativeDiffs) < 2 {

View file

@ -12,7 +12,7 @@ import (
"path/filepath"
"sync"
corelog "dappco.re/go/core/log"
coreerr "dappco.re/go/core/log"
cli "dappco.re/go/core/cli/pkg/cli"
store "dappco.re/go/core/store"
@ -29,35 +29,31 @@ import (
// chain explorer --data-dir ~/.lethean/chain
//
// Use it alongside `AddChainCommands` to expose the TUI node view.
func newChainExplorerCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool) *cobra.Command {
func newChainExplorerCommand(dataDir, seed *string, testnet *bool) *cobra.Command {
return &cobra.Command{
Use: "explorer",
Short: "TUI block explorer",
Long: "Interactive terminal block explorer with live sync status.",
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
return validateChainOptions(*chainDataDir, *seedPeerAddress)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runChainExplorer(*chainDataDir, *seedPeerAddress, *useTestnet)
return runChainExplorer(*dataDir, *seed, *testnet)
},
}
}
func runChainExplorer(chainDataDir, seedPeerAddress string, useTestnet bool) error {
if err := ensureChainDataDirExists(chainDataDir); err != nil {
func runChainExplorer(dataDir, seed string, testnet bool) error {
if err := ensureChainDataDirExists(dataDir); err != nil {
return err
}
dbPath := filepath.Join(chainDataDir, "chain.db")
dbPath := filepath.Join(dataDir, "chain.db")
chainStore, err := store.New(dbPath)
if err != nil {
return corelog.E("runChainExplorer", "open store", err)
return coreerr.E("runChainExplorer", "open store", err)
}
defer chainStore.Close()
blockchain := chain.New(chainStore)
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
@ -78,7 +74,6 @@ func runChainExplorer(chainDataDir, seedPeerAddress string, useTestnet bool) err
frame.Header(status)
frame.Content(explorer)
frame.Footer(hints)
corelog.Info("running chain explorer", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet)
frame.Run()
cancel() // Signal the sync loop to stop.

View file

@ -7,7 +7,6 @@ package p2p
import (
"encoding/binary"
"fmt"
"dappco.re/go/core/p2p/node/levin"
)
@ -174,29 +173,3 @@ func (r *HandshakeResponse) Decode(data []byte) error {
}
return nil
}
// ValidateHandshakeResponse verifies that a remote peer's handshake response
// matches the expected network and satisfies the minimum build version gate.
//
// Example:
//
// err := ValidateHandshakeResponse(&resp, config.NetworkIDMainnet, false)
func ValidateHandshakeResponse(resp *HandshakeResponse, expectedNetworkID [16]byte, isTestnet bool) error {
if resp.NodeData.NetworkID != expectedNetworkID {
return fmt.Errorf("p2p: peer network id %x does not match expected %x",
resp.NodeData.NetworkID, expectedNetworkID)
}
buildVersion, ok := PeerBuildVersion(resp.PayloadData.ClientVersion)
if !ok {
return fmt.Errorf("p2p: peer build %q is malformed", resp.PayloadData.ClientVersion)
}
if !MeetsMinimumBuildVersion(resp.PayloadData.ClientVersion, isTestnet) {
minBuild := MinimumRequiredBuildVersion(isTestnet)
return fmt.Errorf("p2p: peer build %q parsed as %d below minimum %d",
resp.PayloadData.ClientVersion, buildVersion, minBuild)
}
return nil
}

View file

@ -7,7 +7,6 @@ package p2p
import (
"encoding/binary"
"strings"
"testing"
"dappco.re/go/core/blockchain/config"
@ -155,75 +154,3 @@ func TestDecodePeerlist_Good_EmptyBlob(t *testing.T) {
t.Errorf("empty peerlist: got %d entries, want 0", len(entries))
}
}
func TestValidateHandshakeResponse_Good(t *testing.T) {
resp := &HandshakeResponse{
NodeData: NodeData{
NetworkID: config.NetworkIDTestnet,
},
PayloadData: CoreSyncData{
ClientVersion: "6.0.1.2[go-blockchain]",
},
}
if err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true); err != nil {
t.Fatalf("ValidateHandshakeResponse: %v", err)
}
}
func TestValidateHandshakeResponse_BadNetwork(t *testing.T) {
resp := &HandshakeResponse{
NodeData: NodeData{
NetworkID: config.NetworkIDMainnet,
},
PayloadData: CoreSyncData{
ClientVersion: "6.0.1.2[go-blockchain]",
},
}
err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true)
if err == nil {
t.Fatal("ValidateHandshakeResponse: expected network mismatch error")
}
if !strings.Contains(err.Error(), "network id") {
t.Fatalf("ValidateHandshakeResponse error: got %v, want network id mismatch", err)
}
}
func TestValidateHandshakeResponse_BadBuildVersion(t *testing.T) {
resp := &HandshakeResponse{
NodeData: NodeData{
NetworkID: config.NetworkIDMainnet,
},
PayloadData: CoreSyncData{
ClientVersion: "0.0.1.0",
},
}
err := ValidateHandshakeResponse(resp, config.NetworkIDMainnet, false)
if err == nil {
t.Fatal("ValidateHandshakeResponse: expected build version error")
}
if !strings.Contains(err.Error(), "below minimum") {
t.Fatalf("ValidateHandshakeResponse error: got %v, want build minimum failure", err)
}
}
func TestValidateHandshakeResponse_BadMalformedBuildVersion(t *testing.T) {
resp := &HandshakeResponse{
NodeData: NodeData{
NetworkID: config.NetworkIDMainnet,
},
PayloadData: CoreSyncData{
ClientVersion: "bogus",
},
}
err := ValidateHandshakeResponse(resp, config.NetworkIDMainnet, false)
if err == nil {
t.Fatal("ValidateHandshakeResponse: expected malformed build version error")
}
if !strings.Contains(err.Error(), "malformed") {
t.Fatalf("ValidateHandshakeResponse error: got %v, want malformed build version failure", err)
}
}

View file

@ -1,86 +0,0 @@
// 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 p2p
import (
"strconv"
"strings"
)
const (
// MinimumRequiredBuildVersionMainnet matches the C++ daemon's mainnet gate.
MinimumRequiredBuildVersionMainnet uint64 = 601
// MinimumRequiredBuildVersionTestnet matches the C++ daemon's testnet gate.
MinimumRequiredBuildVersionTestnet uint64 = 2
)
// MinimumRequiredBuildVersion returns the minimum accepted peer version gate
// for the given network.
//
// Example:
//
// MinimumRequiredBuildVersion(false) // 601 on mainnet
func MinimumRequiredBuildVersion(isTestnet bool) uint64 {
if isTestnet {
return MinimumRequiredBuildVersionTestnet
}
return MinimumRequiredBuildVersionMainnet
}
// PeerBuildVersion extracts the numeric major.minor.revision component from a
// daemon client version string.
//
// The daemon formats its version as "major.minor.revision.build[extra]".
// The minimum build gate compares the first three components, so
// "6.0.1.2[go-blockchain]" becomes 601.
func PeerBuildVersion(clientVersion string) (uint64, bool) {
parts := strings.SplitN(clientVersion, ".", 4)
if len(parts) < 3 {
return 0, false
}
major, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return 0, false
}
minor, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return 0, false
}
revPart := parts[2]
for i := 0; i < len(revPart); i++ {
if revPart[i] < '0' || revPart[i] > '9' {
revPart = revPart[:i]
break
}
}
if revPart == "" {
return 0, false
}
revision, err := strconv.ParseUint(revPart, 10, 64)
if err != nil {
return 0, false
}
return major*100 + minor*10 + revision, true
}
// MeetsMinimumBuildVersion reports whether the peer's version is acceptable
// for the given network.
//
// Example:
//
// MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", false) // true
func MeetsMinimumBuildVersion(clientVersion string, isTestnet bool) bool {
buildVersion, ok := PeerBuildVersion(clientVersion)
if !ok {
return false
}
return buildVersion >= MinimumRequiredBuildVersion(isTestnet)
}

View file

@ -1,71 +0,0 @@
// 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 p2p
import "testing"
func TestPeerBuildVersion_Good(t *testing.T) {
tests := []struct {
name string
input string
want uint64
wantOK bool
}{
{"release", "6.0.1.2[go-blockchain]", 601, true},
{"two_digits", "12.3.4.5", 1234, true},
{"suffix", "6.0.1-beta.2", 601, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := PeerBuildVersion(tt.input)
if ok != tt.wantOK {
t.Fatalf("PeerBuildVersion(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
}
if got != tt.want {
t.Fatalf("PeerBuildVersion(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
func TestPeerBuildVersion_Bad(t *testing.T) {
tests := []string{
"",
"6",
"6.0",
"abc.def.ghi",
}
for _, input := range tests {
t.Run(input, func(t *testing.T) {
if got, ok := PeerBuildVersion(input); ok || got != 0 {
t.Fatalf("PeerBuildVersion(%q) = (%d, %v), want (0, false)", input, got, ok)
}
})
}
}
func TestMeetsMinimumBuildVersion_Good(t *testing.T) {
if !MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", false) {
t.Fatal("expected mainnet build version to satisfy minimum")
}
if !MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", true) {
t.Fatal("expected testnet build version to satisfy minimum")
}
}
func TestMeetsMinimumBuildVersion_Bad(t *testing.T) {
if MeetsMinimumBuildVersion("0.0.1.0", false) {
t.Fatal("expected low mainnet build version to fail")
}
if MeetsMinimumBuildVersion("0.0.1.0", true) {
t.Fatal("expected low testnet build version to fail")
}
if MeetsMinimumBuildVersion("bogus", false) {
t.Fatal("expected malformed version to fail")
}
}

View file

@ -8,13 +8,14 @@ package blockchain
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
corelog "dappco.re/go/core/log"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/process"
@ -31,7 +32,7 @@ import (
// chain sync --stop
//
// It keeps the foreground and daemon modes behind a predictable command path.
func newChainSyncCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool) *cobra.Command {
func newChainSyncCommand(dataDir, seed *string, testnet *bool) *cobra.Command {
var (
daemon bool
stop bool
@ -41,21 +42,14 @@ func newChainSyncCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool
Use: "sync",
Short: "Headless P2P chain sync",
Long: "Sync the blockchain from P2P peers without the TUI explorer.",
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
if daemon && stop {
return corelog.E("newChainSyncCommand", "flags --daemon and --stop cannot be combined", nil)
}
return validateChainOptions(*chainDataDir, *seedPeerAddress)
},
RunE: func(cmd *cobra.Command, args []string) error {
if stop {
return stopChainSyncDaemon(*chainDataDir)
return stopChainSyncDaemon(*dataDir)
}
if daemon {
return runChainSyncDaemon(*chainDataDir, *seedPeerAddress, *useTestnet)
return runChainSyncDaemon(*dataDir, *seed, *testnet)
}
return runChainSyncForeground(*chainDataDir, *seedPeerAddress, *useTestnet)
return runChainSyncForeground(*dataDir, *seed, *testnet)
},
}
@ -65,36 +59,36 @@ func newChainSyncCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool
return cmd
}
func runChainSyncForeground(chainDataDir, seedPeerAddress string, useTestnet bool) error {
if err := ensureChainDataDirExists(chainDataDir); err != nil {
func runChainSyncForeground(dataDir, seed string, testnet bool) error {
if err := ensureChainDataDirExists(dataDir); err != nil {
return err
}
dbPath := filepath.Join(chainDataDir, "chain.db")
dbPath := filepath.Join(dataDir, "chain.db")
chainStore, err := store.New(dbPath)
if err != nil {
return corelog.E("runChainSyncForeground", "open store", err)
return coreerr.E("runChainSyncForeground", "open store", err)
}
defer chainStore.Close()
blockchain := chain.New(chainStore)
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
corelog.Info("starting headless P2P sync", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet)
log.Println("Starting headless P2P sync...")
runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed)
corelog.Info("headless P2P sync stopped", "data_dir", chainDataDir)
log.Println("Sync stopped.")
return nil
}
func runChainSyncDaemon(chainDataDir, seedPeerAddress string, useTestnet bool) error {
if err := ensureChainDataDirExists(chainDataDir); err != nil {
func runChainSyncDaemon(dataDir, seed string, testnet bool) error {
if err := ensureChainDataDirExists(dataDir); err != nil {
return err
}
pidFile := filepath.Join(chainDataDir, "sync.pid")
pidFile := filepath.Join(dataDir, "sync.pid")
daemon := process.NewDaemon(process.DaemonOptions{
PIDFile: pidFile,
@ -106,25 +100,25 @@ func runChainSyncDaemon(chainDataDir, seedPeerAddress string, useTestnet bool) e
})
if err := daemon.Start(); err != nil {
return corelog.E("runChainSyncDaemon", "daemon start", err)
return coreerr.E("runChainSyncDaemon", "daemon start", err)
}
dbPath := filepath.Join(chainDataDir, "chain.db")
dbPath := filepath.Join(dataDir, "chain.db")
chainStore, err := store.New(dbPath)
if err != nil {
_ = daemon.Stop()
return corelog.E("runChainSyncDaemon", "open store", err)
return coreerr.E("runChainSyncDaemon", "open store", err)
}
defer chainStore.Close()
blockchain := chain.New(chainStore)
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
daemon.SetReady(true)
corelog.Info("sync daemon started", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet)
log.Println("Sync daemon started.")
var wg sync.WaitGroup
wg.Add(1)
@ -138,22 +132,22 @@ func runChainSyncDaemon(chainDataDir, seedPeerAddress string, useTestnet bool) e
return err
}
func stopChainSyncDaemon(chainDataDir string) error {
pidFile := filepath.Join(chainDataDir, "sync.pid")
func stopChainSyncDaemon(dataDir string) error {
pidFile := filepath.Join(dataDir, "sync.pid")
pid, running := process.ReadPID(pidFile)
if pid == 0 || !running {
return corelog.E("stopChainSyncDaemon", "no running sync daemon found", nil)
return coreerr.E("stopChainSyncDaemon", "no running sync daemon found", nil)
}
processHandle, err := os.FindProcess(pid)
if err != nil {
return corelog.E("stopChainSyncDaemon", fmt.Sprintf("find process %d", pid), err)
return coreerr.E("stopChainSyncDaemon", fmt.Sprintf("find process %d", pid), err)
}
if err := processHandle.Signal(syscall.SIGTERM); err != nil {
return corelog.E("stopChainSyncDaemon", fmt.Sprintf("signal process %d", pid), err)
return coreerr.E("stopChainSyncDaemon", fmt.Sprintf("signal process %d", pid), err)
}
corelog.Info("sent SIGTERM to sync daemon", "pid", pid)
log.Printf("Sent SIGTERM to sync daemon (PID %d)", pid)
return nil
}

View file

@ -10,10 +10,11 @@ import (
"crypto/rand"
"encoding/binary"
"fmt"
"log"
"net"
"time"
corelog "dappco.re/go/core/log"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/blockchain/config"
@ -21,7 +22,7 @@ import (
levin "dappco.re/go/core/p2p/node/levin"
)
func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig *config.ChainConfig, hardForks []config.HardFork, seedPeerAddress string) {
func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig *config.ChainConfig, hardForks []config.HardFork, seed string) {
opts := chain.SyncOptions{
VerifySignatures: false,
Forks: hardForks,
@ -34,8 +35,8 @@ func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig
default:
}
if err := runChainSyncOnce(ctx, blockchain, chainConfig, opts, seedPeerAddress); err != nil {
corelog.Warn("sync failed, retrying in 10s", "error", err, "seed", seedPeerAddress)
if err := runChainSyncOnce(ctx, blockchain, chainConfig, opts, seed); err != nil {
log.Printf("sync: %v (retrying in 10s)", err)
select {
case <-ctx.Done():
return
@ -52,27 +53,22 @@ func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig
}
}
func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig *config.ChainConfig, opts chain.SyncOptions, seedPeerAddress string) error {
conn, err := net.DialTimeout("tcp", seedPeerAddress, 10*time.Second)
func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig *config.ChainConfig, opts chain.SyncOptions, seed string) error {
conn, err := net.DialTimeout("tcp", seed, 10*time.Second)
if err != nil {
return corelog.E("runChainSyncOnce", fmt.Sprintf("dial %s", seedPeerAddress), err)
return coreerr.E("runChainSyncOnce", fmt.Sprintf("dial %s", seed), err)
}
defer conn.Close()
p2pConn := levin.NewConnection(conn)
levinConn := levin.NewConnection(conn)
var peerIDBytes [8]byte
if _, err := rand.Read(peerIDBytes[:]); err != nil {
return corelog.E("runChainSyncOnce", "generate peer id", err)
}
rand.Read(peerIDBytes[:])
peerID := binary.LittleEndian.Uint64(peerIDBytes[:])
localHeight, err := blockchain.Height()
if err != nil {
return corelog.E("runChainSyncOnce", "get local height", err)
}
localHeight, _ := blockchain.Height()
handshakeRequest := p2p.HandshakeRequest{
handshakeReq := p2p.HandshakeRequest{
NodeData: p2p.NodeData{
NetworkID: chainConfig.NetworkID,
PeerID: peerID,
@ -85,37 +81,33 @@ func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig
NonPruningMode: true,
},
}
payload, err := p2p.EncodeHandshakeRequest(&handshakeRequest)
payload, err := p2p.EncodeHandshakeRequest(&handshakeReq)
if err != nil {
return corelog.E("runChainSyncOnce", "encode handshake", err)
return coreerr.E("runChainSyncOnce", "encode handshake", err)
}
if err := p2pConn.WritePacket(p2p.CommandHandshake, payload, true); err != nil {
return corelog.E("runChainSyncOnce", "write handshake", err)
if err := levinConn.WritePacket(p2p.CommandHandshake, payload, true); err != nil {
return coreerr.E("runChainSyncOnce", "write handshake", err)
}
packetHeader, packetData, err := p2pConn.ReadPacket()
hdr, data, err := levinConn.ReadPacket()
if err != nil {
return corelog.E("runChainSyncOnce", "read handshake", err)
return coreerr.E("runChainSyncOnce", "read handshake", err)
}
if packetHeader.Command != uint32(p2p.CommandHandshake) {
return corelog.E("runChainSyncOnce", fmt.Sprintf("unexpected command %d", packetHeader.Command), nil)
if hdr.Command != uint32(p2p.CommandHandshake) {
return coreerr.E("runChainSyncOnce", fmt.Sprintf("unexpected command %d", hdr.Command), nil)
}
var handshakeResponse p2p.HandshakeResponse
if err := handshakeResponse.Decode(packetData); err != nil {
return corelog.E("runChainSyncOnce", "decode handshake", err)
var handshakeResp p2p.HandshakeResponse
if err := handshakeResp.Decode(data); err != nil {
return coreerr.E("runChainSyncOnce", "decode handshake", err)
}
if err := p2p.ValidateHandshakeResponse(&handshakeResponse, chainConfig.NetworkID, chainConfig.IsTestnet); err != nil {
return corelog.E("runChainSyncOnce", "validate handshake", err)
}
localSyncData := p2p.CoreSyncData{
localSync := p2p.CoreSyncData{
CurrentHeight: localHeight,
ClientVersion: config.ClientVersion,
NonPruningMode: true,
}
p2pConnection := chain.NewLevinP2PConn(p2pConn, handshakeResponse.PayloadData.CurrentHeight, localSyncData)
p2pConn := chain.NewLevinP2PConn(levinConn, handshakeResp.PayloadData.CurrentHeight, localSync)
return blockchain.P2PSync(ctx, p2pConnection, opts)
return blockchain.P2PSync(ctx, p2pConn, opts)
}

View file

@ -309,19 +309,26 @@ func (m *ExplorerModel) viewTxDetail() string {
if len(tx.Vin) > 0 {
b.WriteString(" Inputs:\n")
for i, in := range tx.Vin {
b.WriteString(fmt.Sprintf(" [%d] %s\n", i, describeTxInput(in)))
switch v := in.(type) {
case types.TxInputGenesis:
b.WriteString(fmt.Sprintf(" [%d] coinbase height=%d\n", i, v.Height))
case types.TxInputToKey:
b.WriteString(fmt.Sprintf(" [%d] to_key amount=%d key_image=%x\n", i, v.Amount, v.KeyImage[:4]))
default:
b.WriteString(fmt.Sprintf(" [%d] %T\n", i, v))
}
}
}
if len(tx.Vout) > 0 {
b.WriteString("\n Outputs:\n")
for i, output := range tx.Vout {
switch v := output.(type) {
for i, out := range tx.Vout {
switch v := out.(type) {
case types.TxOutputBare:
if targetKey, ok := v.SpendKey(); ok {
b.WriteString(fmt.Sprintf(" [%d] bare amount=%d key=%x\n", i, v.Amount, targetKey[:4]))
if toKey, ok := v.Target.(types.TxOutToKey); ok {
b.WriteString(fmt.Sprintf(" [%d] bare amount=%d key=%x\n", i, v.Amount, toKey.Key[:4]))
} else {
b.WriteString(fmt.Sprintf(" [%d] bare amount=%d %s\n", i, v.Amount, describeTxOutTarget(v.Target)))
b.WriteString(fmt.Sprintf(" [%d] bare amount=%d target=%T\n", i, v.Amount, v.Target))
}
case types.TxOutputZarcanum:
b.WriteString(fmt.Sprintf(" [%d] zarcanum stealth=%x\n", i, v.StealthAddress[:4]))
@ -334,41 +341,6 @@ func (m *ExplorerModel) viewTxDetail() string {
return b.String()
}
// describeTxOutTarget renders a human-readable summary for non-to-key outputs.
func describeTxOutTarget(target types.TxOutTarget) string {
switch t := target.(type) {
case types.TxOutMultisig:
return fmt.Sprintf("multisig minimum_sigs=%d keys=%d", t.MinimumSigs, len(t.Keys))
case types.TxOutHTLC:
return fmt.Sprintf("htlc expiration=%d flags=%d redeem=%x refund=%x", t.Expiration, t.Flags, t.PKRedeem[:4], t.PKRefund[:4])
case types.TxOutToKey:
return fmt.Sprintf("to_key key=%x mix_attr=%d", t.Key[:4], t.MixAttr)
case nil:
return "target=<nil>"
default:
return fmt.Sprintf("target=%T", t)
}
}
// describeTxInput renders a human-readable summary for transaction inputs in
// the explorer tx detail view.
func describeTxInput(input types.TxInput) string {
switch v := input.(type) {
case types.TxInputGenesis:
return fmt.Sprintf("coinbase height=%d", v.Height)
case types.TxInputToKey:
return fmt.Sprintf("to_key amount=%d key_image=%x", v.Amount, v.KeyImage[:4])
case types.TxInputHTLC:
return fmt.Sprintf("htlc origin=%q amount=%d key_image=%x", v.HTLCOrigin, v.Amount, v.KeyImage[:4])
case types.TxInputMultisig:
return fmt.Sprintf("multisig amount=%d sigs=%d out=%x", v.Amount, v.SigsCount, v.MultisigOutID[:4])
case types.TxInputZC:
return fmt.Sprintf("zc inputs=%d key_image=%x", len(v.KeyOffsets), v.KeyImage[:4])
default:
return fmt.Sprintf("%T", v)
}
}
// loadBlocks refreshes the block list from the chain store.
// Blocks are listed from newest (top) to oldest.
func (m *ExplorerModel) loadBlocks() {

View file

@ -10,8 +10,6 @@ import (
"testing"
tea "github.com/charmbracelet/bubbletea"
"dappco.re/go/core/blockchain/types"
)
func TestExplorerModel_View_Good_BlockList(t *testing.T) {
@ -176,52 +174,3 @@ func TestExplorerModel_ViewBlockDetail_Good_CoinbaseOnly(t *testing.T) {
t.Errorf("block detail should contain 'coinbase only' for blocks with no TxHashes, got:\n%s", out)
}
}
func TestDescribeTxInput_Good(t *testing.T) {
tests := []struct {
name string
input types.TxInput
want string
}{
{
name: "genesis",
input: types.TxInputGenesis{Height: 12},
want: "coinbase height=12",
},
{
name: "to_key",
input: types.TxInputToKey{
Amount: 42,
KeyImage: types.KeyImage{0xaa, 0xbb, 0xcc, 0xdd},
},
want: "to_key amount=42 key_image=aabbccdd",
},
{
name: "htlc",
input: types.TxInputHTLC{
HTLCOrigin: "origin-hash",
Amount: 7,
KeyImage: types.KeyImage{0x10, 0x20, 0x30, 0x40},
},
want: `htlc origin="origin-hash" amount=7 key_image=10203040`,
},
{
name: "multisig",
input: types.TxInputMultisig{
Amount: 99,
SigsCount: 3,
MultisigOutID: types.Hash{0x01, 0x02, 0x03, 0x04},
},
want: "multisig amount=99 sigs=3 out=01020304",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := describeTxInput(tt.input)
if got != tt.want {
t.Fatalf("describeTxInput() = %q, want %q", got, tt.want)
}
})
}
}

View file

@ -31,9 +31,6 @@ type Address struct {
SpendPublicKey PublicKey
ViewPublicKey PublicKey
Flags uint8
// Prefix records the original base58 prefix when an address was decoded.
// It is optional for manually constructed addresses.
Prefix uint64 `json:"-"`
}
// IsAuditable reports whether the address has the auditable flag set.
@ -44,7 +41,11 @@ func (a *Address) IsAuditable() bool {
// IsIntegrated reports whether the given prefix corresponds to an integrated
// address type (standard integrated or auditable integrated).
func (a *Address) IsIntegrated() bool {
return IsIntegratedPrefix(a.Prefix)
// This method checks whether the address was decoded with an integrated
// prefix. Since we do not store the prefix in the Address struct, callers
// should use the prefix returned by DecodeAddress to determine this.
// This helper exists for convenience when the prefix is not available.
return false
}
// IsIntegratedPrefix reports whether the given prefix corresponds to an
@ -78,8 +79,6 @@ func (a *Address) Encode(prefix uint64) string {
// DecodeAddress parses a CryptoNote base58-encoded address string. It returns
// the decoded address, the prefix that was used, and any error.
//
// addr, prefix, err := types.DecodeAddress("iTHN6...")
func DecodeAddress(s string) (*Address, uint64, error) {
raw, err := base58Decode(s)
if err != nil {
@ -118,7 +117,6 @@ func DecodeAddress(s string) (*Address, uint64, error) {
copy(addr.SpendPublicKey[:], remaining[0:32])
copy(addr.ViewPublicKey[:], remaining[32:64])
addr.Flags = remaining[64]
addr.Prefix = prefix
return addr, prefix, nil
}

View file

@ -65,10 +65,6 @@ func TestAddressEncodeDecodeRoundTrip_Good(t *testing.T) {
if decoded.Flags != original.Flags {
t.Errorf("Flags mismatch: got 0x%02x, want 0x%02x", decoded.Flags, original.Flags)
}
if decoded.IsIntegrated() != IsIntegratedPrefix(tt.prefix) {
t.Errorf("IsIntegrated mismatch: got %v, want %v", decoded.IsIntegrated(), IsIntegratedPrefix(tt.prefix))
}
})
}
}
@ -107,27 +103,6 @@ func TestIsIntegratedPrefix_Good(t *testing.T) {
}
}
func TestAddressIsIntegrated_Good(t *testing.T) {
decoded, prefix, err := DecodeAddress(makeTestAddress(0x00).Encode(config.IntegratedAddressPrefix))
if err != nil {
t.Fatalf("DecodeAddress failed: %v", err)
}
if prefix != config.IntegratedAddressPrefix {
t.Fatalf("prefix mismatch: got 0x%x, want 0x%x", prefix, config.IntegratedAddressPrefix)
}
if !decoded.IsIntegrated() {
t.Fatal("decoded integrated address should report IsIntegrated() == true")
}
standard, _, err := DecodeAddress(makeTestAddress(0x00).Encode(config.AddressPrefix))
if err != nil {
t.Fatalf("DecodeAddress failed: %v", err)
}
if standard.IsIntegrated() {
t.Fatal("decoded standard address should report IsIntegrated() == false")
}
}
func TestDecodeAddress_Bad(t *testing.T) {
tests := []struct {
name string

View file

@ -1,143 +0,0 @@
// 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 types
import (
"fmt"
"unicode/utf8"
coreerr "dappco.re/go/core/log"
)
// AssetDescriptorOperationTag is the wire tag for asset_descriptor_operation
// extra variants.
const AssetDescriptorOperationTag uint8 = 40
// Asset operation types used by the HF5 asset_descriptor_operation variant.
const (
AssetOpRegister uint8 = 0 // deploy new asset
AssetOpEmit uint8 = 1 // emit additional supply
AssetOpUpdate uint8 = 2 // update asset metadata
AssetOpBurn uint8 = 3 // burn supply with proof
AssetOpPublicBurn uint8 = 4 // burn supply publicly
)
// AssetDescriptorBase holds the core asset metadata referenced by
// asset_descriptor_operation extra variants.
type AssetDescriptorBase struct {
Ticker string
FullName string
TotalMaxSupply uint64
CurrentSupply uint64
DecimalPoint uint8
MetaInfo string
OwnerKey PublicKey
Etc []byte
}
// Validate checks that the base asset metadata is structurally valid.
//
// base := types.AssetDescriptorBase{Ticker: "LTHN", FullName: "Lethean", TotalMaxSupply: 1_000_000, OwnerKey: ownerPub}
// if err := base.Validate(); err != nil { ... }
func (base AssetDescriptorBase) Validate() error {
tickerLen := utf8.RuneCountInString(base.Ticker)
fullNameLen := utf8.RuneCountInString(base.FullName)
if base.TotalMaxSupply == 0 {
return coreerr.E("AssetDescriptorBase.Validate", "total max supply must be non-zero", nil)
}
if base.CurrentSupply > base.TotalMaxSupply {
return coreerr.E("AssetDescriptorBase.Validate", fmt.Sprintf("current supply %d exceeds max supply %d", base.CurrentSupply, base.TotalMaxSupply), nil)
}
if tickerLen == 0 || tickerLen > 6 {
return coreerr.E("AssetDescriptorBase.Validate", fmt.Sprintf("ticker length %d out of range [1,6]", tickerLen), nil)
}
if fullNameLen == 0 || fullNameLen > 64 {
return coreerr.E("AssetDescriptorBase.Validate", fmt.Sprintf("full name length %d out of range [1,64]", fullNameLen), nil)
}
if base.OwnerKey.IsZero() {
return coreerr.E("AssetDescriptorBase.Validate", "owner key must be non-zero", nil)
}
return nil
}
// AssetDescriptorOperation represents a deploy/emit/update/burn operation.
// The wire format is parsed in wire/ as an opaque blob for round-tripping.
type AssetDescriptorOperation struct {
Version uint8
OperationType uint8
Descriptor *AssetDescriptorBase
AssetID Hash
AmountToEmit uint64
AmountToBurn uint64
Etc []byte
}
// Validate checks that the operation is structurally valid for HF5 parsing.
//
// op := types.AssetDescriptorOperation{Version: 1, OperationType: types.AssetOpRegister, Descriptor: &base}
// if err := op.Validate(); err != nil { ... }
func (op AssetDescriptorOperation) Validate() error {
switch op.Version {
case 0, 1:
default:
return coreerr.E("AssetDescriptorOperation.Validate", fmt.Sprintf("unsupported version %d", op.Version), nil)
}
switch op.OperationType {
case AssetOpRegister:
if !op.AssetID.IsZero() {
return coreerr.E("AssetDescriptorOperation.Validate", "register operation must not carry asset id", nil)
}
if op.Descriptor == nil {
return coreerr.E("AssetDescriptorOperation.Validate", "register operation missing descriptor", nil)
}
if err := op.Descriptor.Validate(); err != nil {
return err
}
if op.AmountToEmit != 0 || op.AmountToBurn != 0 {
return coreerr.E("AssetDescriptorOperation.Validate", "register operation must not include emission or burn amounts", nil)
}
case AssetOpEmit:
if op.AssetID.IsZero() {
return coreerr.E("AssetDescriptorOperation.Validate", "emit operation must carry asset id", nil)
}
if op.AmountToEmit == 0 {
return coreerr.E("AssetDescriptorOperation.Validate", "emit operation has zero amount", nil)
}
if op.Descriptor != nil {
return coreerr.E("AssetDescriptorOperation.Validate", "emit operation must not carry descriptor", nil)
}
case AssetOpUpdate:
if op.AssetID.IsZero() {
return coreerr.E("AssetDescriptorOperation.Validate", "update operation must carry asset id", nil)
}
if op.Descriptor == nil {
return coreerr.E("AssetDescriptorOperation.Validate", "update operation missing descriptor", nil)
}
if err := op.Descriptor.Validate(); err != nil {
return err
}
if op.AmountToEmit != 0 || op.AmountToBurn != 0 {
return coreerr.E("AssetDescriptorOperation.Validate", "update operation must not include emission or burn amounts", nil)
}
case AssetOpBurn, AssetOpPublicBurn:
if op.AssetID.IsZero() {
return coreerr.E("AssetDescriptorOperation.Validate", "burn operation must carry asset id", nil)
}
if op.AmountToBurn == 0 {
return coreerr.E("AssetDescriptorOperation.Validate", "burn operation has zero amount", nil)
}
if op.Descriptor != nil {
return coreerr.E("AssetDescriptorOperation.Validate", "burn operation must not carry descriptor", nil)
}
default:
return coreerr.E("AssetDescriptorOperation.Validate", fmt.Sprintf("unsupported operation type %d", op.OperationType), nil)
}
return nil
}

View file

@ -1,140 +0,0 @@
// 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 types
import "testing"
func TestAssetOperationConstants_Good(t *testing.T) {
tests := []struct {
name string
got uint8
want uint8
}{
{"register", AssetOpRegister, 0},
{"emit", AssetOpEmit, 1},
{"update", AssetOpUpdate, 2},
{"burn", AssetOpBurn, 3},
{"public_burn", AssetOpPublicBurn, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("got %d, want %d", tt.got, tt.want)
}
})
}
}
func TestAssetDescriptorTypes_Good(t *testing.T) {
base := AssetDescriptorBase{
Ticker: "LTHN",
FullName: "Lethean",
TotalMaxSupply: 1000000,
CurrentSupply: 0,
DecimalPoint: 12,
MetaInfo: "{}",
OwnerKey: PublicKey{1},
Etc: []byte{1, 2, 3},
}
op := AssetDescriptorOperation{
Version: 1,
OperationType: AssetOpRegister,
Descriptor: &base,
AssetID: Hash{1},
AmountToEmit: 100,
AmountToBurn: 10,
Etc: []byte{4, 5, 6},
}
if op.Descriptor == nil || op.Descriptor.Ticker != "LTHN" {
t.Fatalf("unexpected descriptor: %+v", op.Descriptor)
}
if op.OperationType != AssetOpRegister || op.Version != 1 {
t.Fatalf("unexpected operation: %+v", op)
}
}
func TestAssetDescriptorBaseValidate_Good(t *testing.T) {
base := AssetDescriptorBase{
Ticker: "LTHN",
FullName: "Lethean",
TotalMaxSupply: 1000000,
CurrentSupply: 0,
DecimalPoint: 12,
MetaInfo: "{}",
OwnerKey: PublicKey{1},
}
if err := base.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
func TestAssetDescriptorBaseValidate_Bad(t *testing.T) {
tests := []struct {
name string
base AssetDescriptorBase
}{
{"zero_supply", AssetDescriptorBase{Ticker: "LTHN", FullName: "Lethean", OwnerKey: PublicKey{1}}},
{"too_many_current", AssetDescriptorBase{Ticker: "LTHN", FullName: "Lethean", TotalMaxSupply: 10, CurrentSupply: 11, OwnerKey: PublicKey{1}}},
{"empty_ticker", AssetDescriptorBase{FullName: "Lethean", TotalMaxSupply: 10, OwnerKey: PublicKey{1}}},
{"empty_name", AssetDescriptorBase{Ticker: "LTHN", TotalMaxSupply: 10, OwnerKey: PublicKey{1}}},
{"zero_owner", AssetDescriptorBase{Ticker: "LTHN", FullName: "Lethean", TotalMaxSupply: 10}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.base.Validate(); err == nil {
t.Fatal("Validate() error = nil, want error")
}
})
}
}
func TestAssetDescriptorOperationValidate_Good(t *testing.T) {
base := AssetDescriptorBase{
Ticker: "LTHN",
FullName: "Lethean",
TotalMaxSupply: 1000000,
CurrentSupply: 0,
DecimalPoint: 12,
MetaInfo: "{}",
OwnerKey: PublicKey{1},
}
op := AssetDescriptorOperation{
Version: 1,
OperationType: AssetOpRegister,
Descriptor: &base,
}
if err := op.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
func TestAssetDescriptorOperationValidate_Bad(t *testing.T) {
tests := []struct {
name string
op AssetDescriptorOperation
}{
{"unsupported_version", AssetDescriptorOperation{Version: 2, OperationType: AssetOpRegister}},
{"register_missing_descriptor", AssetDescriptorOperation{Version: 1, OperationType: AssetOpRegister}},
{"emit_zero_amount", AssetDescriptorOperation{Version: 1, OperationType: AssetOpEmit, AssetID: Hash{1}}},
{"update_missing_descriptor", AssetDescriptorOperation{Version: 1, OperationType: AssetOpUpdate, AssetID: Hash{1}}},
{"burn_zero_amount", AssetDescriptorOperation{Version: 1, OperationType: AssetOpBurn, AssetID: Hash{1}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.op.Validate(); err == nil {
t.Fatal("Validate() error = nil, want error")
}
})
}
}

View file

@ -121,15 +121,6 @@ type TxOutTarget interface {
TargetType() uint8
}
// AsTxOutToKey returns target as a TxOutToKey when it is a standard
// transparent output target.
//
// toKey, ok := types.AsTxOutToKey(bare.Target)
func AsTxOutToKey(target TxOutTarget) (TxOutToKey, bool) {
v, ok := target.(TxOutToKey)
return v, ok
}
// TxOutToKey is the txout_to_key target variant. On the wire it is
// serialised as a 33-byte packed blob: 32-byte public key + 1-byte mix_attr.
type TxOutToKey struct {
@ -228,7 +219,7 @@ func (t TxInputHTLC) InputType() uint8 { return InputTypeHTLC }
// TxInputMultisig spends from a multisig output (HF1+).
type TxInputMultisig struct {
Amount uint64
MultisigOutID Hash // 32-byte hash identifying the multisig output
MultisigOutID Hash // 32-byte hash identifying the multisig output
SigsCount uint64
EtcDetails []byte // opaque variant vector
}
@ -246,19 +237,6 @@ type TxOutputBare struct {
Target TxOutTarget
}
// SpendKey returns the standard transparent spend key when the target is
// TxOutToKey. Callers that only care about transparent key outputs can use
// this instead of repeating a type assertion.
//
// if key, ok := bareOutput.SpendKey(); ok { /* use key */ }
func (t TxOutputBare) SpendKey() (PublicKey, bool) {
target, ok := AsTxOutToKey(t.Target)
if !ok {
return PublicKey{}, false
}
return target.Key, true
}
// OutputType returns the wire variant tag for bare outputs.
func (t TxOutputBare) OutputType() uint8 { return OutputTypeBare }

View file

@ -14,46 +14,6 @@ func TestTxOutToKey_TargetType_Good(t *testing.T) {
}
}
func TestAsTxOutToKey_Good(t *testing.T) {
target, ok := AsTxOutToKey(TxOutToKey{Key: PublicKey{1}, MixAttr: 7})
if !ok {
t.Fatal("AsTxOutToKey: expected true for TxOutToKey target")
}
if target.Key != (PublicKey{1}) {
t.Errorf("Key: got %x, want %x", target.Key, PublicKey{1})
}
if target.MixAttr != 7 {
t.Errorf("MixAttr: got %d, want %d", target.MixAttr, 7)
}
}
func TestAsTxOutToKey_Bad(t *testing.T) {
if _, ok := AsTxOutToKey(TxOutHTLC{}); ok {
t.Fatal("AsTxOutToKey: expected false for non-to-key target")
}
}
func TestTxOutputBare_SpendKey_Good(t *testing.T) {
out := TxOutputBare{
Amount: 10,
Target: TxOutToKey{Key: PublicKey{0xAA, 0xBB}, MixAttr: 3},
}
key, ok := out.SpendKey()
if !ok {
t.Fatal("SpendKey: expected true for TxOutToKey target")
}
if key != (PublicKey{0xAA, 0xBB}) {
t.Errorf("SpendKey: got %x, want %x", key, PublicKey{0xAA, 0xBB})
}
}
func TestTxOutputBare_SpendKey_Bad(t *testing.T) {
out := TxOutputBare{Target: TxOutHTLC{Expiration: 100}}
if key, ok := out.SpendKey(); ok || key != (PublicKey{}) {
t.Fatalf("SpendKey: got (%x, %v), want zero key and false", key, ok)
}
}
func TestTxOutMultisig_TargetType_Good(t *testing.T) {
var target TxOutTarget = TxOutMultisig{MinimumSigs: 2, Keys: []PublicKey{{1}, {2}}}
if target.TargetType() != TargetTypeMultisig {

View file

@ -23,7 +23,6 @@ import (
store "dappco.re/go/core/store"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/crypto"
"dappco.re/go/core/blockchain/types"
)
@ -108,7 +107,6 @@ func (a *Account) Address() types.Address {
return types.Address{
SpendPublicKey: a.SpendPublicKey,
ViewPublicKey: a.ViewPublicKey,
Prefix: config.AddressPrefix,
}
}

View file

@ -52,33 +52,33 @@ func (s *V1Scanner) ScanTransaction(tx *types.Transaction, txHash types.Hash,
isCoinbase := len(tx.Vin) > 0 && tx.Vin[0].InputType() == types.InputTypeGenesis
var transfers []Transfer
for i, output := range tx.Vout {
bare, ok := output.(types.TxOutputBare)
for i, out := range tx.Vout {
bare, ok := out.(types.TxOutputBare)
if !ok {
continue
}
expectedPublicKey, err := crypto.DerivePublicKey(
expectedPub, err := crypto.DerivePublicKey(
derivation, uint64(i), [32]byte(s.account.SpendPublicKey))
if err != nil {
continue
}
targetKey, ok := bare.SpendKey()
toKey, ok := bare.Target.(types.TxOutToKey)
if !ok {
continue
}
if types.PublicKey(expectedPublicKey) != targetKey {
if types.PublicKey(expectedPub) != toKey.Key {
continue
}
ephemeralSecretKey, err := crypto.DeriveSecretKey(
ephSec, err := crypto.DeriveSecretKey(
derivation, uint64(i), [32]byte(s.account.SpendSecretKey))
if err != nil {
continue
}
keyImage, err := crypto.GenerateKeyImage(expectedPublicKey, ephemeralSecretKey)
ki, err := crypto.GenerateKeyImage(expectedPub, ephSec)
if err != nil {
continue
}
@ -89,10 +89,10 @@ func (s *V1Scanner) ScanTransaction(tx *types.Transaction, txHash types.Hash,
Amount: bare.Amount,
BlockHeight: blockHeight,
EphemeralKey: KeyPair{
Public: types.PublicKey(expectedPublicKey),
Secret: types.SecretKey(ephemeralSecretKey),
Public: types.PublicKey(expectedPub),
Secret: types.SecretKey(ephSec),
},
KeyImage: types.KeyImage(keyImage),
KeyImage: types.KeyImage(ki),
Coinbase: isCoinbase,
UnlockTime: extra.UnlockTime,
})

View file

@ -122,23 +122,17 @@ func (w *Wallet) scanTx(tx *types.Transaction, blockHeight uint64) error {
// Check key images for spend detection.
for _, vin := range tx.Vin {
var keyImage types.KeyImage
switch v := vin.(type) {
case types.TxInputToKey:
keyImage = v.KeyImage
case types.TxInputHTLC:
keyImage = v.KeyImage
default:
toKey, ok := vin.(types.TxInputToKey)
if !ok {
continue
}
// Try to mark any matching transfer as spent.
tr, err := getTransfer(w.store, keyImage)
tr, err := getTransfer(w.store, toKey.KeyImage)
if err != nil {
continue // not our transfer
}
if !tr.Spent {
markTransferSpent(w.store, keyImage, blockHeight)
markTransferSpent(w.store, toKey.KeyImage, blockHeight)
}
}

View file

@ -12,11 +12,11 @@ package wallet
import (
"testing"
store "dappco.re/go/core/store"
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/blockchain/crypto"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
store "dappco.re/go/core/store"
)
func makeTestBlock(t *testing.T, height uint64, prevHash types.Hash,
@ -133,55 +133,3 @@ func TestWalletTransfers(t *testing.T) {
t.Fatalf("got %d transfers, want 1", len(transfers))
}
}
func TestWalletScanTxMarksHTLCSpend(t *testing.T) {
s, err := store.New(":memory:")
if err != nil {
t.Fatal(err)
}
defer s.Close()
acc, err := GenerateAccount()
if err != nil {
t.Fatal(err)
}
ki := types.KeyImage{0x42}
if err := putTransfer(s, &Transfer{
KeyImage: ki,
Amount: 100,
BlockHeight: 1,
}); err != nil {
t.Fatal(err)
}
w := &Wallet{
store: s,
scanner: NewV1Scanner(acc),
}
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputHTLC{
Amount: 100,
KeyImage: ki,
},
},
}
if err := w.scanTx(tx, 10); err != nil {
t.Fatal(err)
}
got, err := getTransfer(s, ki)
if err != nil {
t.Fatal(err)
}
if !got.Spent {
t.Fatal("expected HTLC spend to be marked spent")
}
if got.SpentHeight != 10 {
t.Fatalf("spent height = %d, want 10", got.SpentHeight)
}
}

View file

@ -44,8 +44,6 @@ func BlockHashingBlob(b *types.Block) []byte {
// varint length prefix, so the actual hash input is:
//
// varint(len(blob)) || blob
//
// blockID := wire.BlockHash(&blk)
func BlockHash(b *types.Block) types.Hash {
blob := BlockHashingBlob(b)
var prefixed []byte
@ -60,8 +58,6 @@ func BlockHash(b *types.Block) types.Hash {
// get_transaction_prefix_hash for all versions. The tx_id is always
// Keccak-256 of the serialised prefix (version + inputs + outputs + extra,
// in version-dependent field order).
//
// txID := wire.TransactionHash(&tx)
func TransactionHash(tx *types.Transaction) types.Hash {
return TransactionPrefixHash(tx)
}
@ -69,8 +65,6 @@ func TransactionHash(tx *types.Transaction) types.Hash {
// TransactionPrefixHash computes the hash of a transaction prefix.
// This is Keccak-256 of the serialised transaction prefix (version + vin +
// vout + extra, in version-dependent order).
//
// prefixHash := wire.TransactionPrefixHash(&tx)
func TransactionPrefixHash(tx *types.Transaction) types.Hash {
var buf bytes.Buffer
enc := NewEncoder(&buf)

View file

@ -176,9 +176,6 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) {
encodeKeyOffsets(enc, v.KeyOffsets)
enc.WriteBlob32((*[32]byte)(&v.KeyImage))
enc.WriteBytes(v.EtcDetails)
default:
enc.err = coreerr.E("encodeInputs", fmt.Sprintf("wire: unsupported input type %T", in), nil)
return
}
}
}
@ -271,63 +268,6 @@ func decodeKeyOffsets(dec *Decoder) []types.TxOutRef {
return refs
}
func encodeTxOutTarget(enc *Encoder, target types.TxOutTarget, context string) bool {
switch t := target.(type) {
case types.TxOutToKey:
enc.WriteVariantTag(types.TargetTypeToKey)
enc.WriteBlob32((*[32]byte)(&t.Key))
enc.WriteUint8(t.MixAttr)
case types.TxOutMultisig:
enc.WriteVariantTag(types.TargetTypeMultisig)
enc.WriteVarint(t.MinimumSigs)
enc.WriteVarint(uint64(len(t.Keys)))
for i := range t.Keys {
enc.WriteBlob32((*[32]byte)(&t.Keys[i]))
}
case types.TxOutHTLC:
enc.WriteVariantTag(types.TargetTypeHTLC)
enc.WriteBlob32((*[32]byte)(&t.HTLCHash))
enc.WriteUint8(t.Flags)
enc.WriteVarint(t.Expiration)
enc.WriteBlob32((*[32]byte)(&t.PKRedeem))
enc.WriteBlob32((*[32]byte)(&t.PKRefund))
default:
enc.err = coreerr.E(context, fmt.Sprintf("wire: unsupported output target type %T", target), nil)
return false
}
return true
}
func decodeTxOutTarget(dec *Decoder, tag uint8, context string) types.TxOutTarget {
switch tag {
case types.TargetTypeToKey:
var t types.TxOutToKey
dec.ReadBlob32((*[32]byte)(&t.Key))
t.MixAttr = dec.ReadUint8()
return t
case types.TargetTypeMultisig:
var t types.TxOutMultisig
t.MinimumSigs = dec.ReadVarint()
keyCount := dec.ReadVarint()
t.Keys = make([]types.PublicKey, keyCount)
for i := uint64(0); i < keyCount; i++ {
dec.ReadBlob32((*[32]byte)(&t.Keys[i]))
}
return t
case types.TargetTypeHTLC:
var t types.TxOutHTLC
dec.ReadBlob32((*[32]byte)(&t.HTLCHash))
t.Flags = dec.ReadUint8()
t.Expiration = dec.ReadVarint()
dec.ReadBlob32((*[32]byte)(&t.PKRedeem))
dec.ReadBlob32((*[32]byte)(&t.PKRefund))
return t
default:
dec.err = coreerr.E(context, fmt.Sprintf("wire: unsupported target tag 0x%02x", tag), nil)
return nil
}
}
// --- outputs ---
// encodeOutputsV1 serialises v0/v1 outputs. In v0/v1, outputs are tx_out_bare
@ -339,12 +279,26 @@ func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) {
case types.TxOutputBare:
enc.WriteVarint(v.Amount)
// Target is a variant (txout_target_v)
if !encodeTxOutTarget(enc, v.Target, "encodeOutputsV1") {
return
switch t := v.Target.(type) {
case types.TxOutToKey:
enc.WriteVariantTag(types.TargetTypeToKey)
enc.WriteBlob32((*[32]byte)(&t.Key))
enc.WriteUint8(t.MixAttr)
case types.TxOutMultisig:
enc.WriteVariantTag(types.TargetTypeMultisig)
enc.WriteVarint(t.MinimumSigs)
enc.WriteVarint(uint64(len(t.Keys)))
for k := range t.Keys {
enc.WriteBlob32((*[32]byte)(&t.Keys[k]))
}
case types.TxOutHTLC:
enc.WriteVariantTag(types.TargetTypeHTLC)
enc.WriteBlob32((*[32]byte)(&t.HTLCHash))
enc.WriteUint8(t.Flags)
enc.WriteVarint(t.Expiration)
enc.WriteBlob32((*[32]byte)(&t.PKRedeem))
enc.WriteBlob32((*[32]byte)(&t.PKRefund))
}
default:
enc.err = coreerr.E("encodeOutputsV1", fmt.Sprintf("wire: unsupported output type %T", out), nil)
return
}
}
}
@ -362,8 +316,31 @@ func decodeOutputsV1(dec *Decoder) []types.TxOutput {
if dec.Err() != nil {
return vout
}
out.Target = decodeTxOutTarget(dec, tag, "decodeOutputsV1")
if dec.Err() != nil {
switch tag {
case types.TargetTypeToKey:
var t types.TxOutToKey
dec.ReadBlob32((*[32]byte)(&t.Key))
t.MixAttr = dec.ReadUint8()
out.Target = t
case types.TargetTypeMultisig:
var t types.TxOutMultisig
t.MinimumSigs = dec.ReadVarint()
keyCount := dec.ReadVarint()
t.Keys = make([]types.PublicKey, keyCount)
for k := uint64(0); k < keyCount; k++ {
dec.ReadBlob32((*[32]byte)(&t.Keys[k]))
}
out.Target = t
case types.TargetTypeHTLC:
var t types.TxOutHTLC
dec.ReadBlob32((*[32]byte)(&t.HTLCHash))
t.Flags = dec.ReadUint8()
t.Expiration = dec.ReadVarint()
dec.ReadBlob32((*[32]byte)(&t.PKRedeem))
dec.ReadBlob32((*[32]byte)(&t.PKRefund))
out.Target = t
default:
dec.err = coreerr.E("decodeOutputsV1", fmt.Sprintf("wire: unsupported target tag 0x%02x", tag), nil)
return vout
}
vout = append(vout, out)
@ -379,8 +356,25 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) {
switch v := out.(type) {
case types.TxOutputBare:
enc.WriteVarint(v.Amount)
if !encodeTxOutTarget(enc, v.Target, "encodeOutputsV2") {
return
switch t := v.Target.(type) {
case types.TxOutToKey:
enc.WriteVariantTag(types.TargetTypeToKey)
enc.WriteBlob32((*[32]byte)(&t.Key))
enc.WriteUint8(t.MixAttr)
case types.TxOutMultisig:
enc.WriteVariantTag(types.TargetTypeMultisig)
enc.WriteVarint(t.MinimumSigs)
enc.WriteVarint(uint64(len(t.Keys)))
for k := range t.Keys {
enc.WriteBlob32((*[32]byte)(&t.Keys[k]))
}
case types.TxOutHTLC:
enc.WriteVariantTag(types.TargetTypeHTLC)
enc.WriteBlob32((*[32]byte)(&t.HTLCHash))
enc.WriteUint8(t.Flags)
enc.WriteVarint(t.Expiration)
enc.WriteBlob32((*[32]byte)(&t.PKRedeem))
enc.WriteBlob32((*[32]byte)(&t.PKRefund))
}
case types.TxOutputZarcanum:
enc.WriteBlob32((*[32]byte)(&v.StealthAddress))
@ -389,9 +383,6 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) {
enc.WriteBlob32((*[32]byte)(&v.BlindedAssetID))
enc.WriteUint64LE(v.EncryptedAmount)
enc.WriteUint8(v.MixAttr)
default:
enc.err = coreerr.E("encodeOutputsV2", fmt.Sprintf("wire: unsupported output type %T", out), nil)
return
}
}
}
@ -412,8 +403,31 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput {
var out types.TxOutputBare
out.Amount = dec.ReadVarint()
targetTag := dec.ReadVariantTag()
out.Target = decodeTxOutTarget(dec, targetTag, "decodeOutputsV2")
if dec.Err() != nil {
switch targetTag {
case types.TargetTypeToKey:
var t types.TxOutToKey
dec.ReadBlob32((*[32]byte)(&t.Key))
t.MixAttr = dec.ReadUint8()
out.Target = t
case types.TargetTypeMultisig:
var t types.TxOutMultisig
t.MinimumSigs = dec.ReadVarint()
keyCount := dec.ReadVarint()
t.Keys = make([]types.PublicKey, keyCount)
for k := uint64(0); k < keyCount; k++ {
dec.ReadBlob32((*[32]byte)(&t.Keys[k]))
}
out.Target = t
case types.TargetTypeHTLC:
var t types.TxOutHTLC
dec.ReadBlob32((*[32]byte)(&t.HTLCHash))
t.Flags = dec.ReadUint8()
t.Expiration = dec.ReadVarint()
dec.ReadBlob32((*[32]byte)(&t.PKRedeem))
dec.ReadBlob32((*[32]byte)(&t.PKRefund))
out.Target = t
default:
dec.err = coreerr.E("decodeOutputsV2", fmt.Sprintf("wire: unsupported target tag 0x%02x", targetTag), nil)
return vout
}
vout = append(vout, out)
@ -517,10 +531,10 @@ const (
tagZarcanumSig = 45 // zarcanum_sig — complex
// Asset operation tags (HF5 confidential assets).
tagAssetDescriptorOperation = types.AssetDescriptorOperationTag // asset_descriptor_operation
tagAssetOperationProof = 49 // asset_operation_proof
tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof
tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth
tagAssetDescriptorOperation = 40 // asset_descriptor_operation
tagAssetOperationProof = 49 // asset_operation_proof
tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof
tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth
// Proof variant tags (proof_v).
tagZCAssetSurjectionProof = 46 // vector<BGE_proof_s>
@ -572,12 +586,6 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
case tagTxPayer, tagTxReceiver:
return readTxPayer(dec)
// Alias types
case tagExtraAliasEntryOld:
return readExtraAliasEntryOld(dec)
case tagExtraAliasEntry:
return readExtraAliasEntry(dec)
// Composite types
case tagExtraAttachmentInfo:
return readExtraAttachmentInfo(dec)
@ -786,96 +794,6 @@ func readTxServiceAttachment(dec *Decoder) []byte {
return raw
}
// readExtraAliasEntryOld reads extra_alias_entry_old (tag 20).
// Structure: alias(string) + address(spend_key(32) + view_key(32)) +
// text_comment(string) + sign(vector of generic_schnorr_sig_s, each 64 bytes).
func readExtraAliasEntryOld(dec *Decoder) []byte {
var raw []byte
// m_alias: string
alias := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, alias...)
// m_address: spend_public_key(32) + view_public_key(32) = 64 bytes
addr := dec.ReadBytes(64)
if dec.err != nil {
return nil
}
raw = append(raw, addr...)
// m_text_comment: string
comment := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, comment...)
// m_sign: vector<generic_schnorr_sig_s> (each is 2 scalars = 64 bytes)
v := readVariantVectorFixed(dec, 64)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
return raw
}
// readExtraAliasEntry reads extra_alias_entry (tag 33).
// Structure: alias(string) + address(spend_key(32) + view_key(32) + optional flag) +
// text_comment(string) + sign(vector of generic_schnorr_sig_s, each 64 bytes) +
// view_key(optional secret_key, 32 bytes).
func readExtraAliasEntry(dec *Decoder) []byte {
var raw []byte
// m_alias: string
alias := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, alias...)
// m_address: account_public_address with optional is_auditable flag
// Same wire format as tx_payer (tag 31): spend_key(32) + view_key(32) + optional
addr := readTxPayer(dec)
if dec.err != nil {
return nil
}
raw = append(raw, addr...)
// m_text_comment: string
comment := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, comment...)
// m_sign: vector<generic_schnorr_sig_s> (each is 2 scalars = 64 bytes)
v := readVariantVectorFixed(dec, 64)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
// m_view_key: optional<crypto::secret_key> — uint8 marker + 32 bytes if present
marker := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, marker)
if marker != 0 {
key := dec.ReadBytes(32)
if dec.err != nil {
return nil
}
raw = append(raw, key...)
}
return raw
}
// readSignedParts reads signed_parts (tag 17).
// Structure: n_outs (varint) + n_extras (varint).
func readSignedParts(dec *Decoder) []byte {
@ -1125,7 +1043,113 @@ func readZarcanumSig(dec *Decoder) []byte {
// decimal_point(uint8) + meta_info(string) + owner_key(32 bytes) +
// etc(vector<uint8>).
func readAssetDescriptorOperation(dec *Decoder) []byte {
raw, _ := parseAssetDescriptorOperation(dec)
var raw []byte
// ver: uint8
ver := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, ver)
// operation_type: uint8
opType := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, opType)
// opt_asset_id: uint8 presence marker + 32 bytes if present
assetMarker := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, assetMarker)
if assetMarker != 0 {
b := dec.ReadBytes(32)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
}
// opt_descriptor: uint8 presence marker + descriptor if present
descMarker := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, descMarker)
if descMarker != 0 {
// AssetDescriptorBase
// ticker: string
s := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, s...)
// full_name: string
s = readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, s...)
// total_max_supply: uint64 LE
b := dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// current_supply: uint64 LE
b = dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// decimal_point: uint8
dp := dec.ReadUint8()
if dec.err != nil {
return nil
}
raw = append(raw, dp)
// meta_info: string
s = readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, s...)
// owner_key: 32 bytes
b = dec.ReadBytes(32)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// etc: vector<uint8>
v := readVariantVectorFixed(dec, 1)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
}
// amount_to_emit: uint64 LE
b := dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// amount_to_burn: uint64 LE
b = dec.ReadBytes(8)
if dec.err != nil {
return nil
}
raw = append(raw, b...)
// etc: vector<uint8>
v := readVariantVectorFixed(dec, 1)
if dec.err != nil {
return nil
}
raw = append(raw, v...)
return raw
}

View file

@ -987,225 +987,3 @@ func TestHTLCTargetV2RoundTrip_Good(t *testing.T) {
t.Errorf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), buf.Bytes())
}
}
type unsupportedTxInput struct{}
func (unsupportedTxInput) InputType() uint8 { return 250 }
type unsupportedTxOutTarget struct{}
func (unsupportedTxOutTarget) TargetType() uint8 { return 250 }
type unsupportedTxOutput struct{}
func (unsupportedTxOutput) OutputType() uint8 { return 250 }
func TestEncodeTransaction_UnsupportedInput_Bad(t *testing.T) {
tx := types.Transaction{
Version: 1,
Vin: []types.TxInput{unsupportedTxInput{}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 1,
Target: types.TxOutToKey{Key: types.PublicKey{1}},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() == nil {
t.Fatal("expected encode error for unsupported input type")
}
}
func TestEncodeTransaction_UnsupportedOutputTarget_Bad(t *testing.T) {
tests := []struct {
name string
version uint64
}{
{name: "v1", version: types.VersionPreHF4},
{name: "v2", version: types.VersionPostHF4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx := types.Transaction{
Version: tt.version,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 1,
Target: unsupportedTxOutTarget{},
}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() == nil {
t.Fatal("expected encode error for unsupported output target type")
}
})
}
}
func TestEncodeTransaction_UnsupportedOutputType_Bad(t *testing.T) {
tests := []struct {
name string
version uint64
}{
{name: "v1", version: types.VersionPreHF4},
{name: "v2", version: types.VersionPostHF4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx := types.Transaction{
Version: tt.version,
Vin: []types.TxInput{types.TxInputGenesis{Height: 1}},
Vout: []types.TxOutput{unsupportedTxOutput{}},
Extra: EncodeVarint(0),
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeTransactionPrefix(enc, &tx)
if enc.Err() == nil {
t.Fatal("expected encode error for unsupported output type")
}
})
}
}
// TestExtraAliasEntryOldRoundTrip_Good verifies that a variant vector
// containing an extra_alias_entry_old (tag 20) round-trips through
// decodeRawVariantVector without error.
func TestExtraAliasEntryOldRoundTrip_Good(t *testing.T) {
// Build a synthetic variant vector with one extra_alias_entry_old element.
// Format: count(1) + tag(20) + alias(string) + address(64 bytes) +
// text_comment(string) + sign(vector of 64-byte sigs).
var raw []byte
raw = append(raw, EncodeVarint(1)...) // 1 element
raw = append(raw, tagExtraAliasEntryOld)
// m_alias: "test.lthn"
alias := []byte("test.lthn")
raw = append(raw, EncodeVarint(uint64(len(alias)))...)
raw = append(raw, alias...)
// m_address: spend_key(32) + view_key(32) = 64 bytes
addr := make([]byte, 64)
for i := range addr {
addr[i] = byte(i)
}
raw = append(raw, addr...)
// m_text_comment: "hello"
comment := []byte("hello")
raw = append(raw, EncodeVarint(uint64(len(comment)))...)
raw = append(raw, comment...)
// m_sign: 1 signature (generic_schnorr_sig_s = 64 bytes)
raw = append(raw, EncodeVarint(1)...) // 1 signature
sig := make([]byte, 64)
for i := range sig {
sig[i] = byte(0xAA)
}
raw = append(raw, sig...)
// Decode and round-trip.
dec := NewDecoder(bytes.NewReader(raw))
decoded := decodeRawVariantVector(dec)
if dec.Err() != nil {
t.Fatalf("decode failed: %v", dec.Err())
}
if !bytes.Equal(decoded, raw) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw))
}
}
// TestExtraAliasEntryRoundTrip_Good verifies that a variant vector
// containing an extra_alias_entry (tag 33) round-trips through
// decodeRawVariantVector without error.
func TestExtraAliasEntryRoundTrip_Good(t *testing.T) {
// Build a synthetic variant vector with one extra_alias_entry element.
// Format: count(1) + tag(33) + alias(string) + address(tx_payer format) +
// text_comment(string) + sign(vector) + view_key(optional).
var raw []byte
raw = append(raw, EncodeVarint(1)...) // 1 element
raw = append(raw, tagExtraAliasEntry)
// m_alias: "myalias"
alias := []byte("myalias")
raw = append(raw, EncodeVarint(uint64(len(alias)))...)
raw = append(raw, alias...)
// m_address: tx_payer format = spend_key(32) + view_key(32) + optional marker
addr := make([]byte, 64)
for i := range addr {
addr[i] = byte(i + 10)
}
raw = append(raw, addr...)
// is_auditable optional marker: 0 = not present
raw = append(raw, 0x00)
// m_text_comment: empty
raw = append(raw, EncodeVarint(0)...)
// m_sign: 0 signatures
raw = append(raw, EncodeVarint(0)...)
// m_view_key: optional, present (marker=1 + 32 bytes)
raw = append(raw, 0x01)
viewKey := make([]byte, 32)
for i := range viewKey {
viewKey[i] = byte(0xBB)
}
raw = append(raw, viewKey...)
// Decode and round-trip.
dec := NewDecoder(bytes.NewReader(raw))
decoded := decodeRawVariantVector(dec)
if dec.Err() != nil {
t.Fatalf("decode failed: %v", dec.Err())
}
if !bytes.Equal(decoded, raw) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw))
}
}
// TestExtraAliasEntryNoViewKey_Good verifies extra_alias_entry with
// the optional view_key marker set to 0 (not present).
func TestExtraAliasEntryNoViewKey_Good(t *testing.T) {
var raw []byte
raw = append(raw, EncodeVarint(1)...) // 1 element
raw = append(raw, tagExtraAliasEntry)
// m_alias: "short"
alias := []byte("short")
raw = append(raw, EncodeVarint(uint64(len(alias)))...)
raw = append(raw, alias...)
// m_address: keys + no auditable flag
raw = append(raw, make([]byte, 64)...)
raw = append(raw, 0x00) // not auditable
// m_text_comment: empty
raw = append(raw, EncodeVarint(0)...)
// m_sign: 0 signatures
raw = append(raw, EncodeVarint(0)...)
// m_view_key: not present (marker=0)
raw = append(raw, 0x00)
dec := NewDecoder(bytes.NewReader(raw))
decoded := decodeRawVariantVector(dec)
if dec.Err() != nil {
t.Fatalf("decode failed: %v", dec.Err())
}
if !bytes.Equal(decoded, raw) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(decoded), len(raw))
}
}

View file

@ -64,23 +64,6 @@ func TestReadAssetDescriptorOperation_Good(t *testing.T) {
if !bytes.Equal(got, blob) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob))
}
op, err := DecodeAssetDescriptorOperation(blob)
if err != nil {
t.Fatalf("DecodeAssetDescriptorOperation failed: %v", err)
}
if op.Version != 1 || op.OperationType != 0 {
t.Fatalf("unexpected operation header: %+v", op)
}
if op.Descriptor == nil {
t.Fatal("expected descriptor to be present")
}
if op.Descriptor.Ticker != "LTHN" || op.Descriptor.FullName != "Lethean" {
t.Fatalf("unexpected descriptor contents: %+v", op.Descriptor)
}
if op.Descriptor.TotalMaxSupply != 1000000 || op.Descriptor.DecimalPoint != 12 {
t.Fatalf("unexpected descriptor values: %+v", op.Descriptor)
}
}
func TestReadAssetDescriptorOperation_Bad(t *testing.T) {
@ -127,23 +110,6 @@ func TestReadAssetDescriptorOperationEmit_Good(t *testing.T) {
if !bytes.Equal(got, blob) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob))
}
op, err := DecodeAssetDescriptorOperation(blob)
if err != nil {
t.Fatalf("DecodeAssetDescriptorOperation (emit) failed: %v", err)
}
if op.Version != 1 || op.OperationType != 1 {
t.Fatalf("unexpected operation header: %+v", op)
}
if op.Descriptor != nil {
t.Fatalf("emit operation should not carry descriptor: %+v", op)
}
if op.AmountToEmit != 500000 || op.AmountToBurn != 0 {
t.Fatalf("unexpected emit amounts: %+v", op)
}
if op.AssetID[0] != 0xAB || op.AssetID[31] != 0xAB {
t.Fatalf("unexpected asset id: %x", op.AssetID)
}
}
func TestVariantVectorWithTag40_Good(t *testing.T) {
@ -170,17 +136,6 @@ func TestVariantVectorWithTag40_Good(t *testing.T) {
if !bytes.Equal(got, raw) {
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(raw))
}
elements, err := DecodeVariantVector(raw)
if err != nil {
t.Fatalf("DecodeVariantVector failed: %v", err)
}
if len(elements) != 1 || elements[0].Tag != tagAssetDescriptorOperation {
t.Fatalf("unexpected elements: %+v", elements)
}
if !bytes.Equal(elements[0].Data, innerBlob) {
t.Fatalf("unexpected element payload length: got %d, want %d", len(elements[0].Data), len(innerBlob))
}
}
func buildAssetOperationProofBlob() []byte {
@ -318,9 +273,9 @@ func TestV3TransactionRoundTrip_Good(t *testing.T) {
// version = 3
enc.WriteVarint(3)
// vin: 1 coinbase input
enc.WriteVarint(1) // input count
enc.WriteVarint(1) // input count
enc.WriteVariantTag(0) // txin_gen tag
enc.WriteVarint(201) // height
enc.WriteVarint(201) // height
// extra: variant vector with 2 elements (public_key + zarcanum_tx_data_v1)
enc.WriteVarint(2)
@ -334,13 +289,13 @@ func TestV3TransactionRoundTrip_Good(t *testing.T) {
// vout: 2 Zarcanum outputs
enc.WriteVarint(2)
for range 2 {
enc.WriteVariantTag(38) // OutputTypeZarcanum
enc.WriteVariantTag(38) // OutputTypeZarcanum
enc.WriteBytes(make([]byte, 32)) // stealth_address
enc.WriteBytes(make([]byte, 32)) // concealing_point
enc.WriteBytes(make([]byte, 32)) // amount_commitment
enc.WriteBytes(make([]byte, 32)) // blinded_asset_id
enc.WriteUint64LE(0) // encrypted_amount
enc.WriteUint8(0) // mix_attr
enc.WriteUint64LE(0) // encrypted_amount
enc.WriteUint8(0) // mix_attr
}
// hardfork_id = 5

View file

@ -1,214 +0,0 @@
// 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 wire
import (
"bytes"
"fmt"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/types"
)
// VariantElement is one tagged element from a raw variant vector.
// Data contains the raw wire bytes for the element payload, without the tag.
type VariantElement struct {
Tag uint8
Data []byte
}
// DecodeVariantVector decodes a raw variant vector into tagged raw elements.
// It is useful for higher-level validation of raw transaction fields such as
// extra, attachment, signatures, and proofs.
func DecodeVariantVector(raw []byte) ([]VariantElement, error) {
dec := NewDecoder(bytes.NewReader(raw))
count := dec.ReadVarint()
if dec.Err() != nil {
return nil, dec.Err()
}
elems := make([]VariantElement, 0, int(count))
for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8()
if dec.Err() != nil {
return nil, coreerr.E("DecodeVariantVector", fmt.Sprintf("read tag %d", i), dec.Err())
}
data := readVariantElementData(dec, tag)
if dec.Err() != nil {
return nil, coreerr.E("DecodeVariantVector", fmt.Sprintf("read element %d", i), dec.Err())
}
elems = append(elems, VariantElement{Tag: tag, Data: data})
}
return elems, nil
}
// DecodeAssetDescriptorOperation decodes a raw asset_descriptor_operation
// payload into its typed representation.
func DecodeAssetDescriptorOperation(raw []byte) (types.AssetDescriptorOperation, error) {
dec := NewDecoder(bytes.NewReader(raw))
_, op := parseAssetDescriptorOperation(dec)
if dec.Err() != nil {
return types.AssetDescriptorOperation{}, coreerr.E("DecodeAssetDescriptorOperation", "decode asset descriptor operation", dec.Err())
}
return op, nil
}
// parseAssetDescriptorOperation is the single source of truth for both raw
// wire preservation and typed HF5 asset operation decoding.
func parseAssetDescriptorOperation(dec *Decoder) ([]byte, types.AssetDescriptorOperation) {
var raw []byte
var op types.AssetDescriptorOperation
appendByte := func(v uint8) {
raw = append(raw, v)
}
appendBytes := func(v []byte) {
raw = append(raw, v...)
}
op.Version = dec.ReadUint8()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendByte(op.Version)
op.OperationType = dec.ReadUint8()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendByte(op.OperationType)
assetMarker := dec.ReadUint8()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendByte(assetMarker)
if assetMarker != 0 {
assetID := dec.ReadBytes(32)
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
copy(op.AssetID[:], assetID)
appendBytes(assetID)
}
descMarker := dec.ReadUint8()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendByte(descMarker)
if descMarker != 0 {
desc := &types.AssetDescriptorBase{}
tickerRaw := readStringBlob(dec)
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
desc.Ticker = decodeStringBlob(tickerRaw)
appendBytes(tickerRaw)
fullNameRaw := readStringBlob(dec)
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
desc.FullName = decodeStringBlob(fullNameRaw)
appendBytes(fullNameRaw)
desc.TotalMaxSupply = dec.ReadUint64LE()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendBytes(uint64LEBytes(desc.TotalMaxSupply))
desc.CurrentSupply = dec.ReadUint64LE()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendBytes(uint64LEBytes(desc.CurrentSupply))
desc.DecimalPoint = dec.ReadUint8()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendByte(desc.DecimalPoint)
metaInfoRaw := readStringBlob(dec)
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
desc.MetaInfo = decodeStringBlob(metaInfoRaw)
appendBytes(metaInfoRaw)
ownerKey := dec.ReadBytes(32)
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
copy(desc.OwnerKey[:], ownerKey)
appendBytes(ownerKey)
desc.Etc = readVariantVectorFixed(dec, 1)
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendBytes(desc.Etc)
op.Descriptor = desc
}
op.AmountToEmit = dec.ReadUint64LE()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendBytes(uint64LEBytes(op.AmountToEmit))
op.AmountToBurn = dec.ReadUint64LE()
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendBytes(uint64LEBytes(op.AmountToBurn))
op.Etc = readVariantVectorFixed(dec, 1)
if dec.Err() != nil {
return nil, types.AssetDescriptorOperation{}
}
appendBytes(op.Etc)
return raw, op
}
func decodeStringBlob(raw []byte) string {
return string(raw[varintPrefixLen(raw):])
}
func varintPrefixLen(raw []byte) int {
n := 0
for n < len(raw) {
n++
if raw[n-1] < 0x80 {
return n
}
}
return len(raw)
}
func uint64LEBytes(v uint64) []byte {
return []byte{
byte(v),
byte(v >> 8),
byte(v >> 16),
byte(v >> 24),
byte(v >> 32),
byte(v >> 40),
byte(v >> 48),
byte(v >> 56),
}
}