Compare commits

..

56 commits
v0.1.3 ... dev

Author SHA1 Message Date
Snider
76488e0beb feat(wire): add alias entry readers + AX usage-example comments
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Add readExtraAliasEntryOld (tag 20) and readExtraAliasEntry (tag 33)
wire readers so the node can deserialise blocks containing alias
registrations. Without these readers, mainnet sync would fail on any
block with an alias transaction. Three round-trip tests validate the
new readers.

Also apply AX-2 (comments as usage examples) across 12 files: add
concrete usage-example comments to exported functions in config/,
types/, wire/, chain/, difficulty/, and consensus/. Fix stale doc
in consensus/doc.go that incorrectly referenced *config.ChainConfig.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-05 08:46:54 +01:00
Snider
caf83faf39 refactor(types,consensus,chain): apply AX design principles across public API
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Migrate types/asset.go from fmt.Errorf to coreerr.E() for consistency
with the rest of the types package. Add usage-example comments (AX-2) to
all key exported functions in consensus/, chain/, and types/ so agents
can see concrete calling patterns without reading implementation details.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-05 07:36:31 +01:00
Virgil
123047bebd refactor(wire): unify HF5 asset operation parsing
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 23:09:26 +00:00
Virgil
330ee2a146 refactor(consensus): expand fork-state naming
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 23:04:19 +00:00
Virgil
d5070cce15 build(crypto): select randomx sources by architecture
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 22:57:24 +00:00
Virgil
9c5b179375 feat(tui): render tx inputs explicitly
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 22:51:57 +00:00
Virgil
602c886400 fix(types): track decoded address prefixes
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 22:46:12 +00:00
Virgil
474fa2f07d chore(rfc): verify spec coverage
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 22:40:53 +00:00
Virgil
99720fff5e refactor(consensus): centralise validation fork state
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 22:38:20 +00:00
Virgil
b7428496bd refactor(p2p): reuse build-version helper in handshake validation
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 22:35:02 +00:00
Virgil
bdbefa7d4a refactor(tui): describe non-to-key outputs in explorer
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 22:31:10 +00:00
Virgil
bb941ebcc5 fix(consensus): tighten fork-era type gating
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Reject Zarcanum inputs and outputs before HF4 instead of letting
unsupported combinations pass semantic validation.

Also restrict PoS miner stake inputs to txin_to_key pre-HF4 and
txin_zc post-HF4, with regression coverage for the affected paths.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 22:18:53 +00:00
Virgil
95edac1d15 refactor(ax): centralise asset validation
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 22:09:17 +00:00
Virgil
8802b94ee5 test(core): align helpers with current APIs
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 22:04:52 +00:00
Virgil
0512861330 refactor(types): centralise transparent spend-key lookup
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 22:00:07 +00:00
Virgil
51d5ce9f14 fix(crypto): restore vendored boost compat
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 21:56:32 +00:00
Virgil
bc3e208691 fix(consensus): restore exported block version check
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 21:50:42 +00:00
Virgil
219aeae540 refactor(consensus): unexport block version helpers
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 21:48:36 +00:00
Virgil
2e92407233 fix(consensus): restore pre-hf1 miner tx version
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
2026-04-04 21:37:38 +00:00
Virgil
0993b081c7 test(chain): pin HTLC expiration boundary
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 21:30:53 +00:00
Virgil
cb43082d18 feat(crypto): add Zarcanum verification context API
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 21:28:12 +00:00
Virgil
2bebe323b8 fix(wire): reject unsupported output types
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 21:22:15 +00:00
Virgil
0ab8bfbd01 fix(consensus): validate miner tx versions by fork era
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 21:18:42 +00:00
Virgil
b34afa827f refactor(ax): make chain sync logging explicit
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 21:03:36 +00:00
Virgil
7e01df15fe fix(blockchain): handle sync setup errors explicitly
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 21:00:03 +00:00
Virgil
2f3f46e8c5 fix(consensus): count zarcanum miner outputs in rewards
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 20:56:56 +00:00
Virgil
92628cec35 refactor(types): centralise to-key target extraction
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:50:54 +00:00
Virgil
92cb5a8fbb feat(wallet): mark HTLC spends during sync
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:42:56 +00:00
Virgil
e25e3e73e7 refactor(wire): deduplicate output target encoding
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:39:45 +00:00
Virgil
c787990b9a refactor(ax): clarify ring and wallet names
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:36:16 +00:00
Virgil
3686a82b33 fix(consensus): validate HTLC tags in v2 signatures
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:32:39 +00:00
Virgil
d6f31dbe57 fix(cli): tighten chain command validation
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:22:48 +00:00
Virgil
41f2d52979 chore(spec): verify RFC coverage
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
No missing HF1/HF3/HF5 implementation gaps were found in the current codebase.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 20:18:13 +00:00
Virgil
050d530b29 feat(consensus): validate HF5 asset operations
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:14:54 +00:00
Virgil
c1b68523c6 fix(consensus): enforce tx versions across fork eras
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 20:04:24 +00:00
Virgil
be99c5e93a fix(wire): reject unsupported transaction variants
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Add explicit errors for unknown input/output variants in the wire encoder and tighten transparent output validation in consensus. Cover the new failure paths with unit tests.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 19:56:50 +00:00
Virgil
d2caf68d94 fix(p2p): report malformed peer builds
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 19:49:03 +00:00
Virgil
ccdcfbaacf refactor(blockchain): clarify handshake sync naming
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 19:21:12 +00:00
Virgil
f1738527bc feat(chain): select HTLC ring keys by expiry
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 19:11:37 +00:00
Virgil
21c5d49ef9 fix(sync): validate peers and persist HTLC spends
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Centralise handshake response validation so outbound sync checks both network identity and minimum peer build version through the p2p layer. Also record HTLC key images as spent during block processing, matching the HF1 input semantics and preventing those spends from being omitted from chain state.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 19:01:07 +00:00
Virgil
0ba5bbe49c feat(consensus): enforce block version in chain sync
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
2026-04-04 18:56:36 +00:00
Virgil
01f4e5cd0a feat(chain): support multisig and HTLC ring outputs
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 18:52:40 +00:00
Virgil
d3143d3f88 feat(consensus): enforce hf5 tx version and add asset descriptors
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 18:34:49 +00:00
Virgil
f7ee451fc4 fix(blockchain): enforce HF5 freeze and peer build gate
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 18:30:32 +00:00
Virgil
8e6dc326df feat(crypto): add generic double-Schnorr bridge
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
Expose generate/verify wrappers for generic_double_schnorr_sig and add a consensus helper for balance-proof checks.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 18:26:33 +00:00
Virgil
a2df164822 refactor(blockchain): spell out command sync names
Some checks failed
Security Scan / security (push) Has been cancelled
Test / Test (push) Has been cancelled
Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 04:18:37 +00:00
Virgil
243749a6d8 refactor: align command paths with AX naming
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
2026-04-04 04:10:23 +00:00
d004158022 Merge pull request '[agent/claude] Migrate module path to dappco.re/go/core/blockchain. Update ...' (#4) from agent/migrate-module-path-to-dappco-re-go-core into main
Some checks failed
Security Scan / security (push) Successful in 9s
Test / Test (push) Failing after 22s
2026-03-22 02:49:31 +00:00
Snider
34128d8e98 refactor: migrate module path to dappco.re/go/core/blockchain
Some checks failed
Security Scan / security (pull_request) Successful in 11s
Test / Test (pull_request) Failing after 19s
Update go.mod module line, all require/replace directives, and every
.go import path from forge.lthn.ai/core/go-blockchain to
dappco.re/go/core/blockchain. Add replace directives to bridge
dappco.re paths to existing forge.lthn.ai registry during migration.
Update CLAUDE.md, README, and docs to reflect the new module path.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 01:49:26 +00:00
Snider
6370d96c31 Merge remote-tracking branch 'github/dev'
Some checks failed
Security Scan / security (push) Successful in 10s
Test / Test (push) Failing after 23s
2026-03-22 00:58:07 +00:00
Snider
2b145d6ebf chore: sync dependencies for v0.1.5
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 17:54:14 +00:00
Snider
abb1e2b748 chore: sync dependencies for v0.1.4
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 17:50:27 +00:00
Claude
97c5510184
refactor: complete coreerr.E() conversion across all packages
Some checks failed
Security Scan / security (push) Successful in 12s
Test / Test (push) Failing after 25s
Convert all remaining fmt.Errorf and errors.New in production code
to coreerr.E(). Covers crypto/ (keygen, signature, clsag, keyimage,
pow), consensus/block, and chain/ring. Only sentinel error definitions
in errors.go and varint.go retain errors.New (correct usage).

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 23:42:51 +00:00
Claude
772cd1b0fd
refactor: convert remaining fmt.Errorf to coreerr.E()
Some checks failed
Security Scan / security (push) Successful in 8s
Test / Test (push) Failing after 22s
Converts the last 3 fmt.Errorf calls in production code (excluding
crypto/ CGo boundary) to coreerr.E() for conventions consistency.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 23:24:53 +00:00
Claude
70fab6f7d0
fix: restore HF5 asset tags, HTLC/multisig inputs, and tx version check after conventions sweep
Some checks failed
Test / Test (push) Failing after 16s
Security Scan / security (push) Failing after 13m58s
The conventions sweep (71f0a5c) overwrote HF5 code and removed HTLC/multisig
input handling. This commit restores:

- wire: HF5 asset wire tags (40/49/50/51) and reader functions for
  asset_descriptor_operation, asset_operation_proof,
  asset_operation_ownership_proof, and asset_operation_ownership_proof_eth
- wire: HTLC and multisig input encode/decode with string field helpers
- consensus: checkTxVersion enforcing version 3 after HF5 / rejecting before
- consensus: HF1-gated acceptance of HTLC and multisig input/output types
- consensus: HTLC key image deduplication in checkKeyImages
- consensus: HTLC ring signature counting in verifyV1Signatures
- chain: corrected error assertion in TestChain_GetBlockByHeight_NotFound

All 14 packages pass go test -race ./...

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 21:32:33 +00:00
Claude
89b0375e18
fix(wire): restore TxOutTarget type switches after conventions sweep
The coreerr.E() sweep reverted the HF1 type assertion changes in
encodeOutputsV1/V2 and decodeOutputsV1/V2. Restores full target
variant support (TxOutToKey, TxOutMultisig, TxOutHTLC) in all four
output functions.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 21:23:48 +00:00
144 changed files with 4340 additions and 965 deletions

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
crypto/build/
.core/
.idea/
.vscode/
*.log
.core/

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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"
)

View file

@ -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:

View file

@ -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"
)

View file

@ -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 {

View file

@ -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"

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
}
}
}

View file

@ -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"
)

View file

@ -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 {

View file

@ -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)
}

View file

@ -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

View file

@ -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{

View file

@ -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, &regularTx)
regularTxBlob := txBuf.Bytes()
regularTxHash := wire.TransactionHash(&regularTx)
minerTx1 := testCoinbaseTx(1)
block1 := types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
Nonce: 42,
PrevID: genesisHash,
Timestamp: 1770897720,
},
MinerTx: minerTx1,
TxHashes: []types.Hash{regularTxHash},
}
var blk1Buf bytes.Buffer
blk1Enc := wire.NewEncoder(&blk1Buf)
wire.EncodeBlock(blk1Enc, &block1)
block1Blob := blk1Buf.Bytes()
orig := GenesisHash
GenesisHash = genesisHash.String()
t.Cleanup(func() { GenesisHash = orig })
s, _ := store.New(":memory:")
defer s.Close()
c := New(s)
opts := SyncOptions{
Forks: []config.HardFork{
{Version: config.HF0Initial, Height: 0, Mandatory: true},
{Version: config.HF5, Height: 2, Mandatory: true},
},
}
if err := c.processBlockBlobs(genesisBytes, nil, 0, 1, opts); err != nil {
t.Fatalf("process genesis: %v", err)
}
err = c.processBlockBlobs(block1Blob, [][]byte{regularTxBlob}, 1, 100, opts)
if err == nil {
t.Fatal("expected freeze rejection, got nil")
}
if !strings.Contains(err.Error(), "freeze") {
t.Fatalf("expected freeze error, got %v", err)
}
}
// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
func testCoinbaseTxV2(height uint64) types.Transaction {
return types.Transaction{
@ -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")
}
}

View file

@ -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)

View file

@ -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
View 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
}

View file

@ -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() {

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
View 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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
)

View file

@ -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) {

View file

@ -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) {

View file

@ -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

View file

@ -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"
)

View file

@ -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

View file

@ -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"
)

View file

@ -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
}

View file

@ -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) {

View file

@ -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")
}

View file

@ -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

View file

@ -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")

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
)

View file

@ -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

View file

@ -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 ──────────────────────────────────

View file

@ -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)

View file

@ -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
}

View 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

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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"

View file

@ -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 {

View file

@ -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) {

View file

@ -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

View file

@ -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 |
|-------|-------|--------|

View file

@ -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")

View file

@ -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) {

View file

@ -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`.

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
View 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
View file

@ -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
View file

@ -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=

View file

@ -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.

View file

@ -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 {

View file

@ -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"
)

View file

@ -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.

View file

@ -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"
)

View file

@ -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 (

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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"
)

View file

@ -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) {

View file

@ -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) {

View file

@ -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 {

View file

@ -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) {

View file

@ -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.

View file

@ -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) {

View file

@ -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
View 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
View 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")
}
}

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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