Compare commits
56 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 | ||
|
|
a2df164822 | ||
|
|
243749a6d8 | ||
| d004158022 | |||
|
|
34128d8e98 | ||
|
|
6370d96c31 | ||
|
|
2b145d6ebf | ||
|
|
abb1e2b748 | ||
|
|
97c5510184 | ||
|
|
772cd1b0fd | ||
|
|
70fab6f7d0 | ||
|
|
89b0375e18 |
144 changed files with 4340 additions and 965 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
|||
crypto/build/
|
||||
.core/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
.core/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
Go implementation of the Lethean blockchain protocol (CryptoNote/Zano-fork) with a CGo crypto bridge.
|
||||
|
||||
Module: `forge.lthn.ai/core/go-blockchain`
|
||||
Module: `dappco.re/go/core/blockchain`
|
||||
Licence: EUPL-1.2 (every source file carries the copyright header)
|
||||
|
||||
## Build
|
||||
|
|
@ -42,7 +42,7 @@ go test -tags integration ./... # integration tests (need C++ te
|
|||
- Co-Author trailer: `Co-Authored-By: Charon <charon@lethean.io>`
|
||||
- Error strings: `package: description` format (e.g. `types: invalid hex for hash`)
|
||||
- Error wrapping: `fmt.Errorf("package: description: %w", err)`
|
||||
- Import order: stdlib, then `golang.org/x`, then `forge.lthn.ai`, blank lines between groups
|
||||
- Import order: stdlib, then `golang.org/x`, then `dappco.re`, blank lines between groups
|
||||
- No emojis in code or comments
|
||||
|
||||
## Test Conventions
|
||||
|
|
@ -77,9 +77,9 @@ types / config ← leaf packages (stdlib only, no internal deps)
|
|||
- Block hash includes a varint length prefix: `Keccak256(varint(len) || block_hashing_blob)`.
|
||||
- Two P2P varint formats exist: CryptoNote LEB128 (`wire/`) and portable storage 2-bit size mark (`go-p2p/node/levin/`).
|
||||
|
||||
**Binary:** `cmd/core-chain/` — cobra CLI via `forge.lthn.ai/core/cli`. Subcommands: `chain sync` (P2P block sync) and `chain explorer` (TUI dashboard).
|
||||
**Binary:** `cmd/core-chain/` — cobra CLI via `dappco.re/go/core/cli`. Subcommands: `chain sync` (P2P block sync) and `chain explorer` (TUI dashboard).
|
||||
|
||||
**Local replace directives:** `go.mod` uses local `replace` for sibling `forge.lthn.ai/core/*` modules.
|
||||
**Local replace directives:** `go.mod` uses `replace` to map `dappco.re/go/core/*` paths to `forge.lthn.ai/core/*` modules during migration.
|
||||
|
||||
## Docs
|
||||
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Pure Go implementation of the Lethean blockchain protocol. Provides chain configuration, core cryptographic data types, CryptoNote wire serialisation, and LWMA difficulty adjustment for the Lethean CryptoNote/Zano-fork chain. Follows ADR-001: protocol logic in Go, cryptographic primitives deferred to a C++ bridge in later phases. Lineage: CryptoNote to IntenseCoin (2017) to Lethean to Zano rebase.
|
||||
|
||||
**Module**: `forge.lthn.ai/core/go-blockchain`
|
||||
**Module**: `dappco.re/go/core/blockchain`
|
||||
**Licence**: EUPL-1.2
|
||||
**Language**: Go 1.25
|
||||
|
||||
|
|
@ -10,10 +10,10 @@ Pure Go implementation of the Lethean blockchain protocol. Provides chain config
|
|||
|
||||
```go
|
||||
import (
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"forge.lthn.ai/core/go-blockchain/difficulty"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/difficulty"
|
||||
)
|
||||
|
||||
// Query the active hardfork version at a given block height
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
package chain
|
||||
|
||||
import (
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
store "dappco.re/go/core/store"
|
||||
)
|
||||
|
||||
// Chain manages blockchain storage and indexing.
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ package chain
|
|||
import (
|
||||
"testing"
|
||||
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
store "dappco.re/go/core/store"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
func newTestChain(t *testing.T) *Chain {
|
||||
|
|
@ -219,8 +219,9 @@ func TestChain_GetBlockByHeight_NotFound(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatal("GetBlockByHeight(99): expected error, got nil")
|
||||
}
|
||||
if got := err.Error(); got != "chain: block 99 not found" {
|
||||
t.Errorf("error message: got %q, want %q", got, "chain: block 99 not found")
|
||||
want := "Chain.GetBlockByHeight: chain: block 99 not found"
|
||||
if got := err.Error(); got != want {
|
||||
t.Errorf("error message: got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ package chain
|
|||
import (
|
||||
"math/big"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/difficulty"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/difficulty"
|
||||
)
|
||||
|
||||
// nextDifficultyWith computes the expected difficulty for the block at the
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ package chain
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
store "dappco.re/go/core/store"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
package chain
|
||||
|
||||
import "forge.lthn.ai/core/go-blockchain/types"
|
||||
import "dappco.re/go/core/blockchain/types"
|
||||
|
||||
// SparseChainHistory builds the exponentially-spaced block hash list used by
|
||||
// NOTIFY_REQUEST_CHAIN. Matches the C++ get_short_chain_history() algorithm:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ package chain
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
store "dappco.re/go/core/store"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/p2p"
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
levin "forge.lthn.ai/core/go-p2p/node/levin"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/p2p"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
levin "dappco.re/go/core/p2p/node/levin"
|
||||
store "dappco.re/go/core/store"
|
||||
)
|
||||
|
||||
const testnetRPCAddr = "http://localhost:46941"
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@
|
|||
package chain
|
||||
|
||||
import (
|
||||
"log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/p2p"
|
||||
levinpkg "forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/blockchain/p2p"
|
||||
levinpkg "dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
// LevinP2PConn adapts a Levin connection to the P2PConnection interface.
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
package chain
|
||||
|
||||
import "forge.lthn.ai/core/go-blockchain/types"
|
||||
import "dappco.re/go/core/blockchain/types"
|
||||
|
||||
// BlockMeta holds metadata stored alongside each block.
|
||||
type BlockMeta struct {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ package chain
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
store "dappco.re/go/core/store"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ package chain
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/consensus"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// GetRingOutputs fetches the public keys for the given global output indices
|
||||
// at the specified amount. This implements the consensus.RingOutputsFn
|
||||
// signature for use during signature verification.
|
||||
func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicKey, error) {
|
||||
pubs := make([]types.PublicKey, len(offsets))
|
||||
// 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, fmt.Errorf("ring output %d: unsupported target type %T", i, out.Target)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ package chain
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
func TestGetRingOutputs_Good(t *testing.T) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
store "dappco.re/go/core/store"
|
||||
)
|
||||
|
||||
// Storage group constants matching the design schema.
|
||||
|
|
@ -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,17 +11,16 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/consensus"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
const syncBatchSize = 10
|
||||
|
|
@ -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,16 +12,17 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
store "dappco.re/go/core/store"
|
||||
)
|
||||
|
||||
// makeGenesisBlockBlob creates a minimal genesis block and returns its hex blob and hash.
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,17 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"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 "forge.lthn.ai/core/go-store"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
chain_commands.go
Normal file
92
chain_commands.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// 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 blockchain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const defaultChainSeed = "seeds.lthn.io:36942"
|
||||
|
||||
// AddChainCommands registers the `chain` command group on a Cobra root.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// cli.WithCommands("chain", blockchain.AddChainCommands)
|
||||
//
|
||||
// The command group owns the explorer and sync subcommands, so the
|
||||
// command path documents the node features directly.
|
||||
func AddChainCommands(root *cobra.Command) {
|
||||
var (
|
||||
chainDataDir string
|
||||
seedPeerAddress string
|
||||
useTestnet bool
|
||||
)
|
||||
|
||||
chainCmd := &cobra.Command{
|
||||
Use: "chain",
|
||||
Short: "Lethean blockchain node",
|
||||
Long: "Manage the Lethean blockchain — sync, explore, and mine.",
|
||||
}
|
||||
|
||||
chainCmd.PersistentFlags().StringVar(&chainDataDir, "data-dir", defaultChainDataDirPath(), "blockchain data directory")
|
||||
chainCmd.PersistentFlags().StringVar(&seedPeerAddress, "seed", defaultChainSeed, "seed peer address (host:port)")
|
||||
chainCmd.PersistentFlags().BoolVar(&useTestnet, "testnet", false, "use testnet")
|
||||
|
||||
chainCmd.AddCommand(
|
||||
newChainExplorerCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
|
||||
newChainSyncCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
|
||||
)
|
||||
|
||||
root.AddCommand(chainCmd)
|
||||
}
|
||||
|
||||
func chainConfigForSeed(useTestnet bool, seedPeerAddress string) (config.ChainConfig, []config.HardFork, string) {
|
||||
if useTestnet {
|
||||
if seedPeerAddress == defaultChainSeed {
|
||||
seedPeerAddress = "localhost:46942"
|
||||
}
|
||||
return config.Testnet, config.TestnetForks, seedPeerAddress
|
||||
}
|
||||
return config.Mainnet, config.MainnetForks, seedPeerAddress
|
||||
}
|
||||
|
||||
func defaultChainDataDirPath() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ".lethean"
|
||||
}
|
||||
return filepath.Join(home, ".lethean", "chain")
|
||||
}
|
||||
|
||||
func ensureChainDataDirExists(dataDir string) error {
|
||||
if err := coreio.Local.EnsureDir(dataDir); err != nil {
|
||||
return coreerr.E("ensureChainDataDirExists", "create data dir", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
cli "forge.lthn.ai/core/cli/pkg/cli"
|
||||
blockchain "forge.lthn.ai/core/go-blockchain"
|
||||
cli "dappco.re/go/core/cli/pkg/cli"
|
||||
blockchain "dappco.re/go/core/blockchain"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||
//
|
||||
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
|
||||
cli "forge.lthn.ai/core/cli/pkg/cli"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/chain"
|
||||
"forge.lthn.ai/core/go-blockchain/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newExplorerCmd(dataDir, seed *string, testnet *bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "explorer",
|
||||
Short: "TUI block explorer",
|
||||
Long: "Interactive terminal block explorer with live sync status.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runExplorer(*dataDir, *seed, *testnet)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runExplorer(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureDataDir(dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
s, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
return coreerr.E("runExplorer", "open store", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
c := chain.New(s)
|
||||
cfg, forks := resolveConfig(testnet, &seed)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
syncLoop(ctx, c, &cfg, forks, seed)
|
||||
}()
|
||||
|
||||
node := tui.NewNode(c)
|
||||
status := tui.NewStatusModel(node)
|
||||
explorer := tui.NewExplorerModel(c)
|
||||
hints := tui.NewKeyHintsModel()
|
||||
|
||||
frame := cli.NewFrame("HCF")
|
||||
frame.Header(status)
|
||||
frame.Content(explorer)
|
||||
frame.Footer(hints)
|
||||
frame.Run()
|
||||
|
||||
cancel() // Signal syncLoop to stop.
|
||||
wg.Wait() // Wait for it before closing store.
|
||||
return nil
|
||||
}
|
||||
144
cmd_sync.go
144
cmd_sync.go
|
|
@ -1,144 +0,0 @@
|
|||
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||
//
|
||||
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/chain"
|
||||
"forge.lthn.ai/core/go-process"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newSyncCmd(dataDir, seed *string, testnet *bool) *cobra.Command {
|
||||
var (
|
||||
daemon bool
|
||||
stop bool
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Headless P2P chain sync",
|
||||
Long: "Sync the blockchain from P2P peers without the TUI explorer.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if stop {
|
||||
return stopSyncDaemon(*dataDir)
|
||||
}
|
||||
if daemon {
|
||||
return runSyncDaemon(*dataDir, *seed, *testnet)
|
||||
}
|
||||
return runSyncForeground(*dataDir, *seed, *testnet)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&daemon, "daemon", false, "run as background daemon")
|
||||
cmd.Flags().BoolVar(&stop, "stop", false, "stop a running sync daemon")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runSyncForeground(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureDataDir(dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
s, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
return coreerr.E("runSyncForeground", "open store", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
c := chain.New(s)
|
||||
cfg, forks := resolveConfig(testnet, &seed)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
log.Println("Starting headless P2P sync...")
|
||||
syncLoop(ctx, c, &cfg, forks, seed)
|
||||
log.Println("Sync stopped.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSyncDaemon(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureDataDir(dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pidFile := filepath.Join(dataDir, "sync.pid")
|
||||
|
||||
d := process.NewDaemon(process.DaemonOptions{
|
||||
PIDFile: pidFile,
|
||||
Registry: process.DefaultRegistry(),
|
||||
RegistryEntry: process.DaemonEntry{
|
||||
Code: "forge.lthn.ai/core/go-blockchain",
|
||||
Daemon: "sync",
|
||||
},
|
||||
})
|
||||
|
||||
if err := d.Start(); err != nil {
|
||||
return coreerr.E("runSyncDaemon", "daemon start", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
s, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
_ = d.Stop()
|
||||
return coreerr.E("runSyncDaemon", "open store", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
c := chain.New(s)
|
||||
cfg, forks := resolveConfig(testnet, &seed)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
d.SetReady(true)
|
||||
log.Println("Sync daemon started.")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
syncLoop(ctx, c, &cfg, forks, seed)
|
||||
}()
|
||||
|
||||
err = d.Run(ctx)
|
||||
wg.Wait() // Wait for syncLoop to finish before closing store.
|
||||
return err
|
||||
}
|
||||
|
||||
func stopSyncDaemon(dataDir string) error {
|
||||
pidFile := filepath.Join(dataDir, "sync.pid")
|
||||
pid, running := process.ReadPID(pidFile)
|
||||
if pid == 0 || !running {
|
||||
return coreerr.E("stopSyncDaemon", "no running sync daemon found", nil)
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return coreerr.E("stopSyncDaemon", fmt.Sprintf("find process %d", pid), err)
|
||||
}
|
||||
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
return coreerr.E("stopSyncDaemon", fmt.Sprintf("signal process %d", pid), err)
|
||||
}
|
||||
|
||||
log.Printf("Sent SIGTERM to sync daemon (PID %d)", pid)
|
||||
return nil
|
||||
}
|
||||
69
commands.go
69
commands.go
|
|
@ -1,69 +0,0 @@
|
|||
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||
//
|
||||
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// AddChainCommands registers the "chain" command group with explorer
|
||||
// and sync subcommands.
|
||||
func AddChainCommands(root *cobra.Command) {
|
||||
var (
|
||||
dataDir string
|
||||
seed string
|
||||
testnet bool
|
||||
)
|
||||
|
||||
chainCmd := &cobra.Command{
|
||||
Use: "chain",
|
||||
Short: "Lethean blockchain node",
|
||||
Long: "Manage the Lethean blockchain — sync, explore, and mine.",
|
||||
}
|
||||
|
||||
chainCmd.PersistentFlags().StringVar(&dataDir, "data-dir", defaultDataDir(), "blockchain data directory")
|
||||
chainCmd.PersistentFlags().StringVar(&seed, "seed", "seeds.lthn.io:36942", "seed peer address (host:port)")
|
||||
chainCmd.PersistentFlags().BoolVar(&testnet, "testnet", false, "use testnet")
|
||||
|
||||
chainCmd.AddCommand(
|
||||
newExplorerCmd(&dataDir, &seed, &testnet),
|
||||
newSyncCmd(&dataDir, &seed, &testnet),
|
||||
)
|
||||
|
||||
root.AddCommand(chainCmd)
|
||||
}
|
||||
|
||||
func resolveConfig(testnet bool, seed *string) (config.ChainConfig, []config.HardFork) {
|
||||
if testnet {
|
||||
if *seed == "seeds.lthn.io:36942" {
|
||||
*seed = "localhost:46942"
|
||||
}
|
||||
return config.Testnet, config.TestnetForks
|
||||
}
|
||||
return config.Mainnet, config.MainnetForks
|
||||
}
|
||||
|
||||
func defaultDataDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ".lethean"
|
||||
}
|
||||
return filepath.Join(home, ".lethean", "chain")
|
||||
}
|
||||
|
||||
func ensureDataDir(dataDir string) error {
|
||||
if err := coreio.Local.EnsureDir(dataDir); err != nil {
|
||||
return coreerr.E("ensureDataDir", "create data dir", 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)
|
||||
}
|
||||
|
|
@ -9,10 +9,10 @@ import (
|
|||
"fmt"
|
||||
"slices"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// IsPoS returns true if the block flags indicate a Proof-of-Stake block.
|
||||
|
|
@ -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 fmt.Errorf("%w: got %d, want %d at height %d",
|
||||
ErrBlockMajorVersion, blk.MajorVersion, expected, height)
|
||||
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,10 +274,12 @@ 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) {
|
||||
return fmt.Errorf("%w: height %d is within HF5 freeze window", ErrPreHardforkFreeze, height)
|
||||
return coreerr.E("ValidateTransactionInBlock", fmt.Sprintf("height %d is within HF5 freeze window", height), ErrPreHardforkFreeze)
|
||||
}
|
||||
|
||||
return ValidateTransaction(tx, txBlob, forks, height)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,14 +9,16 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
func TestIsPreHardforkFreeze_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
store "dappco.re/go/core/store"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/chain"
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/chain"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
)
|
||||
|
||||
func TestConsensusIntegration(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import (
|
|||
"encoding/binary"
|
||||
"math/big"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/crypto"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// maxTarget is 2^256, used for difficulty comparison.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,16 @@ import (
|
|||
"fmt"
|
||||
"math/bits"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
|
|||
157
consensus/tx.go
157
consensus/tx.go
|
|
@ -8,16 +8,40 @@ package consensus
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"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, state, height); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. Blob size.
|
||||
if uint64(len(txBlob)) >= config.MaxTransactionBlobSize {
|
||||
|
|
@ -33,12 +57,17 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
|||
}
|
||||
|
||||
// 3. Input types — TxInputGenesis not allowed in regular transactions.
|
||||
if err := checkInputTypes(tx, hf4Active); err != nil {
|
||||
if err := checkInputTypes(tx, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. Output validation.
|
||||
if err := checkOutputs(tx, 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
|
||||
}
|
||||
|
||||
|
|
@ -56,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
|
||||
}
|
||||
|
|
@ -65,29 +94,68 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
|||
return nil
|
||||
}
|
||||
|
||||
func checkInputTypes(tx *types.Transaction, hf4Active bool) error {
|
||||
// checkTxVersion validates that the transaction version is appropriate for the
|
||||
// current hardfork era.
|
||||
//
|
||||
// Pre-HF4: regular transactions must use version 1.
|
||||
// HF4 era: regular transactions must use version 2.
|
||||
// HF5+: transaction version must be exactly version 3 and the embedded
|
||||
// hardfork_id must match the active hardfork version.
|
||||
func checkTxVersion(tx *types.Transaction, state transactionForkState, height uint64) error {
|
||||
var expectedVersion uint64
|
||||
switch {
|
||||
case state.hardForkFiveActive:
|
||||
expectedVersion = types.VersionPostHF5
|
||||
case state.hardForkFourActive:
|
||||
expectedVersion = types.VersionPostHF4
|
||||
default:
|
||||
expectedVersion = types.VersionPreHF4
|
||||
}
|
||||
|
||||
if tx.Version != expectedVersion {
|
||||
return coreerr.E("checkTxVersion",
|
||||
fmt.Sprintf("version %d invalid at height %d (expected %d)", tx.Version, height, expectedVersion),
|
||||
ErrTxVersionInvalid)
|
||||
}
|
||||
|
||||
if tx.Version >= types.VersionPostHF5 && tx.HardforkID != state.activeHardForkVersion {
|
||||
return coreerr.E("checkTxVersion",
|
||||
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, state transactionForkState) error {
|
||||
for _, vin := range tx.Vin {
|
||||
switch vin.(type) {
|
||||
case types.TxInputToKey:
|
||||
// Always valid.
|
||||
case types.TxInputGenesis:
|
||||
return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType)
|
||||
default:
|
||||
// Future types (multisig, HTLC, ZC) — accept if HF4+.
|
||||
if !hf4Active {
|
||||
case types.TxInputHTLC, types.TxInputMultisig:
|
||||
// HTLC and multisig inputs require at least HF1.
|
||||
if !state.hardForkOneActive {
|
||||
return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF1", vin.InputType()), ErrInvalidInputType)
|
||||
}
|
||||
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, 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)
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +169,24 @@ func checkOutputs(tx *types.Transaction, hf4Active bool) error {
|
|||
if o.Amount == 0 {
|
||||
return coreerr.E("checkOutputs", fmt.Sprintf("output %d has zero amount", i), ErrInvalidOutput)
|
||||
}
|
||||
// Only known transparent output targets are accepted.
|
||||
switch o.Target.(type) {
|
||||
case types.TxOutToKey:
|
||||
case types.TxOutHTLC, types.TxOutMultisig:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,14 +196,49 @@ func checkOutputs(tx *types.Transaction, hf4Active bool) error {
|
|||
func checkKeyImages(tx *types.Transaction) error {
|
||||
seen := make(map[types.KeyImage]struct{})
|
||||
for _, vin := range tx.Vin {
|
||||
toKey, ok := vin.(types.TxInputToKey)
|
||||
if !ok {
|
||||
var ki types.KeyImage
|
||||
switch v := vin.(type) {
|
||||
case types.TxInputToKey:
|
||||
ki = v.KeyImage
|
||||
case types.TxInputHTLC:
|
||||
ki = v.KeyImage
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[toKey.KeyImage]; exists {
|
||||
return coreerr.E("checkKeyImages", toKey.KeyImage.String(), ErrDuplicateKeyImage)
|
||||
if _, exists := seen[ki]; exists {
|
||||
return coreerr.E("checkKeyImages", ki.String(), ErrDuplicateKeyImage)
|
||||
}
|
||||
seen[toKey.KeyImage] = struct{}{}
|
||||
seen[ki] = struct{}{}
|
||||
}
|
||||
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"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"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) {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// validV2Tx returns a minimal valid v2 (Zarcanum) transaction for testing.
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
// zcSigData holds the parsed components of a ZC_sig variant element
|
||||
|
|
|
|||
|
|
@ -11,13 +11,25 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"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"
|
||||
)
|
||||
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@ package consensus
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/crypto"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"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,27 +52,29 @@ 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 {
|
||||
// Count key inputs.
|
||||
var keyInputCount int
|
||||
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
|
||||
for _, vin := range tx.Vin {
|
||||
if _, ok := vin.(types.TxInputToKey); ok {
|
||||
keyInputCount++
|
||||
switch vin.(type) {
|
||||
case types.TxInputToKey, types.TxInputHTLC:
|
||||
ringInputCount++
|
||||
}
|
||||
}
|
||||
|
||||
if len(tx.Signatures) != keyInputCount {
|
||||
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), keyInputCount), nil)
|
||||
if len(tx.Signatures) != ringInputCount {
|
||||
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), ringInputCount), nil)
|
||||
}
|
||||
|
||||
// Actual NLSAG verification requires the crypto bridge and ring outputs.
|
||||
|
|
@ -82,18 +87,31 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err
|
|||
|
||||
var sigIdx int
|
||||
for _, vin := range tx.Vin {
|
||||
inp, ok := vin.(types.TxInputToKey)
|
||||
if !ok {
|
||||
continue
|
||||
// Extract amount and key offsets from ring-signing input types.
|
||||
var amount uint64
|
||||
var keyOffsets []types.TxOutRef
|
||||
var keyImage types.KeyImage
|
||||
|
||||
switch v := vin.(type) {
|
||||
case types.TxInputToKey:
|
||||
amount = v.Amount
|
||||
keyOffsets = v.KeyOffsets
|
||||
keyImage = v.KeyImage
|
||||
case types.TxInputHTLC:
|
||||
amount = v.Amount
|
||||
keyOffsets = v.KeyOffsets
|
||||
keyImage = v.KeyImage
|
||||
default:
|
||||
continue // TxInputMultisig and others do not use NLSAG
|
||||
}
|
||||
|
||||
// Extract absolute global indices from key offsets.
|
||||
offsets := make([]uint64, len(inp.KeyOffsets))
|
||||
for i, ref := range inp.KeyOffsets {
|
||||
offsets := make([]uint64, len(keyOffsets))
|
||||
for i, ref := range keyOffsets {
|
||||
offsets[i] = ref.GlobalIndex
|
||||
}
|
||||
|
||||
ringKeys, err := getRingOutputs(inp.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)
|
||||
}
|
||||
|
|
@ -114,7 +132,7 @@ func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) err
|
|||
sigs[i] = [64]byte(s)
|
||||
}
|
||||
|
||||
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(inp.KeyImage), pubs, sigs) {
|
||||
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(keyImage), pubs, sigs) {
|
||||
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: ring signature verification failed for input %d", sigIdx), nil)
|
||||
}
|
||||
|
||||
|
|
@ -137,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:
|
||||
|
|
@ -148,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/crypto"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ package crypto
|
|||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// PointMul8 multiplies a curve point by the cofactor 8.
|
||||
|
|
@ -20,7 +21,7 @@ func PointMul8(pk [32]byte) ([32]byte, error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return result, errors.New("crypto: point_mul8 failed")
|
||||
return result, coreerr.E("PointMul8", "point_mul8 failed", nil)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -34,7 +35,7 @@ func PointDiv8(pk [32]byte) ([32]byte, error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return result, errors.New("crypto: point_div8 failed")
|
||||
return result, coreerr.E("PointDiv8", "point_div8 failed", nil)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@ func PointSub(a, b [32]byte) ([32]byte, error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return result, errors.New("crypto: point_sub failed")
|
||||
return result, coreerr.E("PointSub", "point_sub failed", nil)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -81,7 +82,7 @@ func GenerateCLSAGGG(hash [32]byte, ring []byte, ringSize int,
|
|||
(*C.uint8_t)(unsafe.Pointer(&sig[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return nil, errors.New("crypto: generate_CLSAG_GG failed")
|
||||
return nil, coreerr.E("GenerateCLSAGGG", "generate_CLSAG_GG failed", nil)
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
|
|
|||
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
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
||||
"dappco.re/go/core/blockchain/crypto"
|
||||
)
|
||||
|
||||
func TestFastHash_Good_KnownVector(t *testing.T) {
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ package crypto
|
|||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GenerateKeys creates a new random key pair.
|
||||
|
|
@ -20,7 +21,7 @@ func GenerateKeys() (pub [32]byte, sec [32]byte, err error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&sec[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
err = fmt.Errorf("crypto: generate_keys failed (rc=%d)", rc)
|
||||
err = coreerr.E("GenerateKeys", fmt.Sprintf("generate_keys failed (rc=%d)", rc), nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ func SecretToPublic(sec [32]byte) ([32]byte, error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&pub[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return pub, fmt.Errorf("crypto: secret_to_public failed (rc=%d)", rc)
|
||||
return pub, coreerr.E("SecretToPublic", fmt.Sprintf("secret_to_public failed (rc=%d)", rc), nil)
|
||||
}
|
||||
return pub, nil
|
||||
}
|
||||
|
|
@ -52,7 +53,7 @@ func GenerateKeyDerivation(pub [32]byte, sec [32]byte) ([32]byte, error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&d[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return d, errors.New("crypto: generate_key_derivation failed")
|
||||
return d, coreerr.E("GenerateKeyDerivation", "generate_key_derivation failed", nil)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
|
@ -67,7 +68,7 @@ func DerivePublicKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte
|
|||
(*C.uint8_t)(unsafe.Pointer(&derived[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return derived, errors.New("crypto: derive_public_key failed")
|
||||
return derived, coreerr.E("DerivePublicKey", "derive_public_key failed", nil)
|
||||
}
|
||||
return derived, nil
|
||||
}
|
||||
|
|
@ -82,7 +83,7 @@ func DeriveSecretKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte
|
|||
(*C.uint8_t)(unsafe.Pointer(&derived[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return derived, errors.New("crypto: derive_secret_key failed")
|
||||
return derived, coreerr.E("DeriveSecretKey", "derive_secret_key failed", nil)
|
||||
}
|
||||
return derived, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ package crypto
|
|||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GenerateKeyImage computes the key image for a public/secret key pair.
|
||||
|
|
@ -22,7 +23,7 @@ func GenerateKeyImage(pub [32]byte, sec [32]byte) ([32]byte, error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&ki[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return ki, errors.New("crypto: generate_key_image failed")
|
||||
return ki, coreerr.E("GenerateKeyImage", "generate_key_image failed", nil)
|
||||
}
|
||||
return ki, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import "C"
|
|||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// RandomXHash computes the RandomX PoW hash. The key is the cache
|
||||
|
|
@ -23,7 +25,7 @@ func RandomXHash(key, input []byte) ([32]byte, error) {
|
|||
(*C.uint8_t)(unsafe.Pointer(&output[0])),
|
||||
)
|
||||
if ret != 0 {
|
||||
return output, fmt.Errorf("crypto: RandomX hash failed with code %d", ret)
|
||||
return output, coreerr.E("RandomXHash", fmt.Sprintf("RandomX hash failed with code %d", ret), nil)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ package crypto
|
|||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GenerateSignature creates a standard (non-ring) signature.
|
||||
|
|
@ -22,7 +23,7 @@ func GenerateSignature(hash [32]byte, pub [32]byte, sec [32]byte) ([64]byte, err
|
|||
(*C.uint8_t)(unsafe.Pointer(&sig[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return sig, errors.New("crypto: generate_signature failed")
|
||||
return sig, coreerr.E("GenerateSignature", "generate_signature failed", nil)
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
|
@ -60,7 +61,7 @@ func GenerateRingSignature(hash [32]byte, image [32]byte, pubs [][32]byte,
|
|||
(*C.uint8_t)(unsafe.Pointer(&flatSigs[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return nil, errors.New("crypto: generate_ring_signature failed")
|
||||
return nil, coreerr.E("GenerateRingSignature", "generate_ring_signature failed", nil)
|
||||
}
|
||||
|
||||
sigs := make([][64]byte, n)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"math/big"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
)
|
||||
|
||||
func TestNextDifficulty_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ Do not use American spellings in identifiers, comments, or documentation.
|
|||
- Error wrapping uses `fmt.Errorf("types: description: %w", err)`
|
||||
- Every source file carries the EUPL-1.2 copyright header
|
||||
- No emojis in code or comments
|
||||
- Imports are ordered: stdlib, then `golang.org/x`, then `forge.lthn.ai`, each
|
||||
- Imports are ordered: stdlib, then `golang.org/x`, then `dappco.re`, each
|
||||
separated by a blank line
|
||||
|
||||
### Dependencies
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ description: Pure Go implementation of the Lethean CryptoNote/Zano-fork blockcha
|
|||
|
||||
`go-blockchain` is a Go reimplementation of the Lethean blockchain protocol. It provides pure-Go implementations of chain logic, data structures, consensus rules, wallet operations, and networking, delegating only mathematically complex cryptographic operations (ring signatures, Bulletproofs+, Zarcanum proofs) to a cleaned C++ library via CGo.
|
||||
|
||||
**Module path:** `forge.lthn.ai/core/go-blockchain`
|
||||
**Module path:** `dappco.re/go/core/blockchain`
|
||||
|
||||
**Licence:** [European Union Public Licence (EUPL) version 1.2](https://joinup.ec.europa.eu/software/page/eupl/licence-eupl)
|
||||
|
||||
|
|
@ -61,9 +61,9 @@ go-blockchain/
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// Query the daemon
|
||||
|
|
@ -109,7 +109,7 @@ When CGo is disabled, stub implementations return errors, allowing the rest of t
|
|||
|
||||
## Development Phases
|
||||
|
||||
The project follows a 9-phase development plan. See the [wiki Development Phases page](https://forge.lthn.ai/core/go-blockchain/wiki/Development-Phases) for detailed phase descriptions.
|
||||
The project follows a 9-phase development plan. See the [wiki Development Phases page](https://dappco.re/go/core/blockchain/wiki/Development-Phases) for detailed phase descriptions.
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|-------|-------|--------|
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ The Lethean node exposes two RPC interfaces: a **daemon** API for blockchain que
|
|||
The `rpc/` package provides a typed Go client:
|
||||
|
||||
```go
|
||||
import "forge.lthn.ai/core/go-blockchain/rpc"
|
||||
import "dappco.re/go/core/blockchain/rpc"
|
||||
|
||||
// Create a client (appends /json_rpc automatically)
|
||||
client := rpc.NewClient("http://localhost:36941")
|
||||
|
|
|
|||
|
|
@ -1085,8 +1085,8 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// validV2Tx returns a minimal valid v2 (Zarcanum) transaction for testing.
|
||||
|
|
@ -1287,8 +1287,8 @@ package consensus
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
func TestIsPreHardforkFreeze_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ package chain
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
store "dappco.re/go/core/store"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -277,8 +277,8 @@ package chain
|
|||
import (
|
||||
"math/big"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/difficulty"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/difficulty"
|
||||
)
|
||||
|
||||
// nextDifficultyWith computes the expected difficulty for the block at the
|
||||
|
|
@ -365,7 +365,7 @@ cd /home/claude/Code/core/go-blockchain && go test -race -run "TestNext.*Difficu
|
|||
**Expected:**
|
||||
|
||||
```
|
||||
ok forge.lthn.ai/core/go-blockchain/chain (cached)
|
||||
ok dappco.re/go/core/blockchain/chain (cached)
|
||||
```
|
||||
|
||||
All 10 tests pass: `TestNextDifficulty_Genesis`, `TestNextDifficulty_FewBlocks`, `TestNextDifficulty_EmptyChain`, `TestNextDifficulty_HF6Boundary_Good`, `TestNextDifficulty_HF6Boundary_Bad`, `TestNextDifficulty_HF6Boundary_Ugly`, `TestNextPoSDifficulty_Good`, `TestNextPoSDifficulty_HF6Boundary_Good`, `TestNextPoSDifficulty_Genesis`.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Date:** 2026-03-16
|
||||
**Author:** Charon
|
||||
**Package:** `forge.lthn.ai/core/go-blockchain`
|
||||
**Package:** `dappco.re/go/core/blockchain`
|
||||
**Status:** Approved
|
||||
|
||||
## Context
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Date:** 2026-03-16
|
||||
**Author:** Charon
|
||||
**Package:** `forge.lthn.ai/core/go-blockchain`
|
||||
**Package:** `dappco.re/go/core/blockchain`
|
||||
**Status:** Approved
|
||||
|
||||
## Context
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Date:** 2026-03-16
|
||||
**Author:** Charon
|
||||
**Package:** `forge.lthn.ai/core/go-blockchain`
|
||||
**Package:** `dappco.re/go/core/blockchain`
|
||||
**Status:** Draft
|
||||
**Depends on:** HF1 (types refactor), HF3 (block version), HF4 (Zarcanum — already implemented)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Date:** 2026-03-16
|
||||
**Author:** Charon
|
||||
**Package:** `forge.lthn.ai/core/go-blockchain`
|
||||
**Package:** `dappco.re/go/core/blockchain`
|
||||
**Status:** Draft
|
||||
**Depends on:** HF5 (confidential assets)
|
||||
|
||||
|
|
|
|||
87
explorer_command.go
Normal file
87
explorer_command.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// 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 blockchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
corelog "dappco.re/go/core/log"
|
||||
|
||||
cli "dappco.re/go/core/cli/pkg/cli"
|
||||
store "dappco.re/go/core/store"
|
||||
|
||||
"dappco.re/go/core/blockchain/chain"
|
||||
"dappco.re/go/core/blockchain/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newChainExplorerCommand builds the interactive `chain explorer` command.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// chain explorer --data-dir ~/.lethean/chain
|
||||
//
|
||||
// Use it alongside `AddChainCommands` to expose the TUI node view.
|
||||
func newChainExplorerCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "explorer",
|
||||
Short: "TUI block explorer",
|
||||
Long: "Interactive terminal block explorer with live sync status.",
|
||||
Args: cobra.NoArgs,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return validateChainOptions(*chainDataDir, *seedPeerAddress)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runChainExplorer(*chainDataDir, *seedPeerAddress, *useTestnet)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runChainExplorer(chainDataDir, seedPeerAddress string, useTestnet bool) error {
|
||||
if err := ensureChainDataDirExists(chainDataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(chainDataDir, "chain.db")
|
||||
chainStore, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
return corelog.E("runChainExplorer", "open store", err)
|
||||
}
|
||||
defer chainStore.Close()
|
||||
|
||||
blockchain := chain.New(chainStore)
|
||||
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed)
|
||||
}()
|
||||
|
||||
node := tui.NewNode(blockchain)
|
||||
status := tui.NewStatusModel(node)
|
||||
explorer := tui.NewExplorerModel(blockchain)
|
||||
hints := tui.NewKeyHintsModel()
|
||||
|
||||
frame := cli.NewFrame("HCF")
|
||||
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.
|
||||
wg.Wait() // Wait for it before closing store.
|
||||
return nil
|
||||
}
|
||||
40
go.mod
40
go.mod
|
|
@ -1,14 +1,14 @@
|
|||
module forge.lthn.ai/core/go-blockchain
|
||||
module dappco.re/go/core/blockchain
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/cli v0.3.5
|
||||
forge.lthn.ai/core/go-io v0.1.5
|
||||
forge.lthn.ai/core/go-log v0.0.4
|
||||
forge.lthn.ai/core/go-p2p v0.1.5
|
||||
forge.lthn.ai/core/go-process v0.2.7
|
||||
forge.lthn.ai/core/go-store v0.1.8
|
||||
dappco.re/go/core/cli v0.3.1
|
||||
dappco.re/go/core/io v0.2.0
|
||||
dappco.re/go/core/log v0.1.0
|
||||
dappco.re/go/core/p2p v0.1.3
|
||||
dappco.re/go/core/process v0.2.3
|
||||
dappco.re/go/core/store v0.1.6
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
|
|
@ -17,8 +17,13 @@ require (
|
|||
|
||||
require (
|
||||
forge.lthn.ai/core/go v0.3.1 // indirect
|
||||
forge.lthn.ai/core/go-i18n v0.1.6 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.5 // indirect
|
||||
forge.lthn.ai/core/go-crypt v0.1.6 // indirect
|
||||
forge.lthn.ai/core/go-i18n v0.1.4 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.4 // indirect
|
||||
forge.lthn.ai/core/go-io v0.1.2 // indirect
|
||||
forge.lthn.ai/core/go-log v0.0.4 // indirect
|
||||
forge.lthn.ai/core/go-process v0.2.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
|
|
@ -27,6 +32,7 @@ require (
|
|||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
|
|
@ -45,6 +51,7 @@ require (
|
|||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
|
|
@ -52,5 +59,18 @@ require (
|
|||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.46.2 // indirect
|
||||
modernc.org/sqlite v1.47.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
dappco.re/go/core => forge.lthn.ai/core/go v0.5.0
|
||||
dappco.re/go/core/cli => forge.lthn.ai/core/cli v0.3.1
|
||||
dappco.re/go/core/crypt => forge.lthn.ai/core/go-crypt v0.1.7
|
||||
dappco.re/go/core/i18n => forge.lthn.ai/core/go-i18n v0.1.4
|
||||
dappco.re/go/core/inference => forge.lthn.ai/core/go-inference v0.1.4
|
||||
dappco.re/go/core/io => forge.lthn.ai/core/go-io v0.2.0
|
||||
dappco.re/go/core/log => forge.lthn.ai/core/go-log v0.1.0
|
||||
dappco.re/go/core/p2p => forge.lthn.ai/core/go-p2p v0.1.3
|
||||
dappco.re/go/core/process => forge.lthn.ai/core/go-process v0.2.3
|
||||
dappco.re/go/core/store => forge.lthn.ai/core/go-store v0.1.6
|
||||
)
|
||||
|
|
|
|||
44
go.sum
44
go.sum
|
|
@ -1,21 +1,31 @@
|
|||
forge.lthn.ai/core/cli v0.3.5 h1:P7yK0DmSA1QnUMFuCjJZf/fk/akKPIxopQ6OwD8Sar8=
|
||||
forge.lthn.ai/core/cli v0.3.5/go.mod h1:SeArHx+hbpX5iZqgASCD7Q1EDoc6uaaGiGBotmNzIx4=
|
||||
forge.lthn.ai/core/cli v0.3.1 h1:ZpHhaDrdbaV98JDxj/f0E5nytYk9tTMRu3qohGyK4M0=
|
||||
forge.lthn.ai/core/cli v0.3.1/go.mod h1:28cOl9eK0H033Otkjrv9f/QCmtHcJl+IIx4om8JskOg=
|
||||
forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM=
|
||||
forge.lthn.ai/core/go v0.3.1/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
|
||||
forge.lthn.ai/core/go-i18n v0.1.6 h1:Z9h6sEZsgJmWlkkq3ZPZyfgWipeeqN5lDCpzltpamHU=
|
||||
forge.lthn.ai/core/go-i18n v0.1.6/go.mod h1:C6CbwdN7sejTx/lbutBPrxm77b8paMHBO6uHVLHOdqQ=
|
||||
forge.lthn.ai/core/go-inference v0.1.5 h1:Az/Euv1DusJQJz/Eca0Ey7sVXQkFLPHW0TBrs9g+Qwg=
|
||||
forge.lthn.ai/core/go-inference v0.1.5/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
||||
forge.lthn.ai/core/go-io v0.1.5 h1:+XJ1YhaGGFLGtcNbPtVlndTjk+pO0Ydi2hRDj5/cHOM=
|
||||
forge.lthn.ai/core/go-io v0.1.5/go.mod h1:FRtXSsi8W+U9vewCU+LBAqqbIj3wjXA4dBdSv3SAtWI=
|
||||
forge.lthn.ai/core/go-crypt v0.1.6 h1:jB7L/28S1NR+91u3GcOYuKfBLzPhhBUY1fRe6WkGVns=
|
||||
forge.lthn.ai/core/go-crypt v0.1.6/go.mod h1:4VZAGqxlbadhSB66sJkdj54/HSJ+bSxVgwWK5kMMYDo=
|
||||
forge.lthn.ai/core/go-i18n v0.1.4 h1:zOHUUJDgRo88/3tj++kN+VELg/buyZ4T2OSdG3HBbLQ=
|
||||
forge.lthn.ai/core/go-i18n v0.1.4/go.mod h1:aDyAfz7MMgWYgLkZCptfFmZ7jJg3ocwjEJ1WkJSvv4U=
|
||||
forge.lthn.ai/core/go-inference v0.1.4 h1:fuAgWbqsEDajHniqAKyvHYbRcBrkGEiGSqR2pfTMRY0=
|
||||
forge.lthn.ai/core/go-inference v0.1.4/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
||||
forge.lthn.ai/core/go-io v0.1.2 h1:q8hj2jtOFqAgHlBr5wsUAOXtaFkxy9gqGrQT/il0WYA=
|
||||
forge.lthn.ai/core/go-io v0.1.2/go.mod h1:PbNKW1Q25ywSOoQXeGdQHbV5aiIrTXvHIQ5uhplA//g=
|
||||
forge.lthn.ai/core/go-io v0.2.0 h1:O/b3E6agFNQEy99FB2PMeeGO0wJleE0C3jx7tPEu9HA=
|
||||
forge.lthn.ai/core/go-io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
forge.lthn.ai/core/go-p2p v0.1.5 h1:/jEhkz3HYCrRPJ37JoXPnIX+UsC3YhX7PRoXp44n7TA=
|
||||
forge.lthn.ai/core/go-p2p v0.1.5/go.mod h1:d32MQdcWRDJYlOnWsaHLbxxz+P9DLxPOBEgz3tsemW4=
|
||||
forge.lthn.ai/core/go-process v0.2.7 h1:yl7jOxzDqWpJd/ZvJ/Ff6bHgPFLA1ZYU5UDcsz3AzLM=
|
||||
forge.lthn.ai/core/go-process v0.2.7/go.mod h1:I6x11UNaZbU3k0FWUaSlPRTE4YZk/lWIjiODm/8Jr9c=
|
||||
forge.lthn.ai/core/go-store v0.1.8 h1:jeFqxilifa/hXtQqCeXX/+Vwy6M/XZE7uCP8XQ0ercw=
|
||||
forge.lthn.ai/core/go-store v0.1.8/go.mod h1:DJocTeTCjFBPn5ppQT/IDheFJhOfwlHeoxEUtDH07zE=
|
||||
forge.lthn.ai/core/go-log v0.1.0 h1:QMr7jeZj2Bb/BovgPbiZOzNt9j/+wym11lBSleucCa0=
|
||||
forge.lthn.ai/core/go-log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
forge.lthn.ai/core/go-p2p v0.1.3 h1:XbETiHrYTDiJTq6EAxdU+MJF1l5UxEQE14wJ7G7FOVc=
|
||||
forge.lthn.ai/core/go-p2p v0.1.3/go.mod h1:F2M4qIzkixQpZEoOEtNaB4rhmi1WQKbR7JqVzGA1r80=
|
||||
forge.lthn.ai/core/go-process v0.2.2 h1:bnHFtzg92udochDDB6bD2luzzmr9ETKWmGzSsGjFFYE=
|
||||
forge.lthn.ai/core/go-process v0.2.2/go.mod h1:gVTbxL16ccUIexlFcyDtCy7LfYvD8Rtyzfo8bnXAXrU=
|
||||
forge.lthn.ai/core/go-process v0.2.3 h1:/ERqRYHgCNZjNT9NMinAAJJGJWSsHuCTiHFNEm6nTPY=
|
||||
forge.lthn.ai/core/go-process v0.2.3/go.mod h1:gVTbxL16ccUIexlFcyDtCy7LfYvD8Rtyzfo8bnXAXrU=
|
||||
forge.lthn.ai/core/go-store v0.1.6 h1:7T+K5cciXOaWRxge0WnGkt0PcK3epliWBa1G2FLEuac=
|
||||
forge.lthn.ai/core/go-store v0.1.6/go.mod h1:/2vqaAn+HgGU14N29B+vIfhjIsBzy7RC+AluI6BIUKI=
|
||||
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
|
|
@ -34,6 +44,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
|||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -133,8 +145,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.46.2 h1:gkXQ6R0+AjxFC/fTDaeIVLbNLNrRoOK7YYVz5BKhTcE=
|
||||
modernc.org/sqlite v1.46.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ package mining
|
|||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/consensus"
|
||||
"dappco.re/go/core/blockchain/crypto"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
// RandomXKey is the cache initialisation key for RandomX hashing.
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import (
|
|||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
func testnetGenesisHeader() types.BlockHeader {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import (
|
|||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/consensus"
|
||||
"dappco.re/go/core/blockchain/crypto"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
// TemplateProvider abstracts the RPC methods needed by the miner.
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/rpc"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// Package p2p implements the CryptoNote P2P protocol for the Lethean blockchain.
|
||||
package p2p
|
||||
|
||||
import "forge.lthn.ai/core/go-p2p/node/levin"
|
||||
import "dappco.re/go/core/p2p/node/levin"
|
||||
|
||||
// Re-export command IDs from the levin package for convenience.
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ package p2p
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
// PeerlistEntrySize is the packed size of a peerlist entry (ip + port + id + last_seen).
|
||||
|
|
@ -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,10 +7,11 @@ package p2p
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
func TestEncodeHandshakeRequest_Good_Roundtrip(t *testing.T) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
package p2p
|
||||
|
||||
import "forge.lthn.ai/core/go-p2p/node/levin"
|
||||
import "dappco.re/go/core/p2p/node/levin"
|
||||
|
||||
// EncodePingRequest returns an encoded empty ping request payload.
|
||||
func EncodePingRequest() ([]byte, error) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ package p2p
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
func TestEncodePingRequest_Good_EmptySection(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
package p2p
|
||||
|
||||
import "forge.lthn.ai/core/go-p2p/node/levin"
|
||||
import "dappco.re/go/core/p2p/node/levin"
|
||||
|
||||
// NewBlockNotification is NOTIFY_NEW_BLOCK (2001).
|
||||
type NewBlockNotification struct {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"bytes"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
func TestNewBlockNotification_Good_Roundtrip(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
package p2p
|
||||
|
||||
import (
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
// CoreSyncData is the blockchain state exchanged during handshake and timed sync.
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ package p2p
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/p2p/node/levin"
|
||||
)
|
||||
|
||||
func TestCoreSyncData_Good_Roundtrip(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
package p2p
|
||||
|
||||
import "forge.lthn.ai/core/go-p2p/node/levin"
|
||||
import "dappco.re/go/core/p2p/node/levin"
|
||||
|
||||
// TimedSyncRequest is a COMMAND_TIMED_SYNC request.
|
||||
type TimedSyncRequest struct {
|
||||
|
|
|
|||
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,7 +8,7 @@ package rpc
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GetLastBlockHeader returns the header of the most recent block.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"net/url"
|
||||
"time"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// Client is a Lethean daemon RPC client.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ package rpc
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GetInfo returns the daemon status.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ package rpc
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// SubmitBlock submits a mined block to the daemon.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ package rpc
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GetTxDetails returns detailed information about a transaction.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// RandomOutputEntry is a decoy output returned by getrandom_outs.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue