Compare commits
45 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76488e0beb | ||
|
|
caf83faf39 | ||
|
|
123047bebd | ||
|
|
330ee2a146 | ||
|
|
d5070cce15 | ||
|
|
9c5b179375 | ||
|
|
602c886400 | ||
|
|
474fa2f07d | ||
|
|
99720fff5e | ||
|
|
b7428496bd | ||
|
|
bdbefa7d4a | ||
|
|
bb941ebcc5 | ||
|
|
95edac1d15 | ||
|
|
8802b94ee5 | ||
|
|
0512861330 | ||
|
|
51d5ce9f14 | ||
|
|
bc3e208691 | ||
|
|
219aeae540 | ||
|
|
2e92407233 | ||
|
|
0993b081c7 | ||
|
|
cb43082d18 | ||
|
|
2bebe323b8 | ||
|
|
0ab8bfbd01 | ||
|
|
b34afa827f | ||
|
|
7e01df15fe | ||
|
|
2f3f46e8c5 | ||
|
|
92628cec35 | ||
|
|
92cb5a8fbb | ||
|
|
e25e3e73e7 | ||
|
|
c787990b9a | ||
|
|
3686a82b33 | ||
|
|
d6f31dbe57 | ||
|
|
41f2d52979 | ||
|
|
050d530b29 | ||
|
|
c1b68523c6 | ||
|
|
be99c5e93a | ||
|
|
d2caf68d94 | ||
|
|
ccdcfbaacf | ||
|
|
f1738527bc | ||
|
|
21c5d49ef9 | ||
|
|
0ba5bbe49c | ||
|
|
01f4e5cd0a | ||
|
|
d3143d3f88 | ||
|
|
f7ee451fc4 | ||
|
|
8e6dc326df |
61 changed files with 3418 additions and 528 deletions
|
|
@ -19,11 +19,16 @@ 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 {
|
||||
|
|
@ -34,6 +39,8 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -76,12 +76,16 @@ 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ 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)
|
||||
|
|
@ -26,6 +28,8 @@ 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) {
|
||||
|
|
@ -92,6 +96,8 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@
|
|||
package chain
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/blockchain/p2p"
|
||||
levinpkg "dappco.re/go/core/p2p/node/levin"
|
||||
|
|
@ -28,6 +26,10 @@ 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
|
||||
|
|
@ -40,40 +42,44 @@ func (c *LevinP2PConn) handleMessage(hdr levinpkg.Header, data []byte) error {
|
|||
resp := p2p.TimedSyncRequest{PayloadData: c.localSync}
|
||||
payload, err := resp.Encode()
|
||||
if err != nil {
|
||||
return coreerr.E("LevinP2PConn.handleMessage", "encode timed_sync response", err)
|
||||
return corelog.E("LevinP2PConn.handleMessage", "encode timed_sync response", err)
|
||||
}
|
||||
if err := c.conn.WriteResponse(p2p.CommandTimedSync, payload, levinpkg.ReturnOK); err != nil {
|
||||
return coreerr.E("LevinP2PConn.handleMessage", "write timed_sync response", err)
|
||||
return corelog.E("LevinP2PConn.handleMessage", "write timed_sync response", err)
|
||||
}
|
||||
log.Printf("p2p: responded to timed_sync")
|
||||
corelog.Info("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, coreerr.E("LevinP2PConn.RequestChain", "encode request_chain", err)
|
||||
return 0, nil, corelog.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, coreerr.E("LevinP2PConn.RequestChain", "write request_chain", err)
|
||||
return 0, nil, corelog.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, coreerr.E("LevinP2PConn.RequestChain", "read response_chain", err)
|
||||
return 0, nil, corelog.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, coreerr.E("LevinP2PConn.RequestChain", "decode response_chain", err)
|
||||
return 0, nil, corelog.E("LevinP2PConn.RequestChain", "decode response_chain", err)
|
||||
}
|
||||
return resp.StartHeight, resp.BlockIDs, nil
|
||||
}
|
||||
|
|
@ -83,27 +89,31 @@ 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, coreerr.E("LevinP2PConn.RequestObjects", "encode request_get_objects", err)
|
||||
return nil, corelog.E("LevinP2PConn.RequestObjects", "encode request_get_objects", err)
|
||||
}
|
||||
|
||||
if err := c.conn.WritePacket(p2p.CommandRequestObjects, payload, false); err != nil {
|
||||
return nil, coreerr.E("LevinP2PConn.RequestObjects", "write request_get_objects", err)
|
||||
return nil, corelog.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, coreerr.E("LevinP2PConn.RequestObjects", "read response_get_objects", err)
|
||||
return nil, corelog.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, coreerr.E("LevinP2PConn.RequestObjects", "decode response_get_objects", err)
|
||||
return nil, corelog.E("LevinP2PConn.RequestObjects", "decode response_get_objects", err)
|
||||
}
|
||||
entries := make([]BlockBlobEntry, len(resp.Blocks))
|
||||
for i, b := range resp.Blocks {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ package chain
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// P2PConnection abstracts the P2P communication needed for block sync.
|
||||
|
|
@ -46,7 +45,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
|
|||
|
||||
localHeight, err := c.Height()
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.P2PSync", "p2p sync: get height", err)
|
||||
return corelog.E("Chain.P2PSync", "p2p sync: get height", err)
|
||||
}
|
||||
|
||||
peerHeight := conn.PeerHeight()
|
||||
|
|
@ -57,7 +56,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
|
|||
// Build sparse chain history.
|
||||
history, err := c.SparseChainHistory()
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.P2PSync", "p2p sync: build history", err)
|
||||
return corelog.E("Chain.P2PSync", "p2p sync: build history", err)
|
||||
}
|
||||
|
||||
// Convert Hash to []byte for P2P.
|
||||
|
|
@ -71,14 +70,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 coreerr.E("Chain.P2PSync", "p2p sync: request chain", err)
|
||||
return corelog.E("Chain.P2PSync", "p2p sync: request chain", err)
|
||||
}
|
||||
|
||||
if len(blockIDs) == 0 {
|
||||
return nil // nothing to sync
|
||||
}
|
||||
|
||||
log.Printf("p2p sync: chain entry from height %d, %d block IDs", startHeight, len(blockIDs))
|
||||
corelog.Info("p2p sync chain entry", "start_height", startHeight, "block_ids", len(blockIDs))
|
||||
|
||||
// The daemon returns the fork-point block as the first entry.
|
||||
// Skip blocks we already have.
|
||||
|
|
@ -108,24 +107,24 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
|
|||
|
||||
entries, err := conn.RequestObjects(batch)
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.P2PSync", "p2p sync: request objects", err)
|
||||
return corelog.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 {
|
||||
log.Printf("p2p sync: processing block %d", blockHeight)
|
||||
corelog.Info("p2p sync processing block", "height", blockHeight)
|
||||
}
|
||||
|
||||
blockDiff, err := c.NextDifficulty(blockHeight, opts.Forks)
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.P2PSync", fmt.Sprintf("p2p sync: compute difficulty for block %d", blockHeight), err)
|
||||
return corelog.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 coreerr.E("Chain.P2PSync", fmt.Sprintf("p2p sync: process block %d", blockHeight), err)
|
||||
return corelog.E("Chain.P2PSync", fmt.Sprintf("p2p sync: process block %d", blockHeight), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ import (
|
|||
)
|
||||
|
||||
// GetRingOutputs fetches the public keys for the given global output indices
|
||||
// at the specified amount. This implements the consensus.RingOutputsFn
|
||||
// signature for use during signature verification.
|
||||
func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
|
||||
pubs := make([]types.PublicKey, len(offsets))
|
||||
// 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))
|
||||
for i, gidx := range offsets {
|
||||
txHash, outNo, err := c.GetOutput(amount, gidx)
|
||||
if err != nil {
|
||||
|
|
@ -36,16 +38,44 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK
|
|||
|
||||
switch out := tx.Vout[outNo].(type) {
|
||||
case types.TxOutputBare:
|
||||
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)
|
||||
spendKey, err := ringOutputSpendKey(height, out.Target)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: %v", i, err), nil)
|
||||
}
|
||||
pubs[i] = toKey.Key
|
||||
publicKeys[i] = spendKey
|
||||
default:
|
||||
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: unsupported output type %T", i, out), nil)
|
||||
}
|
||||
}
|
||||
return pubs, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// GetZCRingOutputs fetches ZC ring members (stealth address, amount commitment,
|
||||
|
|
@ -53,6 +83,8 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK
|
|||
// 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 {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func TestGetRingOutputs_Good(t *testing.T) {
|
|||
t.Fatalf("gidx: got %d, want 0", gidx)
|
||||
}
|
||||
|
||||
pubs, err := c.GetRingOutputs(1000, []uint64{0})
|
||||
pubs, err := c.GetRingOutputs(100, 1000, []uint64{0})
|
||||
if err != nil {
|
||||
t.Fatalf("GetRingOutputs: %v", err)
|
||||
}
|
||||
|
|
@ -55,6 +55,134 @@ 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)
|
||||
|
||||
|
|
@ -97,7 +225,7 @@ func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
|
|||
t.Fatalf("PutOutput(tx2): %v", err)
|
||||
}
|
||||
|
||||
pubs, err := c.GetRingOutputs(500, []uint64{0, 1})
|
||||
pubs, err := c.GetRingOutputs(500, 500, []uint64{0, 1})
|
||||
if err != nil {
|
||||
t.Fatalf("GetRingOutputs: %v", err)
|
||||
}
|
||||
|
|
@ -115,7 +243,7 @@ func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
|
|||
func TestGetRingOutputs_Bad_OutputNotFound(t *testing.T) {
|
||||
c := newTestChain(t)
|
||||
|
||||
_, err := c.GetRingOutputs(1000, []uint64{99})
|
||||
_, err := c.GetRingOutputs(1000, 1000, []uint64{99})
|
||||
if err == nil {
|
||||
t.Fatal("GetRingOutputs: expected error for missing output, got nil")
|
||||
}
|
||||
|
|
@ -130,7 +258,7 @@ func TestGetRingOutputs_Bad_TxNotFound(t *testing.T) {
|
|||
t.Fatalf("PutOutput: %v", err)
|
||||
}
|
||||
|
||||
_, err := c.GetRingOutputs(1000, []uint64{0})
|
||||
_, err := c.GetRingOutputs(1000, 1000, []uint64{0})
|
||||
if err == nil {
|
||||
t.Fatal("GetRingOutputs: expected error for missing tx, got nil")
|
||||
}
|
||||
|
|
@ -159,7 +287,7 @@ func TestGetRingOutputs_Bad_OutputIndexOutOfRange(t *testing.T) {
|
|||
t.Fatalf("PutOutput: %v", err)
|
||||
}
|
||||
|
||||
_, err := c.GetRingOutputs(1000, []uint64{0})
|
||||
_, err := c.GetRingOutputs(1000, 1000, []uint64{0})
|
||||
if err == nil {
|
||||
t.Fatal("GetRingOutputs: expected error for out-of-range index, got nil")
|
||||
}
|
||||
|
|
@ -168,7 +296,7 @@ func TestGetRingOutputs_Bad_OutputIndexOutOfRange(t *testing.T) {
|
|||
func TestGetRingOutputs_Good_EmptyOffsets(t *testing.T) {
|
||||
c := newTestChain(t)
|
||||
|
||||
pubs, err := c.GetRingOutputs(1000, []uint64{})
|
||||
pubs, err := c.GetRingOutputs(1000, 1000, []uint64{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetRingOutputs: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ 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)
|
||||
|
|
@ -72,6 +74,8 @@ 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 {
|
||||
|
|
@ -84,6 +88,8 @@ 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 {
|
||||
|
|
@ -106,6 +112,8 @@ 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)
|
||||
|
|
@ -130,6 +138,8 @@ 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 {
|
||||
|
|
@ -156,6 +166,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/consensus"
|
||||
|
|
@ -52,12 +51,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 coreerr.E("Chain.Sync", "sync: get local height", err)
|
||||
return corelog.E("Chain.Sync", "sync: get local height", err)
|
||||
}
|
||||
|
||||
remoteHeight, err := client.GetHeight()
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.Sync", "sync: get remote height", err)
|
||||
return corelog.E("Chain.Sync", "sync: get remote height", err)
|
||||
}
|
||||
|
||||
for localHeight < remoteHeight {
|
||||
|
|
@ -72,22 +71,22 @@ func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions)
|
|||
|
||||
blocks, err := client.GetBlocksDetails(localHeight, batch)
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: fetch blocks at %d", localHeight), err)
|
||||
return corelog.E("Chain.Sync", fmt.Sprintf("sync: fetch blocks at %d", localHeight), err)
|
||||
}
|
||||
|
||||
if err := resolveBlockBlobs(blocks, client); err != nil {
|
||||
return coreerr.E("Chain.Sync", fmt.Sprintf("sync: resolve blobs at %d", localHeight), err)
|
||||
return corelog.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 coreerr.E("Chain.Sync", fmt.Sprintf("sync: process block %d", bd.Height), err)
|
||||
return corelog.E("Chain.Sync", fmt.Sprintf("sync: process block %d", bd.Height), err)
|
||||
}
|
||||
}
|
||||
|
||||
localHeight, err = c.Height()
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.Sync", "sync: get height after batch", err)
|
||||
return corelog.E("Chain.Sync", "sync: get height after batch", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,12 +95,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 {
|
||||
log.Printf("sync: processing block %d", bd.Height)
|
||||
corelog.Info("sync processing block", "height", bd.Height)
|
||||
}
|
||||
|
||||
blockBlob, err := hex.DecodeString(bd.Blob)
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.processBlock", "decode block hex", err)
|
||||
return corelog.E("Chain.processBlock", "decode block hex", err)
|
||||
}
|
||||
|
||||
// Build a set of the block's regular tx hashes for lookup.
|
||||
|
|
@ -111,7 +110,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 coreerr.E("Chain.processBlock", "decode block for tx hashes", err)
|
||||
return corelog.E("Chain.processBlock", "decode block for tx hashes", err)
|
||||
}
|
||||
|
||||
regularTxs := make(map[string]struct{}, len(blk.TxHashes))
|
||||
|
|
@ -126,7 +125,7 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
|
|||
}
|
||||
txBlobBytes, err := hex.DecodeString(txInfo.Blob)
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.processBlock", fmt.Sprintf("decode tx hex %s", txInfo.ID), err)
|
||||
return corelog.E("Chain.processBlock", fmt.Sprintf("decode tx hex %s", txInfo.ID), err)
|
||||
}
|
||||
txBlobs = append(txBlobs, txBlobBytes)
|
||||
}
|
||||
|
|
@ -137,10 +136,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 coreerr.E("Chain.processBlock", "parse daemon block hash", err)
|
||||
return corelog.E("Chain.processBlock", "parse daemon block hash", err)
|
||||
}
|
||||
if computedHash != daemonHash {
|
||||
return coreerr.E("Chain.processBlock", fmt.Sprintf("block hash mismatch: computed %s, daemon says %s", computedHash, daemonHash), nil)
|
||||
return corelog.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)
|
||||
|
|
@ -155,7 +154,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 coreerr.E("Chain.processBlockBlobs", "decode block wire", err)
|
||||
return corelog.E("Chain.processBlockBlobs", "decode block wire", err)
|
||||
}
|
||||
|
||||
// Compute the block hash.
|
||||
|
|
@ -165,21 +164,21 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
|||
if height == 0 {
|
||||
genesisHash, err := types.HashFromHex(GenesisHash)
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.processBlockBlobs", "parse genesis hash", err)
|
||||
return corelog.E("Chain.processBlockBlobs", "parse genesis hash", err)
|
||||
}
|
||||
if blockHash != genesisHash {
|
||||
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("genesis hash %s does not match expected %s", blockHash, GenesisHash), nil)
|
||||
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("genesis hash %s does not match expected %s", blockHash, GenesisHash), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate header.
|
||||
if err := c.ValidateHeader(&blk, height); err != nil {
|
||||
if err := c.ValidateHeader(&blk, height, opts.Forks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate miner transaction structure.
|
||||
if err := consensus.ValidateMinerTx(&blk.MinerTx, height, opts.Forks); err != nil {
|
||||
return coreerr.E("Chain.processBlockBlobs", "validate miner tx", err)
|
||||
return corelog.E("Chain.processBlockBlobs", "validate miner tx", err)
|
||||
}
|
||||
|
||||
// Calculate cumulative difficulty.
|
||||
|
|
@ -187,7 +186,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
|||
if height > 0 {
|
||||
_, prevMeta, err := c.TopBlock()
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.processBlockBlobs", "get prev block meta", err)
|
||||
return corelog.E("Chain.processBlockBlobs", "get prev block meta", err)
|
||||
}
|
||||
cumulDiff = prevMeta.CumulativeDiff + difficulty
|
||||
} else {
|
||||
|
|
@ -198,13 +197,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 coreerr.E("Chain.processBlockBlobs", "index miner tx outputs", err)
|
||||
return corelog.E("Chain.processBlockBlobs", "index miner tx outputs", err)
|
||||
}
|
||||
if err := c.PutTransaction(minerTxHash, &blk.MinerTx, &TxMeta{
|
||||
KeeperBlock: height,
|
||||
GlobalOutputIndexes: minerGindexes,
|
||||
}); err != nil {
|
||||
return coreerr.E("Chain.processBlockBlobs", "store miner tx", err)
|
||||
return corelog.E("Chain.processBlockBlobs", "store miner tx", err)
|
||||
}
|
||||
|
||||
// Process regular transactions from txBlobs.
|
||||
|
|
@ -212,27 +211,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 coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("decode tx wire [%d]", i), err)
|
||||
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("decode tx wire [%d]", i), err)
|
||||
}
|
||||
|
||||
txHash := wire.TransactionHash(&tx)
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("verify tx signatures %s", txHash), err)
|
||||
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("verify tx signatures %s", txHash), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Index outputs.
|
||||
gindexes, err := c.indexOutputs(txHash, &tx)
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("index tx outputs %s", txHash), err)
|
||||
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("index tx outputs %s", txHash), err)
|
||||
}
|
||||
|
||||
// Mark key images as spent.
|
||||
|
|
@ -240,11 +239,15 @@ 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 coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
|
||||
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)
|
||||
}
|
||||
case types.TxInputZC:
|
||||
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
|
||||
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
|
||||
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,7 +257,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
|||
KeeperBlock: height,
|
||||
GlobalOutputIndexes: gindexes,
|
||||
}); err != nil {
|
||||
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("store tx %s", txHash), err)
|
||||
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("store tx %s", txHash), err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,13 +332,13 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
|
|||
// Batch-fetch tx blobs.
|
||||
txHexes, missed, err := client.GetTransactions(allHashes)
|
||||
if err != nil {
|
||||
return coreerr.E("resolveBlockBlobs", "fetch tx blobs", err)
|
||||
return corelog.E("resolveBlockBlobs", "fetch tx blobs", err)
|
||||
}
|
||||
if len(missed) > 0 {
|
||||
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("daemon missed %d tx(es): %v", len(missed), missed), nil)
|
||||
return corelog.E("resolveBlockBlobs", fmt.Sprintf("daemon missed %d tx(es): %v", len(missed), missed), nil)
|
||||
}
|
||||
if len(txHexes) != len(allHashes) {
|
||||
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("expected %d tx blobs, got %d", len(allHashes), len(txHexes)), nil)
|
||||
return corelog.E("resolveBlockBlobs", fmt.Sprintf("expected %d tx blobs, got %d", len(allHashes), len(txHexes)), nil)
|
||||
}
|
||||
|
||||
// Index fetched blobs by hash.
|
||||
|
|
@ -363,16 +366,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 coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse header", bd.Height), err)
|
||||
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse header", bd.Height), err)
|
||||
}
|
||||
|
||||
// Miner tx blob is transactions_details[0].
|
||||
if len(bd.Transactions) == 0 {
|
||||
return coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d has no transactions_details", bd.Height), nil)
|
||||
return corelog.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 coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: decode miner tx hex", bd.Height), err)
|
||||
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d: decode miner tx hex", bd.Height), err)
|
||||
}
|
||||
|
||||
// Collect regular tx hashes.
|
||||
|
|
@ -380,7 +383,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 coreerr.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse tx hash %s", bd.Height, txInfo.ID), err)
|
||||
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d: parse tx hash %s", bd.Height, txInfo.ID), err)
|
||||
}
|
||||
txHashes = append(txHashes, h)
|
||||
}
|
||||
|
|
@ -410,17 +413,17 @@ var aggregatedRE = regexp.MustCompile(`"AGGREGATED"\s*:\s*\{([^}]+)\}`)
|
|||
func parseBlockHeader(objectInJSON string) (*types.BlockHeader, error) {
|
||||
m := aggregatedRE.FindStringSubmatch(objectInJSON)
|
||||
if m == nil {
|
||||
return nil, coreerr.E("parseBlockHeader", "AGGREGATED section not found in object_in_json", nil)
|
||||
return nil, corelog.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, coreerr.E("parseBlockHeader", "unmarshal AGGREGATED", err)
|
||||
return nil, corelog.E("parseBlockHeader", "unmarshal AGGREGATED", err)
|
||||
}
|
||||
|
||||
prevID, err := types.HashFromHex(hj.PrevID)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("parseBlockHeader", "parse prev_id", err)
|
||||
return nil, corelog.E("parseBlockHeader", "parse prev_id", err)
|
||||
}
|
||||
|
||||
return &types.BlockHeader{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -735,6 +736,87 @@ 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, ®ularTx)
|
||||
regularTxBlob := txBuf.Bytes()
|
||||
regularTxHash := wire.TransactionHash(®ularTx)
|
||||
|
||||
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{
|
||||
|
|
@ -937,3 +1019,148 @@ 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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ 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) error {
|
||||
func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64, forks []config.HardFork) error {
|
||||
currentHeight, err := c.Height()
|
||||
if err != nil {
|
||||
return coreerr.E("Chain.ValidateHeader", "validate: get height", err)
|
||||
|
|
@ -34,6 +35,9 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +50,11 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ package chain
|
|||
import (
|
||||
"testing"
|
||||
|
||||
store "dappco.re/go/core/store"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
store "dappco.re/go/core/store"
|
||||
)
|
||||
|
||||
func TestValidateHeader_Good_Genesis(t *testing.T) {
|
||||
|
|
@ -19,13 +20,13 @@ func TestValidateHeader_Good_Genesis(t *testing.T) {
|
|||
|
||||
blk := &types.Block{
|
||||
BlockHeader: types.BlockHeader{
|
||||
MajorVersion: 1,
|
||||
MajorVersion: 0,
|
||||
Timestamp: 1770897600,
|
||||
},
|
||||
MinerTx: testCoinbaseTx(0),
|
||||
}
|
||||
|
||||
err := c.ValidateHeader(blk, 0)
|
||||
err := c.ValidateHeader(blk, 0, config.MainnetForks)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateHeader genesis: %v", err)
|
||||
}
|
||||
|
|
@ -38,7 +39,7 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
|
|||
|
||||
// Store block 0.
|
||||
blk0 := &types.Block{
|
||||
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
|
||||
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
|
||||
MinerTx: testCoinbaseTx(0),
|
||||
}
|
||||
hash0 := types.Hash{0x01}
|
||||
|
|
@ -47,14 +48,14 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
|
|||
// Validate block 1.
|
||||
blk1 := &types.Block{
|
||||
BlockHeader: types.BlockHeader{
|
||||
MajorVersion: 1,
|
||||
MajorVersion: 0,
|
||||
Timestamp: 1770897720,
|
||||
PrevID: hash0,
|
||||
},
|
||||
MinerTx: testCoinbaseTx(1),
|
||||
}
|
||||
|
||||
err := c.ValidateHeader(blk1, 1)
|
||||
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateHeader block 1: %v", err)
|
||||
}
|
||||
|
|
@ -66,21 +67,21 @@ func TestValidateHeader_Bad_WrongPrevID(t *testing.T) {
|
|||
c := New(s)
|
||||
|
||||
blk0 := &types.Block{
|
||||
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
|
||||
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: 1,
|
||||
MajorVersion: 0,
|
||||
Timestamp: 1770897720,
|
||||
PrevID: types.Hash{0xFF}, // wrong
|
||||
},
|
||||
MinerTx: testCoinbaseTx(1),
|
||||
}
|
||||
|
||||
err := c.ValidateHeader(blk1, 1)
|
||||
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong prev_id")
|
||||
}
|
||||
|
|
@ -92,12 +93,12 @@ func TestValidateHeader_Bad_WrongHeight(t *testing.T) {
|
|||
c := New(s)
|
||||
|
||||
blk := &types.Block{
|
||||
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
|
||||
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)
|
||||
err := c.ValidateHeader(blk, 5, config.MainnetForks)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong height")
|
||||
}
|
||||
|
|
@ -110,14 +111,33 @@ func TestValidateHeader_Bad_GenesisNonZeroPrev(t *testing.T) {
|
|||
|
||||
blk := &types.Block{
|
||||
BlockHeader: types.BlockHeader{
|
||||
MajorVersion: 1,
|
||||
MajorVersion: 0,
|
||||
PrevID: types.Hash{0xFF}, // genesis must have zero prev_id
|
||||
},
|
||||
MinerTx: testCoinbaseTx(0),
|
||||
}
|
||||
|
||||
err := c.ValidateHeader(blk, 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,
|
||||
Timestamp: 1770897600,
|
||||
},
|
||||
MinerTx: testCoinbaseTx(0),
|
||||
}
|
||||
|
||||
err := c.ValidateHeader(blk, 0, config.MainnetForks)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong block version")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
package blockchain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
|
@ -28,9 +30,9 @@ const defaultChainSeed = "seeds.lthn.io:36942"
|
|||
// command path documents the node features directly.
|
||||
func AddChainCommands(root *cobra.Command) {
|
||||
var (
|
||||
dataDir string
|
||||
seed string
|
||||
testnet bool
|
||||
chainDataDir string
|
||||
seedPeerAddress string
|
||||
useTestnet bool
|
||||
)
|
||||
|
||||
chainCmd := &cobra.Command{
|
||||
|
|
@ -39,26 +41,26 @@ func AddChainCommands(root *cobra.Command) {
|
|||
Long: "Manage the Lethean blockchain — sync, explore, and mine.",
|
||||
}
|
||||
|
||||
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.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.AddCommand(
|
||||
newChainExplorerCommand(&dataDir, &seed, &testnet),
|
||||
newChainSyncCommand(&dataDir, &seed, &testnet),
|
||||
newChainExplorerCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
|
||||
newChainSyncCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
|
||||
)
|
||||
|
||||
root.AddCommand(chainCmd)
|
||||
}
|
||||
|
||||
func chainConfigForSeed(testnet bool, seed string) (config.ChainConfig, []config.HardFork, string) {
|
||||
if testnet {
|
||||
if seed == defaultChainSeed {
|
||||
seed = "localhost:46942"
|
||||
func chainConfigForSeed(useTestnet bool, seedPeerAddress string) (config.ChainConfig, []config.HardFork, string) {
|
||||
if useTestnet {
|
||||
if seedPeerAddress == defaultChainSeed {
|
||||
seedPeerAddress = "localhost:46942"
|
||||
}
|
||||
return config.Testnet, config.TestnetForks, seed
|
||||
return config.Testnet, config.TestnetForks, seedPeerAddress
|
||||
}
|
||||
return config.Mainnet, config.MainnetForks, seed
|
||||
return config.Mainnet, config.MainnetForks, seedPeerAddress
|
||||
}
|
||||
|
||||
func defaultChainDataDirPath() string {
|
||||
|
|
@ -75,3 +77,16 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,3 +46,68 @@ 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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ 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 {
|
||||
|
|
@ -85,6 +87,8 @@ 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 {
|
||||
|
|
@ -97,6 +101,8 @@ 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 {
|
||||
|
|
|
|||
20
consensus/balance.go
Normal file
20
consensus/balance.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ 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
|
||||
|
|
@ -61,10 +63,38 @@ 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)
|
||||
}
|
||||
|
|
@ -86,12 +116,13 @@ func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFo
|
|||
switch tx.Vin[1].(type) {
|
||||
case types.TxInputToKey:
|
||||
// Pre-HF4 PoS.
|
||||
default:
|
||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
if !hf4Active {
|
||||
case types.TxInputZC:
|
||||
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
if !hardForkFourActive {
|
||||
return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs)
|
||||
}
|
||||
// Post-HF4: accept ZC inputs.
|
||||
default:
|
||||
return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs)
|
||||
}
|
||||
} else {
|
||||
return coreerr.E("ValidateMinerTx", fmt.Sprintf("%d inputs (expected 1 or 2)", len(tx.Vin)), ErrMinerTxInputs)
|
||||
|
|
@ -102,6 +133,11 @@ 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)
|
||||
|
|
@ -109,14 +145,17 @@ func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSi
|
|||
return err
|
||||
}
|
||||
|
||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
expected := MinerReward(reward, totalFees, hf4Active)
|
||||
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
expected := MinerReward(reward, totalFees, hardForkFourActive)
|
||||
|
||||
// Sum miner tx outputs.
|
||||
var outputSum uint64
|
||||
for _, vout := range minerTx.Vout {
|
||||
if bare, ok := vout.(types.TxOutputBare); ok {
|
||||
outputSum += bare.Amount
|
||||
switch out := vout.(type) {
|
||||
case types.TxOutputBare:
|
||||
outputSum += out.Amount
|
||||
case types.TxOutputZarcanum:
|
||||
outputSum += out.EncryptedAmount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,24 +188,34 @@ 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(blk *types.Block, forks []config.HardFork, height uint64) error {
|
||||
func checkBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
|
||||
expected := expectedBlockMajorVersion(forks, height)
|
||||
if blk.MajorVersion != expected {
|
||||
return coreerr.E("checkBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
|
||||
blk.MajorVersion, expected, height), ErrBlockMajorVersion)
|
||||
if majorVersion != expected {
|
||||
return coreerr.E("CheckBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
|
||||
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, forks, height); err != nil {
|
||||
if err := checkBlockVersion(blk.MajorVersion, forks, height); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +248,8 @@ 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 {
|
||||
|
|
@ -223,6 +274,8 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,15 @@ 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)
|
||||
|
|
@ -87,14 +96,14 @@ func TestValidateMinerTx_Bad_WrongHeight(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateMinerTx_Bad_NoInputs(t *testing.T) {
|
||||
tx := &types.Transaction{Version: types.VersionInitial}
|
||||
tx := &types.Transaction{Version: types.VersionPreHF4}
|
||||
err := ValidateMinerTx(tx, 100, config.MainnetForks)
|
||||
assert.ErrorIs(t, err, ErrMinerTxInputs)
|
||||
}
|
||||
|
||||
func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
|
||||
tx := &types.Transaction{
|
||||
Version: types.VersionInitial,
|
||||
Version: types.VersionPreHF4,
|
||||
Vin: []types.TxInput{types.TxInputToKey{Amount: 1}},
|
||||
}
|
||||
err := ValidateMinerTx(tx, 100, config.MainnetForks)
|
||||
|
|
@ -103,7 +112,7 @@ func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
|
|||
|
||||
func TestValidateMinerTx_Good_PoS(t *testing.T) {
|
||||
tx := &types.Transaction{
|
||||
Version: types.VersionInitial,
|
||||
Version: types.VersionPreHF4,
|
||||
Vin: []types.TxInput{
|
||||
types.TxInputGenesis{Height: 100},
|
||||
types.TxInputToKey{Amount: 1}, // PoS stake input
|
||||
|
|
@ -117,6 +126,148 @@ 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)
|
||||
|
|
@ -127,7 +278,7 @@ func TestValidateBlockReward_Good(t *testing.T) {
|
|||
func TestValidateBlockReward_Bad_TooMuch(t *testing.T) {
|
||||
height := uint64(100)
|
||||
tx := &types.Transaction{
|
||||
Version: types.VersionInitial,
|
||||
Version: types.VersionPreHF4,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
|
||||
Vout: []types.TxOutput{
|
||||
types.TxOutputBare{Amount: config.BlockReward + 1, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
|
||||
|
|
@ -141,7 +292,7 @@ func TestValidateBlockReward_Good_WithFees(t *testing.T) {
|
|||
height := uint64(100)
|
||||
fees := uint64(50_000_000_000)
|
||||
tx := &types.Transaction{
|
||||
Version: types.VersionInitial,
|
||||
Version: types.VersionPreHF4,
|
||||
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
|
||||
Vout: []types.TxOutput{
|
||||
types.TxOutputBare{Amount: config.BlockReward + fees, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
|
||||
|
|
@ -151,6 +302,53 @@ 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)
|
||||
|
|
@ -294,7 +492,7 @@ func TestCheckBlockVersion_Good(t *testing.T) {
|
|||
Flags: 0,
|
||||
},
|
||||
}
|
||||
err := checkBlockVersion(blk, tt.forks, tt.height)
|
||||
err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
@ -323,7 +521,7 @@ func TestCheckBlockVersion_Bad(t *testing.T) {
|
|||
Flags: 0,
|
||||
},
|
||||
}
|
||||
err := checkBlockVersion(blk, tt.forks, tt.height)
|
||||
err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
|
||||
assert.ErrorIs(t, err, ErrBlockVersion)
|
||||
})
|
||||
}
|
||||
|
|
@ -336,17 +534,17 @@ func TestCheckBlockVersion_Ugly(t *testing.T) {
|
|||
blk := &types.Block{
|
||||
BlockHeader: types.BlockHeader{MajorVersion: 255, Timestamp: now},
|
||||
}
|
||||
err := checkBlockVersion(blk, config.MainnetForks, 0)
|
||||
err := checkBlockVersion(blk.MajorVersion, config.MainnetForks, 0)
|
||||
assert.ErrorIs(t, err, ErrBlockVersion)
|
||||
|
||||
err = checkBlockVersion(blk, config.MainnetForks, 10081)
|
||||
err = checkBlockVersion(blk.MajorVersion, 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, config.MainnetForks, 10080)
|
||||
err = checkBlockVersion(blk0.MajorVersion, config.MainnetForks, 10080)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -376,7 +574,7 @@ func TestValidateBlock_MajorVersion_Good(t *testing.T) {
|
|||
Timestamp: now,
|
||||
Flags: 0,
|
||||
},
|
||||
MinerTx: *validMinerTx(tt.height),
|
||||
MinerTx: *validMinerTxForForks(tt.height, tt.forks),
|
||||
}
|
||||
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -410,7 +608,7 @@ func TestValidateBlock_MajorVersion_Bad(t *testing.T) {
|
|||
Timestamp: now,
|
||||
Flags: 0,
|
||||
},
|
||||
MinerTx: *validMinerTx(tt.height),
|
||||
MinerTx: *validMinerTxForForks(tt.height, tt.forks),
|
||||
}
|
||||
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
|
||||
assert.ErrorIs(t, err, ErrBlockMajorVersion)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
// - Cryptographic: PoW hash verification (RandomX via CGo),
|
||||
// ring signature verification, proof verification.
|
||||
//
|
||||
// All functions take *config.ChainConfig and a block height for
|
||||
// hardfork-aware validation. The package has no dependency on chain/.
|
||||
// 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.
|
||||
package consensus
|
||||
|
|
|
|||
|
|
@ -29,15 +29,16 @@ 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")
|
||||
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")
|
||||
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")
|
||||
|
||||
// ErrBlockVersion is an alias for ErrBlockMajorVersion, used by
|
||||
// checkBlockVersion when the block major version does not match
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ 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
|
||||
|
|
@ -39,6 +41,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ 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
|
||||
|
|
@ -33,6 +35,8 @@ 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 {
|
||||
|
|
@ -72,6 +76,9 @@ 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
|
||||
|
|
|
|||
122
consensus/tx.go
122
consensus/tx.go
|
|
@ -12,15 +12,34 @@ 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 {
|
||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
state := newTransactionForkState(forks, height)
|
||||
|
||||
// 0. Transaction version.
|
||||
if err := checkTxVersion(tx, forks, height); err != nil {
|
||||
if err := checkTxVersion(tx, state, height); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -37,15 +56,18 @@ 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, hf1Active, hf4Active); err != nil {
|
||||
if err := checkInputTypes(tx, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. Output validation.
|
||||
if err := checkOutputs(tx, hf1Active, hf4Active); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +85,7 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
|||
}
|
||||
|
||||
// 7. Balance check (pre-HF4 only — post-HF4 uses commitment proofs).
|
||||
if !hf4Active {
|
||||
if !state.hardForkFourActive {
|
||||
if _, err := TxFee(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -75,27 +97,37 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
|||
// checkTxVersion validates that the transaction version is appropriate for the
|
||||
// current hardfork era.
|
||||
//
|
||||
// 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)
|
||||
// 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
|
||||
}
|
||||
|
||||
if hf5Active && tx.Version < types.VersionPostHF5 {
|
||||
if tx.Version != expectedVersion {
|
||||
return coreerr.E("checkTxVersion",
|
||||
fmt.Sprintf("version %d too low after HF5 at height %d", tx.Version, height),
|
||||
fmt.Sprintf("version %d invalid at height %d (expected %d)", tx.Version, height, expectedVersion),
|
||||
ErrTxVersionInvalid)
|
||||
}
|
||||
|
||||
if !hf5Active && tx.Version >= types.VersionPostHF5 {
|
||||
if tx.Version >= types.VersionPostHF5 && tx.HardforkID != state.activeHardForkVersion {
|
||||
return coreerr.E("checkTxVersion",
|
||||
fmt.Sprintf("version %d not allowed before HF5 at height %d", tx.Version, height),
|
||||
fmt.Sprintf("hardfork id %d does not match active fork %d at height %d", tx.HardforkID, state.activeHardForkVersion, height),
|
||||
ErrTxVersionInvalid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error {
|
||||
func checkInputTypes(tx *types.Transaction, state transactionForkState) error {
|
||||
for _, vin := range tx.Vin {
|
||||
switch vin.(type) {
|
||||
case types.TxInputToKey:
|
||||
|
|
@ -104,25 +136,26 @@ func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) 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 !hf1Active {
|
||||
if !state.hardForkOneActive {
|
||||
return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF1", vin.InputType()), ErrInvalidInputType)
|
||||
}
|
||||
default:
|
||||
// Future types (ZC) — accept if HF4+.
|
||||
if !hf4Active {
|
||||
case types.TxInputZC:
|
||||
if !state.hardForkFourActive {
|
||||
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, hf1Active, hf4Active bool) error {
|
||||
func checkOutputs(tx *types.Transaction, state transactionForkState) error {
|
||||
if len(tx.Vout) == 0 {
|
||||
return ErrNoOutputs
|
||||
}
|
||||
|
||||
if hf4Active && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
|
||||
if state.hardForkFourActive && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
|
||||
return coreerr.E("checkOutputs", fmt.Sprintf("%d (min %d)", len(tx.Vout), config.TxMinAllowedOutputs), ErrTooFewOutputs)
|
||||
}
|
||||
|
||||
|
|
@ -136,15 +169,24 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
|
|||
if o.Amount == 0 {
|
||||
return coreerr.E("checkOutputs", fmt.Sprintf("output %d has zero amount", i), ErrInvalidOutput)
|
||||
}
|
||||
// HTLC and Multisig output targets require at least HF1.
|
||||
// Only known transparent output targets are accepted.
|
||||
switch o.Target.(type) {
|
||||
case types.TxOutToKey:
|
||||
case types.TxOutHTLC, types.TxOutMultisig:
|
||||
if !hf1Active {
|
||||
if !state.hardForkOneActive {
|
||||
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:
|
||||
// Validated by proof verification.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,3 +212,33 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@
|
|||
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"
|
||||
)
|
||||
|
|
@ -32,6 +34,10 @@ 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
|
||||
|
|
@ -238,6 +244,178 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -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, tt.forks, tt.height)
|
||||
err := checkTxVersion(tt.tx, newTransactionForkState(tt.forks, tt.height), tt.height)
|
||||
if err != nil {
|
||||
t.Errorf("checkTxVersion returned unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -79,15 +79,37 @@ 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, tt.forks, tt.height)
|
||||
err := checkTxVersion(tt.tx, newTransactionForkState(tt.forks, tt.height), tt.height)
|
||||
if err == nil {
|
||||
t.Error("expected ErrTxVersionInvalid, got nil")
|
||||
}
|
||||
|
|
@ -96,16 +118,30 @@ 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, config.TestnetForks, 201)
|
||||
err = checkTxVersion(tx, newTransactionForkState(config.TestnetForks, 201), 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, config.TestnetForks, 201)
|
||||
err = checkTxVersion(tx2, newTransactionForkState(config.TestnetForks, 201), 201)
|
||||
if err == nil {
|
||||
t.Error("v2 at HF5 activation boundary should be rejected")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ 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()
|
||||
|
|
@ -147,6 +159,20 @@ 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")
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import (
|
|||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
|
||||
// ZCRingMember holds the three public keys per ring entry needed for
|
||||
// CLSAG GGX verification (HF4+). All fields are premultiplied by 1/8
|
||||
|
|
@ -41,6 +41,9 @@ 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 {
|
||||
|
||||
|
|
@ -49,17 +52,17 @@ func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork,
|
|||
return nil
|
||||
}
|
||||
|
||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
|
||||
if !hf4Active {
|
||||
return verifyV1Signatures(tx, getRingOutputs)
|
||||
if !hardForkFourActive {
|
||||
return verifyV1Signatures(tx, height, getRingOutputs)
|
||||
}
|
||||
|
||||
return verifyV2Signatures(tx, getZCRingOutputs)
|
||||
}
|
||||
|
||||
// verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions.
|
||||
func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error {
|
||||
func verifyV1Signatures(tx *types.Transaction, height uint64, getRingOutputs RingOutputsFn) error {
|
||||
// Count ring-signing inputs (TxInputToKey and TxInputHTLC contribute
|
||||
// ring signatures; TxInputMultisig does not).
|
||||
var ringInputCount int
|
||||
|
|
@ -108,7 +111,7 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err
|
|||
offsets[i] = ref.GlobalIndex
|
||||
}
|
||||
|
||||
ringKeys, err := getRingOutputs(amount, offsets)
|
||||
ringKeys, err := getRingOutputs(height, amount, offsets)
|
||||
if err != nil {
|
||||
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err)
|
||||
}
|
||||
|
|
@ -152,7 +155,8 @@ 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 vice versa.
|
||||
// Validate that ZC inputs have ZC_sig and that ring-spending inputs use
|
||||
// the ring-signature tags that match their spending model.
|
||||
for i, vin := range tx.Vin {
|
||||
switch vin.(type) {
|
||||
case types.TxInputZC:
|
||||
|
|
@ -163,6 +167,10 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,8 +253,9 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Verify balance proof (generic_double_schnorr_sig).
|
||||
// Requires computing commitment_to_zero and a new bridge function.
|
||||
// Balance proofs are verified by the generic double-Schnorr helper in
|
||||
// consensus.VerifyBalanceProof once the transaction-specific public
|
||||
// points have been constructed.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
|
||||
getRing := func(height, 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(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
|
||||
getRing := func(height, amount uint64, offsets []uint64) ([]types.PublicKey, error) {
|
||||
return []types.PublicKey{types.PublicKey(pub)}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@ 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
|
||||
|
|
@ -58,23 +56,47 @@ 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
|
||||
|
|
@ -85,15 +107,18 @@ set_property(TARGET randomx PROPERTY CXX_STANDARD_REQUIRED ON)
|
|||
|
||||
# Platform-specific flags for RandomX
|
||||
enable_language(ASM)
|
||||
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)
|
||||
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()
|
||||
endif()
|
||||
|
||||
target_compile_options(randomx PRIVATE
|
||||
|
|
@ -106,7 +131,6 @@ 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})
|
||||
|
|
@ -116,7 +140,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -104,37 +104,91 @@ 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_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;
|
||||
if (!read_bppe_at(buf, len, &off, sig)) 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_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;
|
||||
if (!read_bge_at(buf, len, &off, proof)) 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" {
|
||||
|
|
@ -639,13 +693,133 @@ 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 ────────────────────────────────────────
|
||||
// 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.
|
||||
// Compatibility wrapper for the historical proof-only API.
|
||||
int cn_zarcanum_verify(const uint8_t /*hash*/[32], const uint8_t * /*proof*/,
|
||||
size_t /*proof_len*/) {
|
||||
return -1; // needs extended API — see bridge.h TODO
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ── RandomX PoW Hashing ──────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -125,12 +125,42 @@ 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 ──────────────────────────────────────────
|
||||
// TODO: extend API to accept kernel_hash, ring, last_pow_block_id,
|
||||
// stake_ki, pos_difficulty. Currently returns -1 (not implemented).
|
||||
// Legacy compatibility wrapper for the historical proof-only API.
|
||||
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)
|
||||
|
|
|
|||
67
crypto/compat/boost/multiprecision/cpp_int.hpp
Normal file
67
crypto/compat/boost/multiprecision/cpp_int.hpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// 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
|
||||
|
|
@ -578,10 +578,82 @@ func TestBGE_Bad_GarbageProof(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestZarcanum_Stub_NotImplemented(t *testing.T) {
|
||||
// Zarcanum bridge API needs extending — verify it returns false.
|
||||
func TestZarcanumCompatibilityWrapper_Bad_EmptyProof(t *testing.T) {
|
||||
hash := [32]byte{0x01}
|
||||
if crypto.VerifyZarcanum(hash, []byte{0x00}) {
|
||||
t.Fatal("Zarcanum stub should return false")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
125
crypto/proof.go
125
crypto/proof.go
|
|
@ -7,7 +7,59 @@ package crypto
|
|||
*/
|
||||
import "C"
|
||||
|
||||
import "unsafe"
|
||||
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
|
||||
}
|
||||
|
||||
// VerifyBPP verifies a Bulletproofs++ range proof (1 delta).
|
||||
// Used for zc_outs_range_proof in post-HF4 transactions.
|
||||
|
|
@ -74,9 +126,36 @@ 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.
|
||||
// Currently returns false — bridge API needs extending to pass kernel_hash,
|
||||
// ring, last_pow_block_id, stake_ki, and pos_difficulty.
|
||||
// This compatibility wrapper remains for the historical proof blob API.
|
||||
// Use VerifyZarcanumWithContext for full verification.
|
||||
func VerifyZarcanum(hash [32]byte, proof []byte) bool {
|
||||
if len(proof) == 0 {
|
||||
return false
|
||||
|
|
@ -87,3 +166,43 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
//
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <stdexcept>
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
#include "crypto.h"
|
||||
#include "eth_signature.h"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
cli "dappco.re/go/core/cli/pkg/cli"
|
||||
store "dappco.re/go/core/store"
|
||||
|
|
@ -29,31 +29,35 @@ import (
|
|||
// chain explorer --data-dir ~/.lethean/chain
|
||||
//
|
||||
// Use it alongside `AddChainCommands` to expose the TUI node view.
|
||||
func newChainExplorerCommand(dataDir, seed *string, testnet *bool) *cobra.Command {
|
||||
func newChainExplorerCommand(chainDataDir, seedPeerAddress *string, useTestnet *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(*dataDir, *seed, *testnet)
|
||||
return runChainExplorer(*chainDataDir, *seedPeerAddress, *useTestnet)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runChainExplorer(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureChainDataDirExists(dataDir); err != nil {
|
||||
func runChainExplorer(chainDataDir, seedPeerAddress string, useTestnet bool) error {
|
||||
if err := ensureChainDataDirExists(chainDataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
dbPath := filepath.Join(chainDataDir, "chain.db")
|
||||
chainStore, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
return coreerr.E("runChainExplorer", "open store", err)
|
||||
return corelog.E("runChainExplorer", "open store", err)
|
||||
}
|
||||
defer chainStore.Close()
|
||||
|
||||
blockchain := chain.New(chainStore)
|
||||
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed)
|
||||
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
|
@ -74,6 +78,7 @@ func runChainExplorer(dataDir, seed string, testnet bool) error {
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package p2p
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
|
@ -173,3 +174,29 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package p2p
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
|
|
@ -154,3 +155,75 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
86
p2p/version.go
Normal file
86
p2p/version.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// 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)
|
||||
}
|
||||
71
p2p/version_test.go
Normal file
71
p2p/version_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -8,14 +8,13 @@ package blockchain
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/blockchain/chain"
|
||||
"dappco.re/go/core/process"
|
||||
|
|
@ -32,7 +31,7 @@ import (
|
|||
// chain sync --stop
|
||||
//
|
||||
// It keeps the foreground and daemon modes behind a predictable command path.
|
||||
func newChainSyncCommand(dataDir, seed *string, testnet *bool) *cobra.Command {
|
||||
func newChainSyncCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool) *cobra.Command {
|
||||
var (
|
||||
daemon bool
|
||||
stop bool
|
||||
|
|
@ -42,14 +41,21 @@ func newChainSyncCommand(dataDir, seed *string, testnet *bool) *cobra.Command {
|
|||
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(*dataDir)
|
||||
return stopChainSyncDaemon(*chainDataDir)
|
||||
}
|
||||
if daemon {
|
||||
return runChainSyncDaemon(*dataDir, *seed, *testnet)
|
||||
return runChainSyncDaemon(*chainDataDir, *seedPeerAddress, *useTestnet)
|
||||
}
|
||||
return runChainSyncForeground(*dataDir, *seed, *testnet)
|
||||
return runChainSyncForeground(*chainDataDir, *seedPeerAddress, *useTestnet)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -59,36 +65,36 @@ func newChainSyncCommand(dataDir, seed *string, testnet *bool) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func runChainSyncForeground(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureChainDataDirExists(dataDir); err != nil {
|
||||
func runChainSyncForeground(chainDataDir, seedPeerAddress string, useTestnet bool) error {
|
||||
if err := ensureChainDataDirExists(chainDataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
dbPath := filepath.Join(chainDataDir, "chain.db")
|
||||
chainStore, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
return coreerr.E("runChainSyncForeground", "open store", err)
|
||||
return corelog.E("runChainSyncForeground", "open store", err)
|
||||
}
|
||||
defer chainStore.Close()
|
||||
|
||||
blockchain := chain.New(chainStore)
|
||||
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed)
|
||||
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
log.Println("Starting headless P2P sync...")
|
||||
corelog.Info("starting headless P2P sync", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet)
|
||||
runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed)
|
||||
log.Println("Sync stopped.")
|
||||
corelog.Info("headless P2P sync stopped", "data_dir", chainDataDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runChainSyncDaemon(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureChainDataDirExists(dataDir); err != nil {
|
||||
func runChainSyncDaemon(chainDataDir, seedPeerAddress string, useTestnet bool) error {
|
||||
if err := ensureChainDataDirExists(chainDataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pidFile := filepath.Join(dataDir, "sync.pid")
|
||||
pidFile := filepath.Join(chainDataDir, "sync.pid")
|
||||
|
||||
daemon := process.NewDaemon(process.DaemonOptions{
|
||||
PIDFile: pidFile,
|
||||
|
|
@ -100,25 +106,25 @@ func runChainSyncDaemon(dataDir, seed string, testnet bool) error {
|
|||
})
|
||||
|
||||
if err := daemon.Start(); err != nil {
|
||||
return coreerr.E("runChainSyncDaemon", "daemon start", err)
|
||||
return corelog.E("runChainSyncDaemon", "daemon start", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
dbPath := filepath.Join(chainDataDir, "chain.db")
|
||||
chainStore, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
_ = daemon.Stop()
|
||||
return coreerr.E("runChainSyncDaemon", "open store", err)
|
||||
return corelog.E("runChainSyncDaemon", "open store", err)
|
||||
}
|
||||
defer chainStore.Close()
|
||||
|
||||
blockchain := chain.New(chainStore)
|
||||
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed)
|
||||
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
daemon.SetReady(true)
|
||||
log.Println("Sync daemon started.")
|
||||
corelog.Info("sync daemon started", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
|
@ -132,22 +138,22 @@ func runChainSyncDaemon(dataDir, seed string, testnet bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func stopChainSyncDaemon(dataDir string) error {
|
||||
pidFile := filepath.Join(dataDir, "sync.pid")
|
||||
func stopChainSyncDaemon(chainDataDir string) error {
|
||||
pidFile := filepath.Join(chainDataDir, "sync.pid")
|
||||
pid, running := process.ReadPID(pidFile)
|
||||
if pid == 0 || !running {
|
||||
return coreerr.E("stopChainSyncDaemon", "no running sync daemon found", nil)
|
||||
return corelog.E("stopChainSyncDaemon", "no running sync daemon found", nil)
|
||||
}
|
||||
|
||||
processHandle, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return coreerr.E("stopChainSyncDaemon", fmt.Sprintf("find process %d", pid), err)
|
||||
return corelog.E("stopChainSyncDaemon", fmt.Sprintf("find process %d", pid), err)
|
||||
}
|
||||
|
||||
if err := processHandle.Signal(syscall.SIGTERM); err != nil {
|
||||
return coreerr.E("stopChainSyncDaemon", fmt.Sprintf("signal process %d", pid), err)
|
||||
return corelog.E("stopChainSyncDaemon", fmt.Sprintf("signal process %d", pid), err)
|
||||
}
|
||||
|
||||
log.Printf("Sent SIGTERM to sync daemon (PID %d)", pid)
|
||||
corelog.Info("sent SIGTERM to sync daemon", "pid", pid)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
60
sync_loop.go
60
sync_loop.go
|
|
@ -10,11 +10,10 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/blockchain/chain"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
|
|
@ -22,7 +21,7 @@ import (
|
|||
levin "dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig *config.ChainConfig, hardForks []config.HardFork, seed string) {
|
||||
func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig *config.ChainConfig, hardForks []config.HardFork, seedPeerAddress string) {
|
||||
opts := chain.SyncOptions{
|
||||
VerifySignatures: false,
|
||||
Forks: hardForks,
|
||||
|
|
@ -35,8 +34,8 @@ func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig
|
|||
default:
|
||||
}
|
||||
|
||||
if err := runChainSyncOnce(ctx, blockchain, chainConfig, opts, seed); err != nil {
|
||||
log.Printf("sync: %v (retrying in 10s)", err)
|
||||
if err := runChainSyncOnce(ctx, blockchain, chainConfig, opts, seedPeerAddress); err != nil {
|
||||
corelog.Warn("sync failed, retrying in 10s", "error", err, "seed", seedPeerAddress)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
|
@ -53,22 +52,27 @@ func runChainSyncLoop(ctx context.Context, blockchain *chain.Chain, chainConfig
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
return coreerr.E("runChainSyncOnce", fmt.Sprintf("dial %s", seed), err)
|
||||
return corelog.E("runChainSyncOnce", fmt.Sprintf("dial %s", seedPeerAddress), err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
levinConn := levin.NewConnection(conn)
|
||||
p2pConn := levin.NewConnection(conn)
|
||||
|
||||
var peerIDBytes [8]byte
|
||||
rand.Read(peerIDBytes[:])
|
||||
if _, err := rand.Read(peerIDBytes[:]); err != nil {
|
||||
return corelog.E("runChainSyncOnce", "generate peer id", err)
|
||||
}
|
||||
peerID := binary.LittleEndian.Uint64(peerIDBytes[:])
|
||||
|
||||
localHeight, _ := blockchain.Height()
|
||||
localHeight, err := blockchain.Height()
|
||||
if err != nil {
|
||||
return corelog.E("runChainSyncOnce", "get local height", err)
|
||||
}
|
||||
|
||||
handshakeReq := p2p.HandshakeRequest{
|
||||
handshakeRequest := p2p.HandshakeRequest{
|
||||
NodeData: p2p.NodeData{
|
||||
NetworkID: chainConfig.NetworkID,
|
||||
PeerID: peerID,
|
||||
|
|
@ -81,33 +85,37 @@ func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig
|
|||
NonPruningMode: true,
|
||||
},
|
||||
}
|
||||
payload, err := p2p.EncodeHandshakeRequest(&handshakeReq)
|
||||
payload, err := p2p.EncodeHandshakeRequest(&handshakeRequest)
|
||||
if err != nil {
|
||||
return coreerr.E("runChainSyncOnce", "encode handshake", err)
|
||||
return corelog.E("runChainSyncOnce", "encode handshake", err)
|
||||
}
|
||||
if err := levinConn.WritePacket(p2p.CommandHandshake, payload, true); err != nil {
|
||||
return coreerr.E("runChainSyncOnce", "write handshake", err)
|
||||
if err := p2pConn.WritePacket(p2p.CommandHandshake, payload, true); err != nil {
|
||||
return corelog.E("runChainSyncOnce", "write handshake", err)
|
||||
}
|
||||
|
||||
hdr, data, err := levinConn.ReadPacket()
|
||||
packetHeader, packetData, err := p2pConn.ReadPacket()
|
||||
if err != nil {
|
||||
return coreerr.E("runChainSyncOnce", "read handshake", err)
|
||||
return corelog.E("runChainSyncOnce", "read handshake", err)
|
||||
}
|
||||
if hdr.Command != uint32(p2p.CommandHandshake) {
|
||||
return coreerr.E("runChainSyncOnce", fmt.Sprintf("unexpected command %d", hdr.Command), nil)
|
||||
if packetHeader.Command != uint32(p2p.CommandHandshake) {
|
||||
return corelog.E("runChainSyncOnce", fmt.Sprintf("unexpected command %d", packetHeader.Command), nil)
|
||||
}
|
||||
|
||||
var handshakeResp p2p.HandshakeResponse
|
||||
if err := handshakeResp.Decode(data); err != nil {
|
||||
return coreerr.E("runChainSyncOnce", "decode handshake", err)
|
||||
var handshakeResponse p2p.HandshakeResponse
|
||||
if err := handshakeResponse.Decode(packetData); err != nil {
|
||||
return corelog.E("runChainSyncOnce", "decode handshake", err)
|
||||
}
|
||||
|
||||
localSync := p2p.CoreSyncData{
|
||||
if err := p2p.ValidateHandshakeResponse(&handshakeResponse, chainConfig.NetworkID, chainConfig.IsTestnet); err != nil {
|
||||
return corelog.E("runChainSyncOnce", "validate handshake", err)
|
||||
}
|
||||
|
||||
localSyncData := p2p.CoreSyncData{
|
||||
CurrentHeight: localHeight,
|
||||
ClientVersion: config.ClientVersion,
|
||||
NonPruningMode: true,
|
||||
}
|
||||
p2pConn := chain.NewLevinP2PConn(levinConn, handshakeResp.PayloadData.CurrentHeight, localSync)
|
||||
p2pConnection := chain.NewLevinP2PConn(p2pConn, handshakeResponse.PayloadData.CurrentHeight, localSyncData)
|
||||
|
||||
return blockchain.P2PSync(ctx, p2pConn, opts)
|
||||
return blockchain.P2PSync(ctx, p2pConnection, opts)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -309,26 +309,19 @@ func (m *ExplorerModel) viewTxDetail() string {
|
|||
if len(tx.Vin) > 0 {
|
||||
b.WriteString(" Inputs:\n")
|
||||
for i, in := range tx.Vin {
|
||||
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))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" [%d] %s\n", i, describeTxInput(in)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(tx.Vout) > 0 {
|
||||
b.WriteString("\n Outputs:\n")
|
||||
for i, out := range tx.Vout {
|
||||
switch v := out.(type) {
|
||||
for i, output := range tx.Vout {
|
||||
switch v := output.(type) {
|
||||
case types.TxOutputBare:
|
||||
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]))
|
||||
if targetKey, ok := v.SpendKey(); ok {
|
||||
b.WriteString(fmt.Sprintf(" [%d] bare amount=%d key=%x\n", i, v.Amount, targetKey[:4]))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf(" [%d] bare amount=%d target=%T\n", i, v.Amount, v.Target))
|
||||
b.WriteString(fmt.Sprintf(" [%d] bare amount=%d %s\n", i, v.Amount, describeTxOutTarget(v.Target)))
|
||||
}
|
||||
case types.TxOutputZarcanum:
|
||||
b.WriteString(fmt.Sprintf(" [%d] zarcanum stealth=%x\n", i, v.StealthAddress[:4]))
|
||||
|
|
@ -341,6 +334,41 @@ 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() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
func TestExplorerModel_View_Good_BlockList(t *testing.T) {
|
||||
|
|
@ -174,3 +176,52 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ 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.
|
||||
|
|
@ -41,11 +44,7 @@ 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 {
|
||||
// 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
|
||||
return IsIntegratedPrefix(a.Prefix)
|
||||
}
|
||||
|
||||
// IsIntegratedPrefix reports whether the given prefix corresponds to an
|
||||
|
|
@ -79,6 +78,8 @@ 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 {
|
||||
|
|
@ -117,6 +118,7 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +107,27 @@ 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
|
||||
|
|
|
|||
143
types/asset.go
Normal file
143
types/asset.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// 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
|
||||
}
|
||||
140
types/asset_test.go
Normal file
140
types/asset_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +121,15 @@ 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 {
|
||||
|
|
@ -219,7 +228,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
|
||||
}
|
||||
|
|
@ -237,6 +246,19 @@ 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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,46 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ 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"
|
||||
)
|
||||
|
|
@ -107,6 +108,7 @@ func (a *Account) Address() types.Address {
|
|||
return types.Address{
|
||||
SpendPublicKey: a.SpendPublicKey,
|
||||
ViewPublicKey: a.ViewPublicKey,
|
||||
Prefix: config.AddressPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, out := range tx.Vout {
|
||||
bare, ok := out.(types.TxOutputBare)
|
||||
for i, output := range tx.Vout {
|
||||
bare, ok := output.(types.TxOutputBare)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
expectedPub, err := crypto.DerivePublicKey(
|
||||
expectedPublicKey, err := crypto.DerivePublicKey(
|
||||
derivation, uint64(i), [32]byte(s.account.SpendPublicKey))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
toKey, ok := bare.Target.(types.TxOutToKey)
|
||||
targetKey, ok := bare.SpendKey()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if types.PublicKey(expectedPub) != toKey.Key {
|
||||
if types.PublicKey(expectedPublicKey) != targetKey {
|
||||
continue
|
||||
}
|
||||
|
||||
ephSec, err := crypto.DeriveSecretKey(
|
||||
ephemeralSecretKey, err := crypto.DeriveSecretKey(
|
||||
derivation, uint64(i), [32]byte(s.account.SpendSecretKey))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ki, err := crypto.GenerateKeyImage(expectedPub, ephSec)
|
||||
keyImage, err := crypto.GenerateKeyImage(expectedPublicKey, ephemeralSecretKey)
|
||||
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(expectedPub),
|
||||
Secret: types.SecretKey(ephSec),
|
||||
Public: types.PublicKey(expectedPublicKey),
|
||||
Secret: types.SecretKey(ephemeralSecretKey),
|
||||
},
|
||||
KeyImage: types.KeyImage(ki),
|
||||
KeyImage: types.KeyImage(keyImage),
|
||||
Coinbase: isCoinbase,
|
||||
UnlockTime: extra.UnlockTime,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -122,17 +122,23 @@ func (w *Wallet) scanTx(tx *types.Transaction, blockHeight uint64) error {
|
|||
|
||||
// Check key images for spend detection.
|
||||
for _, vin := range tx.Vin {
|
||||
toKey, ok := vin.(types.TxInputToKey)
|
||||
if !ok {
|
||||
var keyImage types.KeyImage
|
||||
switch v := vin.(type) {
|
||||
case types.TxInputToKey:
|
||||
keyImage = v.KeyImage
|
||||
case types.TxInputHTLC:
|
||||
keyImage = v.KeyImage
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to mark any matching transfer as spent.
|
||||
tr, err := getTransfer(w.store, toKey.KeyImage)
|
||||
tr, err := getTransfer(w.store, keyImage)
|
||||
if err != nil {
|
||||
continue // not our transfer
|
||||
}
|
||||
if !tr.Spent {
|
||||
markTransferSpent(w.store, toKey.KeyImage, blockHeight)
|
||||
markTransferSpent(w.store, keyImage, blockHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,3 +133,55 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ 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
|
||||
|
|
@ -58,6 +60,8 @@ 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)
|
||||
}
|
||||
|
|
@ -65,6 +69,8 @@ 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)
|
||||
|
|
|
|||
|
|
@ -176,6 +176,9 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -268,6 +271,63 @@ 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
|
||||
|
|
@ -279,26 +339,12 @@ func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) {
|
|||
case types.TxOutputBare:
|
||||
enc.WriteVarint(v.Amount)
|
||||
// Target is a variant (txout_target_v)
|
||||
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))
|
||||
if !encodeTxOutTarget(enc, v.Target, "encodeOutputsV1") {
|
||||
return
|
||||
}
|
||||
default:
|
||||
enc.err = coreerr.E("encodeOutputsV1", fmt.Sprintf("wire: unsupported output type %T", out), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -316,31 +362,8 @@ func decodeOutputsV1(dec *Decoder) []types.TxOutput {
|
|||
if dec.Err() != nil {
|
||||
return vout
|
||||
}
|
||||
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)
|
||||
out.Target = decodeTxOutTarget(dec, tag, "decodeOutputsV1")
|
||||
if dec.Err() != nil {
|
||||
return vout
|
||||
}
|
||||
vout = append(vout, out)
|
||||
|
|
@ -356,25 +379,8 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) {
|
|||
switch v := out.(type) {
|
||||
case types.TxOutputBare:
|
||||
enc.WriteVarint(v.Amount)
|
||||
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))
|
||||
if !encodeTxOutTarget(enc, v.Target, "encodeOutputsV2") {
|
||||
return
|
||||
}
|
||||
case types.TxOutputZarcanum:
|
||||
enc.WriteBlob32((*[32]byte)(&v.StealthAddress))
|
||||
|
|
@ -383,6 +389,9 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -403,31 +412,8 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput {
|
|||
var out types.TxOutputBare
|
||||
out.Amount = dec.ReadVarint()
|
||||
targetTag := dec.ReadVariantTag()
|
||||
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)
|
||||
out.Target = decodeTxOutTarget(dec, targetTag, "decodeOutputsV2")
|
||||
if dec.Err() != nil {
|
||||
return vout
|
||||
}
|
||||
vout = append(vout, out)
|
||||
|
|
@ -531,10 +517,10 @@ const (
|
|||
tagZarcanumSig = 45 // zarcanum_sig — complex
|
||||
|
||||
// Asset operation tags (HF5 confidential assets).
|
||||
tagAssetDescriptorOperation = 40 // asset_descriptor_operation
|
||||
tagAssetOperationProof = 49 // asset_operation_proof
|
||||
tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof
|
||||
tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth
|
||||
tagAssetDescriptorOperation = types.AssetDescriptorOperationTag // 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>
|
||||
|
|
@ -586,6 +572,12 @@ 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)
|
||||
|
|
@ -794,6 +786,96 @@ 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 {
|
||||
|
|
@ -1043,113 +1125,7 @@ func readZarcanumSig(dec *Decoder) []byte {
|
|||
// decimal_point(uint8) + meta_info(string) + owner_key(32 bytes) +
|
||||
// etc(vector<uint8>).
|
||||
func readAssetDescriptorOperation(dec *Decoder) []byte {
|
||||
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...)
|
||||
|
||||
raw, _ := parseAssetDescriptorOperation(dec)
|
||||
return raw
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -987,3 +987,225 @@ 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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,23 @@ 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) {
|
||||
|
|
@ -110,6 +127,23 @@ 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) {
|
||||
|
|
@ -136,6 +170,17 @@ 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 {
|
||||
|
|
@ -273,9 +318,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)
|
||||
|
|
@ -289,13 +334,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
|
||||
|
|
|
|||
214
wire/variant.go
Normal file
214
wire/variant.go
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// 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),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue