Compare commits

..

88 commits
v0.1.0 ... 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
Snider
f19054f7b1 chore: sync dependencies for v0.1.3
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-16 22:20:06 +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
Snider
71f0a5c1d5 refactor: replace fmt.Errorf/os.* with go-io/go-log conventions
Some checks failed
Security Scan / security (push) Successful in 11s
Test / Test (push) Failing after 23s
Replace all fmt.Errorf and errors.New in production code with
coreerr.E("Caller.Method", "message", err) from go-log. Replace
os.MkdirAll with coreio.Local.EnsureDir from go-io. Sentinel errors
(consensus/errors.go, wire/varint.go) intentionally kept as errors.New
for errors.Is compatibility.

270 error call sites converted across 38 files. Test files untouched.
crypto/ directory (CGO) untouched.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-16 21:17:49 +00:00
Claude
8d41b76db3
feat(consensus): add pre-hardfork transaction freeze for HF5
Some checks failed
Security Scan / security (push) Successful in 8s
Test / Test (push) Failing after 16s
Rejects non-coinbase transactions during the 60-block window before
HF5 activation. Coinbase transactions are exempt. Implements
IsPreHardforkFreeze and ValidateTransactionInBlock.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:59:19 +00:00
Claude
efbf050c1b
feat(consensus): enforce transaction version 3 after HF5
After HF5 activation, only version 3 transactions are accepted.
Before HF5, version 3 is rejected. Matches C++ check_tx_semantic
hardfork gating logic.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:57:54 +00:00
Claude
939ad198fe
test(wire): add v3 transaction round-trip tests with asset operations
Tests v3 transactions containing asset_descriptor_operation (tag 40)
in extra and asset_operation_proof (tag 49) in proofs. Validates
hardfork_id encoding and bit-identical round-tripping.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:56:36 +00:00
Claude
d8e12a1539
feat(wire): add asset proof tags 49, 50, 51 readers
Reads asset_operation_proof, asset_operation_ownership_proof, and
asset_operation_ownership_proof_eth structures. All use CHAIN_TRANSITION_VER
with version byte prefix. Stored as opaque bytes.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:55:33 +00:00
Claude
3e79f34a65
feat(wire): add asset_descriptor_operation tag 40 reader
Reads the CHAIN_TRANSITION_VER structure for asset deploy/emit/update/burn
operations. Stores as opaque bytes for bit-identical round-tripping.
Required for HF5 block deserialisation.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:53:39 +00:00
Claude
9631efa5a8
feat(config): add HardforkActivationHeight helper
Returns the raw activation height for a given hardfork version.
Needed by the pre-hardfork transaction freeze logic.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:51:54 +00:00
Claude
18ceb7fa26
fix(chain): gate difficulty target switch on HF6, not HF2
Some checks failed
Security Scan / security (push) Successful in 8s
Test / Test (push) Failing after 20s
The 240s PoW target was incorrectly gated on HF2 (block 10,080), matching
the Zano upstream where HF2 coincides with the difficulty target change.
Lethean mainnet uses 120s blocks between HF2 and HF6 (999,999,999), so
the gate is corrected to HF6.

Also adds NextPoSDifficulty with the same HF6 gate using the PoS target
constants (DifficultyPosTarget / DifficultyPosTargetHF6). Both public
methods delegate to a shared nextDifficultyWith helper to avoid
duplicating the LWMA window logic.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:47:56 +00:00
Claude
d7917234ed
feat(consensus): add ErrBlockVersion alias and comprehensive block version tests
Add ErrBlockVersion as an alias for ErrBlockMajorVersion for clarity.
Add table-driven tests for expectedBlockMajorVersion covering all
hardfork boundaries (HF0 through HF4+) on both mainnet and testnet
schedules. Add standalone checkBlockVersion tests with Good/Bad/Ugly
pattern including version 255 edge case and exact HF1 boundary checks.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:47:47 +00:00
Claude
6a1f516f5f
test(wire): add HF1 mixed transaction round-trip integration test
Some checks failed
Security Scan / security (push) Successful in 8s
Test / Test (push) Failing after 17s
Verifies that a transaction containing TxInputToKey, TxInputHTLC,
TxInputMultisig inputs with TxOutToKey, TxOutMultisig, TxOutHTLC
output targets survives bit-identical encode/decode round-trip.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:41:28 +00:00
Claude
b1a0e9637b
feat(consensus): validate block major version for HF1
Adds expectedBlockMajorVersion and checkBlockVersion, called from
ValidateBlock before timestamp validation. Block version must match
the fork era: HF0->0, HF1->1, HF3->2, HF4+->3.
Tests cover both mainnet and testnet fork schedules including
boundary heights.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:40:11 +00:00
Claude
f88d582c64
feat(consensus): verify NLSAG signatures for HTLC inputs
verifyV1Signatures now counts and verifies TxInputHTLC alongside
TxInputToKey. HTLC inputs use the same ring signature scheme.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:38:28 +00:00
Claude
192d681ecd
feat(consensus): include HTLC/multisig in fee calculation and key image checks
sumInputs now sums TxInputHTLC.Amount and TxInputMultisig.Amount.
checkKeyImages now checks TxInputHTLC.KeyImage for double-spend.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:37:06 +00:00
Claude
ba29b55644
feat(consensus): gate HTLC and multisig types on HF1
checkInputTypes and checkOutputs now accept hf1Active flag.
HTLC and multisig inputs/outputs are rejected before HF1
(block 10,080) and accepted after.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:36:58 +00:00
Claude
830aa6055e
feat(wire): encode/decode TxOutMultisig and TxOutHTLC targets
Some checks failed
Security Scan / security (push) Successful in 9s
Test / Test (push) Failing after 20s
Adds target variant serialisation in both V1 and V2 output
encoders/decoders. Supports multisig (tag 0x04) and HTLC
(tag 0x23) targets within TxOutputBare.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:31:29 +00:00
Claude
14a2da9396
feat(wire): encode/decode TxInputHTLC and TxInputMultisig
Adds wire serialisation for HF1 HTLC (tag 0x22) and multisig
(tag 0x02) input types.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:28:55 +00:00
Claude
1ca75f9e3f
feat(types): add TxInputHTLC and TxInputMultisig input types
Input types for HF1 HTLC and multisig transactions.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:27:49 +00:00
Claude
30d174eaac
feat(types): add TxOutMultisig and TxOutHTLC target types
Output target types for HF1 HTLC and multisig transactions.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:27:01 +00:00
Claude
cc99c92c42
docs: HF5 confidential assets implementation plan
Some checks failed
Security Scan / security (push) Successful in 8s
Test / Test (push) Failing after 20s
Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:25:56 +00:00
Claude
0408d2f3fa
refactor(types): change TxOutputBare.Target to TxOutTarget interface
Some checks failed
Security Scan / security (push) Successful in 9s
Test / Test (push) Failing after 15s
Prepares for HF1 output target types (TxOutMultisig, TxOutHTLC).
All call sites updated to type-assert TxOutToKey where needed.

Modified files:
- types/transaction.go: TxOutputBare.Target is now TxOutTarget
- wire/transaction.go: encode/decode use type switch on target
- chain/ring.go: type-assert target to TxOutToKey for key extraction
- wallet/scanner.go: type-assert target before key comparison
- tui/explorer_model.go: type-assert target for display
- wire/transaction_test.go: type-assert in assertions
- wallet/builder_test.go: type-assert in assertions

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:23:27 +00:00
Claude
6eabe2a64d
docs: HF6 block time halving implementation plan
Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:21:34 +00:00
Claude
023f4b813c
docs: HF3 block version implementation plan
Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:20:56 +00:00
Claude
00e83582b7
feat(types): add TxOutTarget interface with TargetType method
Establishes the interface that all output target types will implement.
TxOutToKey now satisfies TxOutTarget via its TargetType() method.
Prepares for HF1 output target types (TxOutMultisig, TxOutHTLC).

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:20:26 +00:00
Claude
5a53c719de
docs: HF5 confidential assets + HF6 block time halving design specs
Some checks failed
Security Scan / security (push) Successful in 10s
Test / Test (push) Failing after 20s
HF5: Asset descriptor types, wire parsing for tag 40 + proof tags,
consensus validation for asset operations, pre-hardfork freeze.
Minimum viable approach: wire parsing first, deep validation later.

HF6: Block time target switch from 120s to 240s. ~10 lines.
Flagged potential bug: current code gates on HF2 not HF6.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:09:56 +00:00
Claude
c7c169dd67
docs: HF1 transaction types implementation plan
Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:09:03 +00:00
Claude
ef232fcb80
docs: HF3 block version 2 design spec
Block major version validation covering all hardfork transitions.
Single expectedBlockMajorVersion function handles HF0→HF4.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:05:55 +00:00
Claude
a71976f259
docs: address spec review — 12 issues fixed
Fix gaps from spec review: sumInputs/checkKeyImages/verifyV1Signatures
for HTLC inputs, complete call-site list for TxOutTarget refactor,
both v1+v2 decoders, function signature changes, block version check
placement, HTLCOrigin naming clarification.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 20:02:02 +00:00
Claude
57b8bbce2d
docs: HF1 transaction types design spec
Add design for HTLC and multisig transaction type support needed for
hardfork 1 activation at block 10,080. Covers types, wire, and consensus
changes with TxOutTarget interface refactor.

Co-Authored-By: Charon <charon@lethean.io>
2026-03-16 19:59:16 +00:00
Snider
dfb8467bb2 chore: sync go.mod dependencies
Some checks failed
Security Scan / security (push) Successful in 10s
Test / Test (push) Failing after 15s
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-15 15:37:24 +00:00
Snider
b4b532ceb9 chore: add .core/ and .idea/ to .gitignore
Some checks failed
Security Scan / security (push) Successful in 7s
Test / Test (push) Failing after 19s
2026-03-15 10:17:49 +00:00
Snider
2e81130b66 fix: update stale import paths and dependency versions from extraction
Some checks failed
Test / Test (push) Failing after 38s
Security Scan / security (push) Failing after 11m7s
Resolve stale forge.lthn.ai/core/cli v0.1.0 references (tag never existed,
earliest is v0.0.1) and regenerate go.sum via workspace-aware go mod tidy.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-14 13:38:59 +00:00
149 changed files with 11624 additions and 1275 deletions

4
.gitignore vendored
View file

@ -1 +1,5 @@
crypto/build/
.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,11 +8,9 @@
package chain
import (
"errors"
"fmt"
"forge.lthn.ai/core/go-blockchain/types"
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.
@ -21,28 +19,35 @@ 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 {
return 0, fmt.Errorf("chain: height: %w", err)
return 0, coreerr.E("Chain.Height", "chain: height", err)
}
return uint64(n), nil
}
// 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 {
return nil, nil, err
}
if h == 0 {
return nil, nil, errors.New("chain: no blocks stored")
return nil, nil, coreerr.E("Chain.TopBlock", "chain: no blocks stored", nil)
}
return c.GetBlockByHeight(h - 1)
}

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,25 +8,31 @@ 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"
)
// NextDifficulty computes the expected difficulty for the block at the given
// height, using the LWMA algorithm over stored block history.
// nextDifficultyWith computes the expected difficulty for the block at the
// given height using the LWMA algorithm, parameterised by pre/post-HF6 targets.
//
// The genesis block (height 0) is excluded from the difficulty window,
// matching the C++ daemon's load_targetdata_cache which skips index 0.
//
// The target block time depends on the hardfork schedule: 120s pre-HF2,
// 240s post-HF2 (matching DIFFICULTY_POW_TARGET_HF6 in the C++ source).
func (c *Chain) NextDifficulty(height uint64, forks []config.HardFork) (uint64, error) {
// The target block time depends on the hardfork schedule:
// - Pre-HF6: baseTarget (120s for both PoW and PoS on Lethean)
// - Post-HF6: hf6Target (240s -- halves block rate, halves emission)
//
// NOTE: This was originally gated on HF2, matching the Zano upstream where
// HF2 coincides with the difficulty target change. Lethean mainnet keeps 120s
// blocks between HF2 (height 10,080) and HF6 (height 999,999,999), so the
// gate was corrected to HF6 in March 2026.
func (c *Chain) nextDifficultyWith(height uint64, forks []config.HardFork, baseTarget, hf6Target uint64) (uint64, error) {
if height == 0 {
return 1, nil
}
// LWMA needs N+1 entries (N solve-time intervals).
// Start from height 1 — genesis is excluded from the difficulty window.
// Start from height 1 -- genesis is excluded from the difficulty window.
maxLookback := difficulty.LWMAWindow + 1
lookback := min(height, maxLookback) // height excludes genesis since we start from 1
@ -48,7 +54,7 @@ func (c *Chain) NextDifficulty(height uint64, forks []config.HardFork) (uint64,
for i := range count {
meta, err := c.getBlockMeta(startHeight + uint64(i))
if err != nil {
// Fewer blocks than expected use what we have.
// Fewer blocks than expected -- use what we have.
timestamps = timestamps[:i]
cumulDiffs = cumulDiffs[:i]
break
@ -58,12 +64,28 @@ func (c *Chain) NextDifficulty(height uint64, forks []config.HardFork) (uint64,
}
// Determine the target block time based on hardfork status.
// HF2 doubles the target from 120s to 240s.
target := config.DifficultyPowTarget
if config.IsHardForkActive(forks, config.HF2, height) {
target = config.DifficultyPowTargetHF6
// HF6 doubles the target from 120s to 240s (corrected from HF2 gate).
target := baseTarget
if config.IsHardForkActive(forks, config.HF6, height) {
target = hf6Target
}
result := difficulty.NextDifficulty(timestamps, cumulDiffs, target)
return result.Uint64(), nil
}
// 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,25 +8,52 @@ 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"
)
// preHF2Forks is a fork schedule where HF2 never activates,
// so the target stays at 120s.
var preHF2Forks = []config.HardFork{
// preHF6Forks is a fork schedule where HF6 never activates,
// so both PoW and PoS targets stay at 120s.
var preHF6Forks = []config.HardFork{
{Version: config.HF0Initial, Height: 0},
}
// hf6ActiveForks is a fork schedule where HF6 activates at height 100,
// switching both PoW and PoS targets to 240s from block 101 onwards.
var hf6ActiveForks = []config.HardFork{
{Version: config.HF0Initial, Height: 0},
{Version: config.HF1, Height: 0},
{Version: config.HF2, Height: 0},
{Version: config.HF3, Height: 0},
{Version: config.HF4Zarcanum, Height: 0},
{Version: config.HF5, Height: 0},
{Version: config.HF6, Height: 100},
}
// storeBlocks inserts count blocks with constant intervals and difficulty.
func storeBlocks(t *testing.T, c *Chain, count int, interval uint64, baseDiff uint64) {
t.Helper()
for i := uint64(0); i < uint64(count); i++ {
err := c.PutBlock(&types.Block{}, &BlockMeta{
Hash: types.Hash{byte(i + 1)},
Height: i,
Timestamp: i * interval,
Difficulty: baseDiff,
CumulativeDiff: baseDiff * (i + 1),
})
require.NoError(t, err)
}
}
func TestNextDifficulty_Genesis(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
diff, err := c.NextDifficulty(0, preHF2Forks)
diff, err := c.NextDifficulty(0, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(1), diff)
}
@ -40,26 +67,14 @@ func TestNextDifficulty_FewBlocks(t *testing.T) {
// Store genesis + 4 blocks with constant 120s intervals and difficulty 1000.
// Genesis at height 0 is excluded from the LWMA window.
baseDiff := uint64(1000)
for i := uint64(0); i < 5; i++ {
err := c.PutBlock(&types.Block{}, &BlockMeta{
Hash: types.Hash{byte(i + 1)},
Height: i,
Timestamp: i * 120,
Difficulty: baseDiff,
CumulativeDiff: baseDiff * (i + 1),
})
require.NoError(t, err)
}
storeBlocks(t, c, 5, 120, 1000)
// Next difficulty for height 5 uses blocks 1-4 (n=3 intervals).
// LWMA formula with constant D and T gives D/n = 1000/3 333.
diff, err := c.NextDifficulty(5, preHF2Forks)
// LWMA formula with constant D and T gives D/n = 1000/3 = 333.
diff, err := c.NextDifficulty(5, preHF6Forks)
require.NoError(t, err)
require.Greater(t, diff, uint64(0))
// LWMA gives total_work * T * (n+1) / (2 * weighted_solvetimes * n).
// For constant intervals: D/n = 1000/3 = 333.
expected := uint64(333)
require.Equal(t, expected, diff)
}
@ -71,8 +86,124 @@ func TestNextDifficulty_EmptyChain(t *testing.T) {
c := New(s)
// Height 1 with no blocks stored — should return starter difficulty.
diff, err := c.NextDifficulty(1, preHF2Forks)
// Height 1 with no blocks stored -- should return starter difficulty.
diff, err := c.NextDifficulty(1, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(1), diff)
}
// --- HF6 boundary tests ---
func TestNextDifficulty_HF6Boundary_Good(t *testing.T) {
// Verify that blocks at height <= 100 use the 120s target and blocks
// at height > 100 use the 240s target, given hf6ActiveForks.
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 105, 120, 1000)
// Height 100 -- HF6 activates at heights > 100, so this is pre-HF6.
diffPre, err := c.NextDifficulty(100, hf6ActiveForks)
require.NoError(t, err)
// Height 101 -- HF6 is active (height > 100), target becomes 240s.
diffPost, err := c.NextDifficulty(101, hf6ActiveForks)
require.NoError(t, err)
// With 120s actual intervals and a 240s target, LWMA should produce
// lower difficulty than with a 120s target. The post-HF6 difficulty
// should differ from the pre-HF6 difficulty because the target doubled.
require.NotEqual(t, diffPre, diffPost,
"difficulty should change across HF6 boundary (120s vs 240s target)")
}
func TestNextDifficulty_HF6Boundary_Bad(t *testing.T) {
// HF6 at height 999,999,999 (mainnet default) -- should never activate
// for realistic heights, so the target stays at 120s.
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 105, 120, 1000)
forks := config.MainnetForks
diff100, err := c.NextDifficulty(100, forks)
require.NoError(t, err)
diff101, err := c.NextDifficulty(101, forks)
require.NoError(t, err)
// Both should use the same 120s target -- no HF6 in sight.
require.Equal(t, diff100, diff101,
"difficulty should be identical when HF6 is far in the future")
}
func TestNextDifficulty_HF6Boundary_Ugly(t *testing.T) {
// HF6 at height 0 (active from genesis) -- the 240s target should
// apply from the very first difficulty calculation.
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 5, 240, 1000)
genesisHF6 := []config.HardFork{
{Version: config.HF0Initial, Height: 0},
{Version: config.HF6, Height: 0},
}
diff, err := c.NextDifficulty(4, genesisHF6)
require.NoError(t, err)
require.Greater(t, diff, uint64(0))
}
// --- PoS difficulty tests ---
func TestNextPoSDifficulty_Good(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 5, 120, 1000)
// Pre-HF6: PoS target should be 120s (same as PoW).
diff, err := c.NextPoSDifficulty(5, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(333), diff)
}
func TestNextPoSDifficulty_HF6Boundary_Good(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 105, 120, 1000)
// Height 100 -- pre-HF6.
diffPre, err := c.NextPoSDifficulty(100, hf6ActiveForks)
require.NoError(t, err)
// Height 101 -- post-HF6, target becomes 240s.
diffPost, err := c.NextPoSDifficulty(101, hf6ActiveForks)
require.NoError(t, err)
require.NotEqual(t, diffPre, diffPost,
"PoS difficulty should change across HF6 boundary")
}
func TestNextPoSDifficulty_Genesis(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
diff, err := c.NextPoSDifficulty(0, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(1), diff)
}

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,26 +11,32 @@ import (
"fmt"
"strconv"
store "forge.lthn.ai/core/go-store"
"forge.lthn.ai/core/go-blockchain/types"
coreerr "dappco.re/go/core/log"
"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 fmt.Errorf("chain: mark spent %s: %w", ki, err)
return coreerr.E("Chain.MarkSpent", fmt.Sprintf("chain: mark spent %s", ki), err)
}
return nil
}
// 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) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("chain: check spent %s: %w", ki, err)
return false, coreerr.E("Chain.IsSpent", fmt.Sprintf("chain: check spent %s", ki), err)
}
return true, nil
}
@ -46,7 +52,7 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
grp := outputGroup(amount)
count, err := c.store.Count(grp)
if err != nil {
return 0, fmt.Errorf("chain: output count: %w", err)
return 0, coreerr.E("Chain.PutOutput", "chain: output count", err)
}
gindex := uint64(count)
@ -56,12 +62,12 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
}
val, err := json.Marshal(entry)
if err != nil {
return 0, fmt.Errorf("chain: marshal output: %w", err)
return 0, coreerr.E("Chain.PutOutput", "chain: marshal output", err)
}
key := strconv.FormatUint(gindex, 10)
if err := c.store.Set(grp, key, string(val)); err != nil {
return 0, fmt.Errorf("chain: store output: %w", err)
return 0, coreerr.E("Chain.PutOutput", "chain: store output", err)
}
return gindex, nil
}
@ -73,27 +79,29 @@ func (c *Chain) GetOutput(amount uint64, gindex uint64) (types.Hash, uint32, err
val, err := c.store.Get(grp, key)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return types.Hash{}, 0, fmt.Errorf("chain: output %d:%d not found", amount, gindex)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", fmt.Sprintf("chain: output %d:%d not found", amount, gindex), nil)
}
return types.Hash{}, 0, fmt.Errorf("chain: get output: %w", err)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", "chain: get output", err)
}
var entry outputEntry
if err := json.Unmarshal([]byte(val), &entry); err != nil {
return types.Hash{}, 0, fmt.Errorf("chain: unmarshal output: %w", err)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", "chain: unmarshal output", err)
}
hash, err := types.HashFromHex(entry.TxID)
if err != nil {
return types.Hash{}, 0, fmt.Errorf("chain: parse output tx_id: %w", err)
return types.Hash{}, 0, coreerr.E("Chain.GetOutput", "chain: parse output tx_id", err)
}
return hash, entry.OutNo, nil
}
// 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 {
return 0, fmt.Errorf("chain: output count: %w", err)
return 0, coreerr.E("Chain.OutputCount", "chain: output count", err)
}
return uint64(n), 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,11 +6,10 @@
package chain
import (
"fmt"
"log"
corelog "dappco.re/go/core/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.
@ -27,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
@ -39,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 fmt.Errorf("encode timed_sync response: %w", err)
return corelog.E("LevinP2PConn.handleMessage", "encode timed_sync response", err)
}
if err := c.conn.WriteResponse(p2p.CommandTimedSync, payload, levinpkg.ReturnOK); err != nil {
return fmt.Errorf("write timed_sync response: %w", 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, fmt.Errorf("encode request_chain: %w", 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, fmt.Errorf("write request_chain: %w", 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, fmt.Errorf("read response_chain: %w", 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, fmt.Errorf("decode response_chain: %w", err)
return 0, nil, corelog.E("LevinP2PConn.RequestChain", "decode response_chain", err)
}
return resp.StartHeight, resp.BlockIDs, nil
}
@ -82,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, fmt.Errorf("encode request_get_objects: %w", 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, fmt.Errorf("write request_get_objects: %w", 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, fmt.Errorf("read response_get_objects: %w", 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, fmt.Errorf("decode response_get_objects: %w", 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,7 +8,8 @@ package chain
import (
"context"
"fmt"
"log"
corelog "dappco.re/go/core/log"
)
// P2PConnection abstracts the P2P communication needed for block sync.
@ -44,7 +45,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
localHeight, err := c.Height()
if err != nil {
return fmt.Errorf("p2p sync: get height: %w", err)
return corelog.E("Chain.P2PSync", "p2p sync: get height", err)
}
peerHeight := conn.PeerHeight()
@ -55,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 fmt.Errorf("p2p sync: build history: %w", err)
return corelog.E("Chain.P2PSync", "p2p sync: build history", err)
}
// Convert Hash to []byte for P2P.
@ -69,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 fmt.Errorf("p2p sync: request chain: %w", 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.
@ -106,24 +107,24 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
entries, err := conn.RequestObjects(batch)
if err != nil {
return fmt.Errorf("p2p sync: request objects: %w", 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 fmt.Errorf("p2p sync: compute difficulty for block %d: %w", 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 fmt.Errorf("p2p sync: process block %d: %w", 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,39 +8,74 @@ package chain
import (
"fmt"
"forge.lthn.ai/core/go-blockchain/consensus"
"forge.lthn.ai/core/go-blockchain/types"
coreerr "dappco.re/go/core/log"
"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 {
return nil, fmt.Errorf("ring output %d (amount=%d, gidx=%d): %w", i, amount, gidx, err)
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d (amount=%d, gidx=%d)", i, amount, gidx), err)
}
tx, _, err := c.GetTransaction(txHash)
if err != nil {
return nil, fmt.Errorf("ring output %d: tx %s: %w", i, txHash, err)
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: tx %s", i, txHash), err)
}
if int(outNo) >= len(tx.Vout) {
return nil, fmt.Errorf("ring output %d: tx %s has %d outputs, want index %d",
i, txHash, len(tx.Vout), outNo)
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: tx %s has %d outputs, want index %d", i, txHash, len(tx.Vout), outNo), nil)
}
switch out := tx.Vout[outNo].(type) {
case types.TxOutputBare:
pubs[i] = out.Target.Key
spendKey, err := ringOutputSpendKey(height, out.Target)
if err != nil {
return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: %v", i, err), nil)
}
publicKeys[i] = spendKey
default:
return nil, fmt.Errorf("ring output %d: unsupported output type %T", i, out)
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,
@ -48,22 +83,23 @@ 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 {
txHash, outNo, err := c.GetOutput(0, gidx)
if err != nil {
return nil, fmt.Errorf("ZC ring output %d (gidx=%d): %w", i, gidx, err)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d (gidx=%d)", i, gidx), err)
}
tx, _, err := c.GetTransaction(txHash)
if err != nil {
return nil, fmt.Errorf("ZC ring output %d: tx %s: %w", i, txHash, err)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d: tx %s", i, txHash), err)
}
if int(outNo) >= len(tx.Vout) {
return nil, fmt.Errorf("ZC ring output %d: tx %s has %d outputs, want index %d",
i, txHash, len(tx.Vout), outNo)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d: tx %s has %d outputs, want index %d", i, txHash, len(tx.Vout), outNo), nil)
}
switch out := tx.Vout[outNo].(type) {
@ -74,7 +110,7 @@ func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, er
BlindedAssetID: [32]byte(out.BlindedAssetID),
}
default:
return nil, fmt.Errorf("ZC ring output %d: expected TxOutputZarcanum, got %T", i, out)
return nil, coreerr.E("Chain.GetZCRingOutputs", fmt.Sprintf("ZC ring output %d: expected TxOutputZarcanum, got %T", i, out), nil)
}
}
return members, nil

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,9 +13,11 @@ import (
"fmt"
"strconv"
store "forge.lthn.ai/core/go-store"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
coreerr "dappco.re/go/core/log"
"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.
@ -39,12 +41,14 @@ 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)
wire.EncodeBlock(enc, b)
if err := enc.Err(); err != nil {
return fmt.Errorf("chain: encode block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: encode block %d", meta.Height), err)
}
rec := blockRecord{
@ -53,46 +57,50 @@ func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
}
val, err := json.Marshal(rec)
if err != nil {
return fmt.Errorf("chain: marshal block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: marshal block %d", meta.Height), err)
}
if err := c.store.Set(groupBlocks, heightKey(meta.Height), string(val)); err != nil {
return fmt.Errorf("chain: store block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: store block %d", meta.Height), err)
}
// Update hash -> height index.
hashHex := meta.Hash.String()
if err := c.store.Set(groupBlockIndex, hashHex, strconv.FormatUint(meta.Height, 10)); err != nil {
return fmt.Errorf("chain: index block %d: %w", meta.Height, err)
return coreerr.E("Chain.PutBlock", fmt.Sprintf("chain: index block %d", meta.Height), err)
}
return nil
}
// 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 {
if errors.Is(err, store.ErrNotFound) {
return nil, nil, fmt.Errorf("chain: block %d not found", height)
return nil, nil, coreerr.E("Chain.GetBlockByHeight", fmt.Sprintf("chain: block %d not found", height), nil)
}
return nil, nil, fmt.Errorf("chain: get block %d: %w", height, err)
return nil, nil, coreerr.E("Chain.GetBlockByHeight", fmt.Sprintf("chain: get block %d", height), err)
}
return decodeBlockRecord(val)
}
// 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 {
if errors.Is(err, store.ErrNotFound) {
return nil, nil, fmt.Errorf("chain: block %s not found", hash)
return nil, nil, coreerr.E("Chain.GetBlockByHash", fmt.Sprintf("chain: block %s not found", hash), nil)
}
return nil, nil, fmt.Errorf("chain: get block index %s: %w", hash, err)
return nil, nil, coreerr.E("Chain.GetBlockByHash", fmt.Sprintf("chain: get block index %s", hash), err)
}
height, err := strconv.ParseUint(heightStr, 10, 64)
if err != nil {
return nil, nil, fmt.Errorf("chain: parse height %q: %w", heightStr, err)
return nil, nil, coreerr.E("Chain.GetBlockByHash", fmt.Sprintf("chain: parse height %q", heightStr), err)
}
return c.GetBlockByHeight(height)
}
@ -104,12 +112,14 @@ 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)
wire.EncodeTransaction(enc, tx)
if err := enc.Err(); err != nil {
return fmt.Errorf("chain: encode tx %s: %w", hash, err)
return coreerr.E("Chain.PutTransaction", fmt.Sprintf("chain: encode tx %s", hash), err)
}
rec := txRecord{
@ -118,42 +128,46 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM
}
val, err := json.Marshal(rec)
if err != nil {
return fmt.Errorf("chain: marshal tx %s: %w", hash, err)
return coreerr.E("Chain.PutTransaction", fmt.Sprintf("chain: marshal tx %s", hash), err)
}
if err := c.store.Set(groupTx, hash.String(), string(val)); err != nil {
return fmt.Errorf("chain: store tx %s: %w", hash, err)
return coreerr.E("Chain.PutTransaction", fmt.Sprintf("chain: store tx %s", hash), err)
}
return nil
}
// 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 {
if errors.Is(err, store.ErrNotFound) {
return nil, nil, fmt.Errorf("chain: tx %s not found", hash)
return nil, nil, coreerr.E("Chain.GetTransaction", fmt.Sprintf("chain: tx %s not found", hash), nil)
}
return nil, nil, fmt.Errorf("chain: get tx %s: %w", hash, err)
return nil, nil, coreerr.E("Chain.GetTransaction", fmt.Sprintf("chain: get tx %s", hash), err)
}
var rec txRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil {
return nil, nil, fmt.Errorf("chain: unmarshal tx: %w", err)
return nil, nil, coreerr.E("Chain.GetTransaction", "chain: unmarshal tx", err)
}
blob, err := hex.DecodeString(rec.Blob)
if err != nil {
return nil, nil, fmt.Errorf("chain: decode tx hex: %w", err)
return nil, nil, coreerr.E("Chain.GetTransaction", "chain: decode tx hex", err)
}
dec := wire.NewDecoder(bytes.NewReader(blob))
tx := wire.DecodeTransaction(dec)
if err := dec.Err(); err != nil {
return nil, nil, fmt.Errorf("chain: decode tx wire: %w", err)
return nil, nil, coreerr.E("Chain.GetTransaction", "chain: decode tx wire", err)
}
return &tx, &rec.Meta, nil
}
// 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
@ -164,11 +178,11 @@ func (c *Chain) HasTransaction(hash types.Hash) bool {
func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
val, err := c.store.Get(groupBlocks, heightKey(height))
if err != nil {
return nil, fmt.Errorf("chain: block meta %d: %w", height, err)
return nil, coreerr.E("Chain.getBlockMeta", fmt.Sprintf("chain: block meta %d", height), err)
}
var rec blockRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil {
return nil, fmt.Errorf("chain: unmarshal block meta %d: %w", height, err)
return nil, coreerr.E("Chain.getBlockMeta", fmt.Sprintf("chain: unmarshal block meta %d", height), err)
}
return &rec.Meta, nil
}
@ -176,16 +190,16 @@ func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) {
var rec blockRecord
if err := json.Unmarshal([]byte(val), &rec); err != nil {
return nil, nil, fmt.Errorf("chain: unmarshal block: %w", err)
return nil, nil, coreerr.E("decodeBlockRecord", "chain: unmarshal block", err)
}
blob, err := hex.DecodeString(rec.Blob)
if err != nil {
return nil, nil, fmt.Errorf("chain: decode block hex: %w", err)
return nil, nil, coreerr.E("decodeBlockRecord", "chain: decode block hex", err)
}
dec := wire.NewDecoder(bytes.NewReader(blob))
blk := wire.DecodeBlock(dec)
if err := dec.Err(); err != nil {
return nil, nil, fmt.Errorf("chain: decode block wire: %w", err)
return nil, nil, coreerr.E("decodeBlockRecord", "chain: decode block wire", err)
}
return &blk, &rec.Meta, nil
}

View file

@ -10,17 +10,17 @@ import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log"
"regexp"
"strconv"
"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"
corelog "dappco.re/go/core/log"
"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
@ -51,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 fmt.Errorf("sync: get local height: %w", err)
return corelog.E("Chain.Sync", "sync: get local height", err)
}
remoteHeight, err := client.GetHeight()
if err != nil {
return fmt.Errorf("sync: get remote height: %w", err)
return corelog.E("Chain.Sync", "sync: get remote height", err)
}
for localHeight < remoteHeight {
@ -71,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 fmt.Errorf("sync: fetch blocks at %d: %w", localHeight, err)
return corelog.E("Chain.Sync", fmt.Sprintf("sync: fetch blocks at %d", localHeight), err)
}
if err := resolveBlockBlobs(blocks, client); err != nil {
return fmt.Errorf("sync: resolve blobs at %d: %w", 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 fmt.Errorf("sync: process block %d: %w", 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 fmt.Errorf("sync: get height after batch: %w", err)
return corelog.E("Chain.Sync", "sync: get height after batch", err)
}
}
@ -95,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 fmt.Errorf("decode block hex: %w", err)
return corelog.E("Chain.processBlock", "decode block hex", err)
}
// Build a set of the block's regular tx hashes for lookup.
@ -110,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 fmt.Errorf("decode block for tx hashes: %w", err)
return corelog.E("Chain.processBlock", "decode block for tx hashes", err)
}
regularTxs := make(map[string]struct{}, len(blk.TxHashes))
@ -125,7 +125,7 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
}
txBlobBytes, err := hex.DecodeString(txInfo.Blob)
if err != nil {
return fmt.Errorf("decode tx hex %s: %w", txInfo.ID, err)
return corelog.E("Chain.processBlock", fmt.Sprintf("decode tx hex %s", txInfo.ID), err)
}
txBlobs = append(txBlobs, txBlobBytes)
}
@ -136,11 +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 fmt.Errorf("parse daemon block hash: %w", err)
return corelog.E("Chain.processBlock", "parse daemon block hash", err)
}
if computedHash != daemonHash {
return fmt.Errorf("block hash mismatch: computed %s, daemon says %s",
computedHash, daemonHash)
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 fmt.Errorf("decode block wire: %w", err)
return corelog.E("Chain.processBlockBlobs", "decode block wire", err)
}
// Compute the block hash.
@ -165,22 +164,21 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
if height == 0 {
genesisHash, err := types.HashFromHex(GenesisHash)
if err != nil {
return fmt.Errorf("parse genesis hash: %w", err)
return corelog.E("Chain.processBlockBlobs", "parse genesis hash", err)
}
if blockHash != genesisHash {
return fmt.Errorf("genesis hash %s does not match expected %s",
blockHash, GenesisHash)
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 fmt.Errorf("validate miner tx: %w", err)
return corelog.E("Chain.processBlockBlobs", "validate miner tx", err)
}
// Calculate cumulative difficulty.
@ -188,7 +186,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
if height > 0 {
_, prevMeta, err := c.TopBlock()
if err != nil {
return fmt.Errorf("get prev block meta: %w", err)
return corelog.E("Chain.processBlockBlobs", "get prev block meta", err)
}
cumulDiff = prevMeta.CumulativeDiff + difficulty
} else {
@ -199,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 fmt.Errorf("index miner tx outputs: %w", 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 fmt.Errorf("store miner tx: %w", err)
return corelog.E("Chain.processBlockBlobs", "store miner tx", err)
}
// Process regular transactions from txBlobs.
@ -213,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 fmt.Errorf("decode tx wire [%d]: %w", 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 fmt.Errorf("validate tx %s: %w", 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 fmt.Errorf("verify tx signatures %s: %w", 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 fmt.Errorf("index tx outputs %s: %w", txHash, err)
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("index tx outputs %s", txHash), err)
}
// Mark key images as spent.
@ -241,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 fmt.Errorf("mark spent %s: %w", 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 fmt.Errorf("mark spent %s: %w", inp.KeyImage, err)
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err)
}
}
}
@ -255,7 +257,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
KeeperBlock: height,
GlobalOutputIndexes: gindexes,
}); err != nil {
return fmt.Errorf("store tx %s: %w", txHash, err)
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("store tx %s", txHash), err)
}
}
@ -330,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 fmt.Errorf("fetch tx blobs: %w", err)
return corelog.E("resolveBlockBlobs", "fetch tx blobs", err)
}
if len(missed) > 0 {
return fmt.Errorf("daemon missed %d tx(es): %v", len(missed), missed)
return corelog.E("resolveBlockBlobs", fmt.Sprintf("daemon missed %d tx(es): %v", len(missed), missed), nil)
}
if len(txHexes) != len(allHashes) {
return fmt.Errorf("expected %d tx blobs, got %d", len(allHashes), len(txHexes))
return corelog.E("resolveBlockBlobs", fmt.Sprintf("expected %d tx blobs, got %d", len(allHashes), len(txHexes)), nil)
}
// Index fetched blobs by hash.
@ -364,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 fmt.Errorf("block %d: parse header: %w", 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 fmt.Errorf("block %d has no transactions_details", bd.Height)
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 fmt.Errorf("block %d: decode miner tx hex: %w", bd.Height, err)
return corelog.E("resolveBlockBlobs", fmt.Sprintf("block %d: decode miner tx hex", bd.Height), err)
}
// Collect regular tx hashes.
@ -381,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 fmt.Errorf("block %d: parse tx hash %s: %w", 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)
}
@ -411,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, errors.New("AGGREGATED section not found in object_in_json")
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, fmt.Errorf("unmarshal AGGREGATED: %w", err)
return nil, corelog.E("parseBlockHeader", "unmarshal AGGREGATED", err)
}
prevID, err := types.HashFromHex(hj.PrevID)
if err != nil {
return nil, fmt.Errorf("parse prev_id: %w", 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

@ -7,32 +7,36 @@ package chain
import (
"bytes"
"errors"
"fmt"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/consensus"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
)
// ValidateHeader checks a block header before storage.
// expectedHeight is the height at which this block would be stored.
func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64, forks []config.HardFork) error {
currentHeight, err := c.Height()
if err != nil {
return fmt.Errorf("validate: get height: %w", err)
return coreerr.E("Chain.ValidateHeader", "validate: get height", err)
}
// Height sequence check.
if expectedHeight != currentHeight {
return fmt.Errorf("validate: expected height %d but chain is at %d",
expectedHeight, currentHeight)
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: expected height %d but chain is at %d", expectedHeight, currentHeight), nil)
}
// Genesis block: prev_id must be zero.
if expectedHeight == 0 {
if !b.PrevID.IsZero() {
return errors.New("validate: genesis block has non-zero prev_id")
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
}
@ -40,11 +44,15 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
// Non-genesis: prev_id must match top block hash.
_, topMeta, err := c.TopBlock()
if err != nil {
return fmt.Errorf("validate: get top block: %w", err)
return coreerr.E("Chain.ValidateHeader", "validate: get top block", err)
}
if b.PrevID != topMeta.Hash {
return fmt.Errorf("validate: prev_id %s does not match top block %s",
b.PrevID, topMeta.Hash)
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.
@ -52,8 +60,7 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, b)
if enc.Err() == nil && uint64(buf.Len()) > config.MaxBlockSize {
return fmt.Errorf("validate: block size %d exceeds max %d",
buf.Len(), config.MaxBlockSize)
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: block size %d exceeds max %d", buf.Len(), config.MaxBlockSize), nil)
}
return nil

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,74 +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"
"os"
"os/signal"
"path/filepath"
"sync"
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 fmt.Errorf("open store: %w", 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,142 +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"
"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 fmt.Errorf("open store: %w", 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 fmt.Errorf("daemon start: %w", err)
}
dbPath := filepath.Join(dataDir, "chain.db")
s, err := store.New(dbPath)
if err != nil {
_ = d.Stop()
return fmt.Errorf("open store: %w", 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 fmt.Errorf("no running sync daemon found")
}
proc, err := os.FindProcess(pid)
if err != nil {
return fmt.Errorf("find process %d: %w", pid, err)
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
return fmt.Errorf("signal process %d: %w", pid, err)
}
log.Printf("Sent SIGTERM to sync daemon (PID %d)", pid)
return nil
}

View file

@ -1,67 +0,0 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package blockchain
import (
"fmt"
"os"
"path/filepath"
"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 := os.MkdirAll(dataDir, 0o755); err != nil {
return fmt.Errorf("create data dir: %w", 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 {
@ -93,3 +97,17 @@ func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool {
}
return false
}
// 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 {
return hf.Height, true
}
}
return 0, false
}

View file

@ -0,0 +1,49 @@
// 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 config
import "testing"
func TestHardforkActivationHeight_Good(t *testing.T) {
tests := []struct {
name string
forks []HardFork
version uint8
want uint64
wantOK bool
}{
{"mainnet_hf5", MainnetForks, HF5, 999999999, true},
{"testnet_hf5", TestnetForks, HF5, 200, true},
{"testnet_hf4", TestnetForks, HF4Zarcanum, 100, true},
{"mainnet_hf0", MainnetForks, HF0Initial, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := HardforkActivationHeight(tt.forks, tt.version)
if ok != tt.wantOK {
t.Fatalf("HardforkActivationHeight ok = %v, want %v", ok, tt.wantOK)
}
if got != tt.want {
t.Errorf("HardforkActivationHeight = %d, want %d", got, tt.want)
}
})
}
}
func TestHardforkActivationHeight_Bad(t *testing.T) {
_, ok := HardforkActivationHeight(MainnetForks, 99)
if ok {
t.Error("HardforkActivationHeight with unknown version should return false")
}
}
func TestHardforkActivationHeight_Ugly(t *testing.T) {
_, ok := HardforkActivationHeight(nil, HF5)
if ok {
t.Error("HardforkActivationHeight with nil forks should return false")
}
}

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,8 +9,10 @@ import (
"fmt"
"slices"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types"
coreerr "dappco.re/go/core/log"
"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.
@ -21,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
@ -28,8 +32,8 @@ func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, rec
limit = config.PosBlockFutureTimeLimit
}
if blockTimestamp > adjustedTime+limit {
return fmt.Errorf("%w: %d > %d + %d", ErrTimestampFuture,
blockTimestamp, adjustedTime, limit)
return coreerr.E("CheckTimestamp", fmt.Sprintf("%d > %d + %d",
blockTimestamp, adjustedTime, limit), ErrTimestampFuture)
}
// Median check — only when we have enough history.
@ -39,8 +43,8 @@ func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, rec
median := medianTimestamp(recentTimestamps)
if blockTimestamp < median {
return fmt.Errorf("%w: %d < median %d", ErrTimestampOld,
blockTimestamp, median)
return coreerr.E("CheckTimestamp", fmt.Sprintf("%d < median %d",
blockTimestamp, median), ErrTimestampOld)
}
return nil
@ -59,21 +63,49 @@ 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 fmt.Errorf("%w: no inputs", ErrMinerTxInputs)
return coreerr.E("ValidateMinerTx", "no inputs", ErrMinerTxInputs)
}
// First input must be TxInputGenesis.
gen, ok := tx.Vin[0].(types.TxInputGenesis)
if !ok {
return fmt.Errorf("%w: first input is not txin_gen", ErrMinerTxInputs)
return coreerr.E("ValidateMinerTx", "first input is not txin_gen", ErrMinerTxInputs)
}
if gen.Height != height {
return fmt.Errorf("%w: got %d, expected %d", ErrMinerTxHeight, gen.Height, height)
return coreerr.E("ValidateMinerTx", fmt.Sprintf("got %d, expected %d", gen.Height, height), ErrMinerTxHeight)
}
// PoW blocks: exactly 1 input. PoS: exactly 2.
@ -84,15 +116,16 @@ 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 {
return fmt.Errorf("%w: invalid PoS stake input type", ErrMinerTxInputs)
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 fmt.Errorf("%w: %d inputs (expected 1 or 2)", ErrMinerTxInputs, len(tx.Vin))
return coreerr.E("ValidateMinerTx", fmt.Sprintf("%d inputs (expected 1 or 2)", len(tx.Vin)), ErrMinerTxInputs)
}
return nil
@ -100,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)
@ -107,31 +145,80 @@ 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
}
}
if outputSum > expected {
return fmt.Errorf("%w: outputs %d > expected %d", ErrRewardMismatch, outputSum, expected)
return coreerr.E("ValidateBlockReward", fmt.Sprintf("outputs %d > expected %d", outputSum, expected), ErrRewardMismatch)
}
return nil
}
// expectedBlockMajorVersion returns the expected block major version for a
// given height and fork schedule. This maps hardfork eras to block versions:
//
// HF0 (genesis) -> 0
// HF1 -> 1
// HF3 -> 2
// HF4+ -> 3
func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 {
if config.IsHardForkActive(forks, config.HF4Zarcanum, height) {
return config.CurrentBlockMajorVersion // 3
}
if config.IsHardForkActive(forks, config.HF3, height) {
return config.HF3BlockMajorVersion // 2
}
if config.IsHardForkActive(forks, config.HF1, height) {
return config.HF1BlockMajorVersion // 1
}
return config.BlockMajorVersionInitial // 0
}
// checkBlockVersion validates that the block's major version matches
// what is expected at the given height in the fork schedule.
func checkBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
expected := expectedBlockMajorVersion(forks, 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 timestamp, miner transaction structure, and reward. Transaction
// semantic validation for regular transactions should be done separately
// via ValidateTransaction for each tx in the block.
// the block version, timestamp, miner transaction structure, and reward.
// Transaction semantic validation for regular transactions should be done
// separately via ValidateTransaction for each tx in the block.
//
// consensus.ValidateBlock(&blk, height, blockSize, medianSize, totalFees, adjustedTime, recentTimestamps, config.MainnetForks)
func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, adjustedTime uint64,
recentTimestamps []uint64, forks []config.HardFork) error {
// Block major version check.
if err := checkBlockVersion(blk.MajorVersion, forks, height); err != nil {
return err
}
// Timestamp validation.
if err := CheckTimestamp(blk.Timestamp, blk.Flags, adjustedTime, recentTimestamps); err != nil {
return err
@ -149,3 +236,51 @@ func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, a
return nil
}
// IsPreHardforkFreeze reports whether the given height falls within the
// pre-hardfork transaction freeze window for the specified fork version.
// The freeze window is the PreHardforkTxFreezePeriod blocks immediately
// before the fork activation height (inclusive).
//
// For a fork with activation height H (active at heights > H):
//
// freeze applies at heights (H - period + 1) .. H
//
// 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 {
return false
}
// A fork at height 0 means active from genesis — no freeze window.
if activationHeight == 0 {
return false
}
// Guard against underflow: if activation height < period, freeze starts at 1.
freezeStart := uint64(1)
if activationHeight >= config.PreHardforkTxFreezePeriod {
freezeStart = activationHeight - config.PreHardforkTxFreezePeriod + 1
}
return height >= freezeStart && height <= activationHeight
}
// ValidateTransactionInBlock performs transaction validation including the
// 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 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,12 +302,59 @@ 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)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0, // pre-HF1 on mainnet
Timestamp: now,
Flags: 0, // PoW
},
@ -172,7 +370,7 @@ func TestValidateBlock_Bad_Timestamp(t *testing.T) {
height := uint64(100)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0, // pre-HF1 on mainnet
Timestamp: now + config.BlockFutureTimeLimit + 100,
Flags: 0,
},
@ -188,7 +386,7 @@ func TestValidateBlock_Bad_MinerTx(t *testing.T) {
height := uint64(100)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0, // pre-HF1 on mainnet
Timestamp: now,
Flags: 0,
},
@ -198,3 +396,251 @@ func TestValidateBlock_Bad_MinerTx(t *testing.T) {
err := ValidateBlock(blk, height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
assert.ErrorIs(t, err, ErrMinerTxHeight)
}
// --- Block major version tests (Task 10) ---
func TestExpectedBlockMajorVersion_Good(t *testing.T) {
tests := []struct {
name string
forks []config.HardFork
height uint64
want uint8
}{
// --- Mainnet ---
{
name: "mainnet/genesis",
forks: config.MainnetForks,
height: 0,
want: config.BlockMajorVersionInitial, // 0
},
{
name: "mainnet/pre_HF1",
forks: config.MainnetForks,
height: 5000,
want: config.BlockMajorVersionInitial, // 0
},
{
name: "mainnet/at_HF1_boundary",
forks: config.MainnetForks,
height: 10080,
want: config.BlockMajorVersionInitial, // 0 (fork at height > 10080)
},
{
name: "mainnet/post_HF1",
forks: config.MainnetForks,
height: 10081,
want: config.HF1BlockMajorVersion, // 1
},
{
name: "mainnet/well_past_HF1",
forks: config.MainnetForks,
height: 100000,
want: config.HF1BlockMajorVersion, // 1 (HF3 not yet active)
},
// --- Testnet (HF3 active from genesis) ---
{
name: "testnet/genesis",
forks: config.TestnetForks,
height: 0,
want: config.HF3BlockMajorVersion, // 2 (HF3 at 0)
},
{
name: "testnet/pre_HF4",
forks: config.TestnetForks,
height: 50,
want: config.HF3BlockMajorVersion, // 2 (HF4 at >100)
},
{
name: "testnet/post_HF4",
forks: config.TestnetForks,
height: 101,
want: config.CurrentBlockMajorVersion, // 3
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := expectedBlockMajorVersion(tt.forks, tt.height)
if got != tt.want {
t.Errorf("expectedBlockMajorVersion(%d) = %d, want %d", tt.height, got, tt.want)
}
})
}
}
func TestCheckBlockVersion_Good(t *testing.T) {
now := uint64(time.Now().Unix())
// Correct version at each mainnet/testnet era.
tests := []struct {
name string
version uint8
height uint64
forks []config.HardFork
}{
{"mainnet/v0_pre_HF1", config.BlockMajorVersionInitial, 5000, config.MainnetForks},
{"mainnet/v1_post_HF1", config.HF1BlockMajorVersion, 10081, config.MainnetForks},
{"testnet/v2_genesis", config.HF3BlockMajorVersion, 0, config.TestnetForks},
{"testnet/v3_post_HF4", config.CurrentBlockMajorVersion, 101, config.TestnetForks},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: tt.version,
Timestamp: now,
Flags: 0,
},
}
err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
require.NoError(t, err)
})
}
}
func TestCheckBlockVersion_Bad(t *testing.T) {
now := uint64(time.Now().Unix())
tests := []struct {
name string
version uint8
height uint64
forks []config.HardFork
}{
{"mainnet/v1_pre_HF1", config.HF1BlockMajorVersion, 5000, config.MainnetForks},
{"mainnet/v0_post_HF1", config.BlockMajorVersionInitial, 10081, config.MainnetForks},
{"mainnet/v2_post_HF1", config.HF3BlockMajorVersion, 10081, config.MainnetForks},
{"testnet/v1_genesis", config.HF1BlockMajorVersion, 0, config.TestnetForks},
{"testnet/v2_post_HF4", config.HF3BlockMajorVersion, 101, config.TestnetForks},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: tt.version,
Timestamp: now,
Flags: 0,
},
}
err := checkBlockVersion(blk.MajorVersion, tt.forks, tt.height)
assert.ErrorIs(t, err, ErrBlockVersion)
})
}
}
func TestCheckBlockVersion_Ugly(t *testing.T) {
now := uint64(time.Now().Unix())
// Version 255 should never be valid at any height.
blk := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 255, Timestamp: now},
}
err := checkBlockVersion(blk.MajorVersion, config.MainnetForks, 0)
assert.ErrorIs(t, err, ErrBlockVersion)
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.MajorVersion, config.MainnetForks, 10080)
require.NoError(t, err)
}
func TestValidateBlock_MajorVersion_Good(t *testing.T) {
now := uint64(time.Now().Unix())
tests := []struct {
name string
forks []config.HardFork
height uint64
version uint8
}{
// Mainnet: pre-HF1 expects version 0.
{name: "mainnet_preHF1", forks: config.MainnetForks, height: 5000, version: 0},
// Mainnet: post-HF1 expects version 1.
{name: "mainnet_postHF1", forks: config.MainnetForks, height: 20000, version: 1},
// Testnet: HF1 active from genesis, HF3 active from genesis, expects version 2.
{name: "testnet_genesis", forks: config.TestnetForks, height: 5, version: 2},
// Testnet: post-HF4 (height > 100) expects version 3.
{name: "testnet_postHF4", forks: config.TestnetForks, height: 200, version: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: tt.version,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTxForForks(tt.height, tt.forks),
}
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
require.NoError(t, err)
})
}
}
func TestValidateBlock_MajorVersion_Bad(t *testing.T) {
now := uint64(time.Now().Unix())
tests := []struct {
name string
forks []config.HardFork
height uint64
version uint8
}{
// Mainnet: pre-HF1 with wrong version 1.
{name: "mainnet_preHF1_v1", forks: config.MainnetForks, height: 5000, version: 1},
// Mainnet: post-HF1 with wrong version 0.
{name: "mainnet_postHF1_v0", forks: config.MainnetForks, height: 20000, version: 0},
// Mainnet: post-HF1 with wrong version 2.
{name: "mainnet_postHF1_v2", forks: config.MainnetForks, height: 20000, version: 2},
// Testnet: post-HF4 with wrong version 2.
{name: "testnet_postHF4_v2", forks: config.TestnetForks, height: 200, version: 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: tt.version,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTxForForks(tt.height, tt.forks),
}
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
assert.ErrorIs(t, err, ErrBlockMajorVersion)
})
}
}
func TestValidateBlock_MajorVersion_Ugly(t *testing.T) {
now := uint64(time.Now().Unix())
// Boundary test: exactly at HF1 activation height (10080) on mainnet.
// HF1 activates at heights strictly greater than 10080, so at height
// 10080 itself HF1 is NOT active; version must be 0.
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 0,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTx(10080),
}
err := ValidateBlock(blk, 10080, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
require.NoError(t, err)
// At height 10081, HF1 IS active; version must be 1.
blk2 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTx(10081),
}
err = ValidateBlock(blk2, 10081, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
require.NoError(t, err)
}

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

@ -20,6 +20,8 @@ var (
ErrInvalidOutput = errors.New("consensus: invalid output")
ErrDuplicateKeyImage = errors.New("consensus: duplicate key image in transaction")
ErrInvalidExtra = errors.New("consensus: invalid extra field")
ErrTxVersionInvalid = errors.New("consensus: invalid transaction version for current hardfork")
ErrPreHardforkFreeze = errors.New("consensus: non-coinbase transaction rejected during pre-hardfork freeze")
// Transaction economic errors.
ErrInputOverflow = errors.New("consensus: input amount overflow")
@ -27,12 +29,19 @@ var (
ErrNegativeFee = errors.New("consensus: outputs exceed inputs")
// Block errors.
ErrBlockTooLarge = errors.New("consensus: block exceeds max size")
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
// the expected version for the height in the hardfork schedule.
ErrBlockVersion = ErrBlockMajorVersion
)

View file

@ -9,12 +9,16 @@ import (
"fmt"
"math"
"forge.lthn.ai/core/go-blockchain/types"
coreerr "dappco.re/go/core/log"
"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
@ -31,7 +35,7 @@ func TxFee(tx *types.Transaction) (uint64, error) {
}
if outputSum > inputSum {
return 0, fmt.Errorf("%w: inputs=%d, outputs=%d", ErrNegativeFee, inputSum, outputSum)
return 0, coreerr.E("TxFee", fmt.Sprintf("inputs=%d, outputs=%d", inputSum, outputSum), ErrNegativeFee)
}
return inputSum - outputSum, nil
@ -46,18 +50,26 @@ func isCoinbase(tx *types.Transaction) bool {
return ok
}
// sumInputs totals all TxInputToKey amounts, checking for overflow.
// sumInputs totals all transparent input amounts, checking for overflow.
// Covers TxInputToKey, TxInputHTLC, and TxInputMultisig.
func sumInputs(tx *types.Transaction) (uint64, error) {
var total uint64
for _, vin := range tx.Vin {
toKey, ok := vin.(types.TxInputToKey)
if !ok {
var amount uint64
switch v := vin.(type) {
case types.TxInputToKey:
amount = v.Amount
case types.TxInputHTLC:
amount = v.Amount
case types.TxInputMultisig:
amount = v.Amount
default:
continue
}
if total > math.MaxUint64-toKey.Amount {
if total > math.MaxUint64-amount {
return 0, ErrInputOverflow
}
total += toKey.Amount
total += amount
}
return total, 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"
)
@ -71,3 +71,52 @@ func TestTxFee_Ugly(t *testing.T) {
_, err := TxFee(tx)
assert.ErrorIs(t, err, ErrInputOverflow)
}
// --- HTLC and multisig fee tests (Task 8) ---
func TestTxFee_HTLCInput_Good(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputHTLC{Amount: 100, KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
fee, err := TxFee(tx)
require.NoError(t, err)
assert.Equal(t, uint64(10), fee)
}
func TestTxFee_MultisigInput_Good(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputMultisig{Amount: 200},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 150, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
fee, err := TxFee(tx)
require.NoError(t, err)
assert.Equal(t, uint64(50), fee)
}
func TestTxFee_MixedInputs_Good(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
types.TxInputHTLC{Amount: 50, KeyImage: types.KeyImage{2}},
types.TxInputMultisig{Amount: 30},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 170, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
fee, err := TxFee(tx)
require.NoError(t, err)
assert.Equal(t, uint64(10), fee) // 180 - 170
}

133
consensus/freeze_test.go Normal file
View file

@ -0,0 +1,133 @@
// 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
//go:build !integration
package consensus
import (
"testing"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
)
func TestIsPreHardforkFreeze_Good(t *testing.T) {
// Testnet HF5 activates at heights > 200.
// Freeze window: heights 141..200 (activation_height - period + 1 .. activation_height).
// Note: HF5 activation height is 200, meaning HF5 is active at height > 200 = 201+.
// The freeze applies for 60 blocks *before* the fork activates, so heights 141..200.
tests := []struct {
name string
height uint64
want bool
}{
{"well_before_freeze", 100, false},
{"just_before_freeze", 140, false},
{"first_freeze_block", 141, true},
{"mid_freeze", 170, true},
{"last_freeze_block", 200, true},
{"after_hf5_active", 201, false},
{"well_after_hf5", 300, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsPreHardforkFreeze(config.TestnetForks, config.HF5, tt.height)
if got != tt.want {
t.Errorf("IsPreHardforkFreeze(testnet, HF5, %d) = %v, want %v",
tt.height, got, tt.want)
}
})
}
}
func TestIsPreHardforkFreeze_Bad(t *testing.T) {
// Mainnet HF5 is at 999999999 — freeze window starts at 999999940.
// At typical mainnet heights, no freeze.
if IsPreHardforkFreeze(config.MainnetForks, config.HF5, 50000) {
t.Error("should not be in freeze period at mainnet height 50000")
}
}
func TestIsPreHardforkFreeze_Ugly(t *testing.T) {
// Unknown fork version — never frozen.
if IsPreHardforkFreeze(config.TestnetForks, 99, 150) {
t.Error("unknown fork version should never trigger freeze")
}
// Fork at height 0 (HF0) — freeze period would be negative/underflow,
// should return false.
if IsPreHardforkFreeze(config.TestnetForks, config.HF0Initial, 0) {
t.Error("fork at genesis should not trigger freeze")
}
}
func TestValidateBlockFreeze_Good(t *testing.T) {
// During freeze, coinbase transactions should still be accepted.
// This test verifies that ValidateBlock does not reject a block
// that only contains its miner transaction during the freeze window.
// (ValidateBlock validates the miner tx; regular tx validation is
// done separately per tx.)
//
// The freeze check applies to regular transactions via
// ValidateTransactionInBlock, not to the miner tx itself.
coinbaseTx := &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 150}},
}
_ = coinbaseTx // structural test — actual block validation needs more fields
}
func TestValidateTransactionInBlock_Good(t *testing.T) {
// Outside freeze window — regular transaction accepted.
tx := validV2Tx()
blob := make([]byte, 100)
err := ValidateTransactionInBlock(tx, blob, config.TestnetForks, 130)
if err != nil {
t.Errorf("expected no error outside freeze, got: %v", err)
}
}
func TestValidateTransactionInBlock_Bad(t *testing.T) {
// Inside freeze window — regular transaction rejected.
tx := validV2Tx()
blob := make([]byte, 100)
err := ValidateTransactionInBlock(tx, blob, config.TestnetForks, 150)
if err == nil {
t.Error("expected ErrPreHardforkFreeze during freeze window")
}
}
func TestValidateTransactionInBlock_Ugly(t *testing.T) {
// Coinbase transaction during freeze — the freeze check itself should
// not reject it (coinbase is exempt). The isCoinbase guard must pass.
// Note: ValidateTransaction separately rejects txin_gen in regular txs,
// but that is the expected path — coinbase txs are validated via
// ValidateMinerTx, not ValidateTransaction. This test verifies the
// freeze guard specifically exempts coinbase inputs.
tx := &types.Transaction{
Version: types.VersionPostHF4,
Vin: []types.TxInput{types.TxInputGenesis{Height: 150}},
Vout: []types.TxOutput{
types.TxOutputZarcanum{StealthAddress: types.PublicKey{1}},
types.TxOutputZarcanum{StealthAddress: types.PublicKey{2}},
},
}
// Directly verify the freeze exemption — isCoinbase should return true,
// and the freeze check should not trigger.
if !isCoinbase(tx) {
t.Fatal("expected coinbase transaction to be identified as coinbase")
}
if IsPreHardforkFreeze(config.TestnetForks, config.HF5, 150) {
// Good — we are in the freeze window. Coinbase should still bypass.
// The freeze check in ValidateTransactionInBlock gates on !isCoinbase,
// so coinbase txs never hit ErrPreHardforkFreeze.
} else {
t.Fatal("expected height 150 to be in freeze window")
}
}

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

@ -6,16 +6,19 @@
package consensus
import (
"errors"
"fmt"
"math/bits"
"forge.lthn.ai/core/go-blockchain/config"
coreerr "dappco.re/go/core/log"
"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
@ -32,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 {
@ -43,7 +48,7 @@ func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
}
if blockSize > 2*effectiveMedian {
return 0, fmt.Errorf("consensus: block size %d too large for median %d", blockSize, effectiveMedian)
return 0, coreerr.E("BlockReward", fmt.Sprintf("consensus: block size %d too large for median %d", blockSize, effectiveMedian), nil)
}
// penalty = baseReward * (2*median - size) * size / median²
@ -56,7 +61,7 @@ func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
// Since hi1 should be 0 for reasonable block sizes, simplify:
if hi1 > 0 {
return 0, errors.New("consensus: reward overflow")
return 0, coreerr.E("BlockReward", "consensus: reward overflow", nil)
}
hi2, lo2 := bits.Mul64(baseReward, lo1)
@ -71,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,18 +8,44 @@ package consensus
import (
"fmt"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types"
coreerr "dappco.re/go/core/log"
"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 {
return fmt.Errorf("%w: %d bytes", ErrTxTooLarge, len(txBlob))
return coreerr.E("ValidateTransaction", fmt.Sprintf("%d bytes", len(txBlob)), ErrTxTooLarge)
}
// 2. Input count.
@ -27,16 +53,21 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
return ErrNoInputs
}
if uint64(len(tx.Vin)) > config.TxMaxAllowedInputs {
return fmt.Errorf("%w: %d", ErrTooManyInputs, len(tx.Vin))
return coreerr.E("ValidateTransaction", fmt.Sprintf("%d", len(tx.Vin)), ErrTooManyInputs)
}
// 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
}
@ -54,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
}
@ -63,44 +94,99 @@ 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 fmt.Errorf("%w: txin_gen in regular transaction", ErrInvalidInputType)
default:
// Future types (multisig, HTLC, ZC) — accept if HF4+.
if !hf4Active {
return fmt.Errorf("%w: tag %d pre-HF4", ErrInvalidInputType, vin.InputType())
return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType)
case types.TxInputHTLC, types.TxInputMultisig:
// HTLC and multisig inputs require at least HF1.
if !state.hardForkOneActive {
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 {
return fmt.Errorf("%w: %d (min %d)", ErrTooFewOutputs, 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)
}
if uint64(len(tx.Vout)) > config.TxMaxAllowedOutputs {
return fmt.Errorf("%w: %d", ErrTooManyOutputs, len(tx.Vout))
return coreerr.E("checkOutputs", fmt.Sprintf("%d", len(tx.Vout)), ErrTooManyOutputs)
}
for i, vout := range tx.Vout {
switch o := vout.(type) {
case types.TxOutputBare:
if o.Amount == 0 {
return fmt.Errorf("%w: output %d has zero amount", ErrInvalidOutput, i)
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)
}
}
@ -110,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 fmt.Errorf("%w: %s", ErrDuplicateKeyImage, toKey.KeyImage)
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
@ -133,3 +139,333 @@ func TestValidateTransaction_NegativeFee(t *testing.T) {
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
assert.ErrorIs(t, err, ErrNegativeFee)
}
// --- HF1 gating tests (Task 7) ---
func TestCheckInputTypes_HTLCPreHF1_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputHTLC{Amount: 100, KeyImage: types.KeyImage{1}},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) // pre-HF1 (10080)
assert.ErrorIs(t, err, ErrInvalidInputType)
}
func TestCheckInputTypes_HTLCPostHF1_Good(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputHTLC{
Amount: 100,
KeyImage: types.KeyImage{1},
},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1
require.NoError(t, err)
}
func TestCheckInputTypes_MultisigPreHF1_Bad(t *testing.T) {
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputMultisig{Amount: 100},
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
assert.ErrorIs(t, err, ErrInvalidInputType)
}
func TestCheckOutputs_HTLCTargetPreHF1_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.TxOutHTLC{Expiration: 20000},
},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
assert.ErrorIs(t, err, ErrInvalidOutput)
}
func TestCheckOutputs_MultisigTargetPreHF1_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.TxOutMultisig{MinimumSigs: 2, Keys: []types.PublicKey{{1}, {2}}},
},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
assert.ErrorIs(t, err, ErrInvalidOutput)
}
func TestCheckOutputs_MultisigTargetPostHF1_Good(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.TxOutMultisig{MinimumSigs: 2, Keys: []types.PublicKey{{1}, {2}}},
},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1
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) {
ki := types.KeyImage{0x42}
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputHTLC{Amount: 100, KeyImage: ki},
types.TxInputHTLC{Amount: 50, KeyImage: ki}, // duplicate
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 140, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1
assert.ErrorIs(t, err, ErrDuplicateKeyImage)
}
func TestCheckKeyImages_HTLCAndToKeyDuplicate_Bad(t *testing.T) {
ki := types.KeyImage{0x42}
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: ki},
types.TxInputHTLC{Amount: 50, KeyImage: ki}, // duplicate across types
},
Vout: []types.TxOutput{
types.TxOutputBare{Amount: 140, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 20000)
assert.ErrorIs(t, err, ErrDuplicateKeyImage)
}
func TestCheckOutputs_HTLCTargetPostHF1_Good(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.TxOutHTLC{Expiration: 20000},
},
},
}
blob := make([]byte, 100)
err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1
require.NoError(t, err)
}

View file

@ -0,0 +1,148 @@
// 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
//go:build !integration
package consensus
import (
"testing"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
)
// validV2Tx returns a minimal valid v2 (Zarcanum) transaction for testing.
func validV2Tx() *types.Transaction {
return &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}},
},
}
}
// validV3Tx returns a minimal valid v3 (HF5) transaction for testing.
func validV3Tx() *types.Transaction {
return &types.Transaction{
Version: types.VersionPostHF5,
HardforkID: 5,
Vin: []types.TxInput{
types.TxInputZC{
KeyImage: types.KeyImage{1},
},
},
Vout: []types.TxOutput{
types.TxOutputZarcanum{StealthAddress: types.PublicKey{1}},
types.TxOutputZarcanum{StealthAddress: types.PublicKey{2}},
},
}
}
func TestCheckTxVersion_Good(t *testing.T) {
tests := []struct {
name string
tx *types.Transaction
forks []config.HardFork
height uint64
}{
// v1 transaction before HF4 — valid.
{"v1_before_hf4", validV1Tx(), config.MainnetForks, 5000},
// v2 transaction after HF4, before HF5 — valid.
{"v2_after_hf4_before_hf5", validV2Tx(), config.TestnetForks, 150},
// v3 transaction after HF5 — valid.
{"v3_after_hf5", validV3Tx(), config.TestnetForks, 250},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkTxVersion(tt.tx, newTransactionForkState(tt.forks, tt.height), tt.height)
if err != nil {
t.Errorf("checkTxVersion returned unexpected error: %v", err)
}
})
}
}
func TestCheckTxVersion_Bad(t *testing.T) {
tests := []struct {
name string
tx *types.Transaction
forks []config.HardFork
height uint64
}{
// v0 regular transaction before HF4 — must still be v1.
{"v0_before_hf4", func() *types.Transaction {
tx := validV1Tx()
tx.Version = types.VersionInitial
return tx
}(), config.MainnetForks, 5000},
// v1 transaction after HF4 — must be v2.
{"v1_after_hf4", validV1Tx(), config.TestnetForks, 150},
// v2 transaction after HF5 — must be v3.
{"v2_after_hf5", validV2Tx(), config.TestnetForks, 250},
// v3 transaction after HF4 but before HF5 — too early.
{"v3_after_hf4_before_hf5", validV3Tx(), config.TestnetForks, 150},
// v3 transaction after HF5 with wrong hardfork id.
{"v3_after_hf5_wrong_hardfork", func() *types.Transaction {
tx := validV3Tx()
tx.HardforkID = 4
return tx
}(), config.TestnetForks, 250},
// v3 transaction before HF5 — too early.
{"v3_before_hf5", validV3Tx(), config.TestnetForks, 150},
// future version must be rejected.
{"v4_after_hf5", func() *types.Transaction {
tx := validV3Tx()
tx.Version = types.VersionPostHF5 + 1
return tx
}(), config.TestnetForks, 250},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkTxVersion(tt.tx, newTransactionForkState(tt.forks, tt.height), tt.height)
if err == nil {
t.Error("expected ErrTxVersionInvalid, got nil")
}
})
}
}
func TestCheckTxVersion_Ugly(t *testing.T) {
// v2 at exact HF4 activation boundary (height 101 on testnet, HF4.Height=100).
txHF4 := validV2Tx()
err := checkTxVersion(txHF4, newTransactionForkState(config.TestnetForks, 101), 101)
if err != nil {
t.Errorf("v2 at HF4 activation boundary should be valid: %v", err)
}
// v1 at exact HF4 activation boundary should be rejected.
txPreHF4 := validV1Tx()
err = checkTxVersion(txPreHF4, newTransactionForkState(config.TestnetForks, 101), 101)
if err == nil {
t.Error("v1 at HF4 activation boundary should be rejected")
}
// v3 at exact HF5 activation boundary (height 201 on testnet, HF5.Height=200).
tx := validV3Tx()
err = checkTxVersion(tx, newTransactionForkState(config.TestnetForks, 201), 201)
if err != nil {
t.Errorf("v3 at HF5 activation boundary should be valid: %v", err)
}
// v2 at exact HF5 activation boundary — should be rejected.
tx2 := validV2Tx()
err = checkTxVersion(tx2, newTransactionForkState(config.TestnetForks, 201), 201)
if err == nil {
t.Error("v2 at HF5 activation boundary should be rejected")
}
}

View file

@ -9,8 +9,10 @@ import (
"bytes"
"fmt"
"forge.lthn.ai/core/go-blockchain/types"
"forge.lthn.ai/core/go-blockchain/wire"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
)
// zcSigData holds the parsed components of a ZC_sig variant element
@ -38,14 +40,14 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
dec := wire.NewDecoder(bytes.NewReader(raw))
count := dec.ReadVarint()
if dec.Err() != nil {
return nil, fmt.Errorf("read sig count: %w", dec.Err())
return nil, coreerr.E("parseV2Signatures", "read sig count", dec.Err())
}
entries := make([]v2SigEntry, 0, count)
for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8()
if dec.Err() != nil {
return nil, fmt.Errorf("read sig tag %d: %w", i, dec.Err())
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("read sig tag %d", i), dec.Err())
}
entry := v2SigEntry{tag: tag}
@ -54,7 +56,7 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
case types.SigTypeZC:
zc, err := parseZCSig(dec)
if err != nil {
return nil, fmt.Errorf("parse ZC_sig %d: %w", i, err)
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("parse ZC_sig %d", i), err)
}
entry.zcSig = zc
@ -74,11 +76,11 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
skipZarcanumSig(dec)
default:
return nil, fmt.Errorf("unsupported sig tag 0x%02x", tag)
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("unsupported sig tag 0x%02x", tag), nil)
}
if dec.Err() != nil {
return nil, fmt.Errorf("parse sig %d (tag 0x%02x): %w", i, tag, dec.Err())
return nil, coreerr.E("parseV2Signatures", fmt.Sprintf("parse sig %d (tag 0x%02x)", i, tag), dec.Err())
}
entries = append(entries, entry)
}
@ -117,7 +119,7 @@ func parseZCSig(dec *wire.Decoder) (*zcSigData, error) {
}
if rgCount != rxCount {
return nil, fmt.Errorf("CLSAG r_g count %d != r_x count %d", rgCount, rxCount)
return nil, coreerr.E("parseZCSig", fmt.Sprintf("CLSAG r_g count %d != r_x count %d", rgCount, rxCount), nil)
}
zc.ringSize = int(rgCount)
@ -155,9 +157,9 @@ func skipZarcanumSig(dec *wire.Decoder) {
_ = dec.ReadBytes(32)
// CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32).
_ = dec.ReadBytes(32) // c
skipVecOfPoints(dec) // r_g
skipVecOfPoints(dec) // r_x
_ = dec.ReadBytes(32) // c
skipVecOfPoints(dec) // r_g
skipVecOfPoints(dec) // r_x
_ = dec.ReadBytes(128) // K1+K2+K3+K4
}
@ -196,48 +198,48 @@ func parseV2Proofs(raw []byte) (*v2ProofData, error) {
dec := wire.NewDecoder(bytes.NewReader(raw))
count := dec.ReadVarint()
if dec.Err() != nil {
return nil, fmt.Errorf("read proof count: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "read proof count", dec.Err())
}
var data v2ProofData
for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8()
if dec.Err() != nil {
return nil, fmt.Errorf("read proof tag %d: %w", i, dec.Err())
return nil, coreerr.E("parseV2Proofs", fmt.Sprintf("read proof tag %d", i), dec.Err())
}
switch tag {
case 46: // zc_asset_surjection_proof: varint(nBGE) + nBGE * BGE_proof
nBGE := dec.ReadVarint()
if dec.Err() != nil {
return nil, fmt.Errorf("parse BGE count: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse BGE count", dec.Err())
}
data.bgeProofs = make([][]byte, nBGE)
for j := uint64(0); j < nBGE; j++ {
data.bgeProofs[j] = readBGEProofBytes(dec)
if dec.Err() != nil {
return nil, fmt.Errorf("parse BGE proof %d: %w", j, dec.Err())
return nil, coreerr.E("parseV2Proofs", fmt.Sprintf("parse BGE proof %d", j), dec.Err())
}
}
case 47: // zc_outs_range_proof: bpp_serialized + aggregation_proof
data.bppProofBytes = readBPPBytes(dec)
if dec.Err() != nil {
return nil, fmt.Errorf("parse BPP proof: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse BPP proof", dec.Err())
}
data.bppCommitments = readAggregationCommitments(dec)
if dec.Err() != nil {
return nil, fmt.Errorf("parse aggregation proof: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse aggregation proof", dec.Err())
}
case 48: // zc_balance_proof: 96 bytes (c, y0, y1)
data.balanceProof = dec.ReadBytes(96)
if dec.Err() != nil {
return nil, fmt.Errorf("parse balance proof: %w", dec.Err())
return nil, coreerr.E("parseV2Proofs", "parse balance proof", dec.Err())
}
default:
return nil, fmt.Errorf("unsupported proof tag 0x%02x", tag)
return nil, coreerr.E("parseV2Proofs", fmt.Sprintf("unsupported proof tag 0x%02x", tag), nil)
}
}

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

@ -6,18 +6,19 @@
package consensus
import (
"errors"
"fmt"
"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"
coreerr "dappco.re/go/core/log"
"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
@ -40,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 {
@ -48,28 +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 fmt.Errorf("consensus: signature count %d != input count %d",
len(tx.Signatures), keyInputCount)
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,27 +87,38 @@ 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 fmt.Errorf("consensus: failed to fetch ring outputs for input %d: %w",
sigIdx, err)
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err)
}
ringSigs := tx.Signatures[sigIdx]
if len(ringSigs) != len(ringKeys) {
return fmt.Errorf("consensus: input %d has %d signatures but ring size %d",
sigIdx, len(ringSigs), len(ringKeys))
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: input %d has %d signatures but ring size %d", sigIdx, len(ringSigs), len(ringKeys)), nil)
}
// Convert typed slices to raw byte arrays for the crypto bridge.
@ -116,8 +132,8 @@ 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) {
return fmt.Errorf("consensus: ring signature verification failed for input %d", sigIdx)
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)
}
sigIdx++
@ -131,27 +147,29 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
// Parse the signature variant vector.
sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
if err != nil {
return fmt.Errorf("consensus: %w", err)
return coreerr.E("verifyV2Signatures", "consensus", err)
}
// Match signatures to inputs: each input must have a corresponding signature.
if len(sigEntries) != len(tx.Vin) {
return fmt.Errorf("consensus: V2 signature count %d != input count %d",
len(sigEntries), len(tx.Vin))
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:
if sigEntries[i].tag != types.SigTypeZC {
return fmt.Errorf("consensus: input %d is ZC but signature tag is 0x%02x",
i, sigEntries[i].tag)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is ZC but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
}
case types.TxInputToKey:
if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid {
return fmt.Errorf("consensus: input %d is to_key but signature tag is 0x%02x",
i, sigEntries[i].tag)
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)
}
}
}
@ -175,7 +193,7 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
zc := sigEntries[i].zcSig
if zc == nil {
return fmt.Errorf("consensus: input %d: missing ZC_sig data", i)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: missing ZC_sig data", i), nil)
}
// Extract absolute global indices from key offsets.
@ -186,12 +204,11 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
ringMembers, err := getZCRingOutputs(offsets)
if err != nil {
return fmt.Errorf("consensus: failed to fetch ZC ring outputs for input %d: %w", i, err)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: failed to fetch ZC ring outputs for input %d", i), err)
}
if len(ringMembers) != zc.ringSize {
return fmt.Errorf("consensus: input %d: ring size %d from chain != %d from sig",
i, len(ringMembers), zc.ringSize)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: ring size %d from chain != %d from sig", i, len(ringMembers), zc.ringSize), nil)
}
// Build flat ring: [stealth(32) | commitment(32) | blinded_asset_id(32)] per entry.
@ -210,20 +227,20 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
[32]byte(zcIn.KeyImage),
zc.clsagFlatSig,
) {
return fmt.Errorf("consensus: CLSAG GGX verification failed for input %d", i)
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: CLSAG GGX verification failed for input %d", i), nil)
}
}
// Parse and verify proofs.
proofs, err := parseV2Proofs(tx.Proofs)
if err != nil {
return fmt.Errorf("consensus: %w", err)
return coreerr.E("verifyV2Signatures", "consensus", err)
}
// Verify BPP range proof if present.
if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 {
if !crypto.VerifyBPP(proofs.bppProofBytes, proofs.bppCommitments) {
return errors.New("consensus: BPP range proof verification failed")
return coreerr.E("verifyV2Signatures", "consensus: BPP range proof verification failed", nil)
}
}
@ -236,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
}
@ -264,8 +282,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
}
if len(proofs.bgeProofs) != len(outputAssetIDs) {
return fmt.Errorf("consensus: BGE proof count %d != Zarcanum output count %d",
len(proofs.bgeProofs), len(outputAssetIDs))
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof count %d != Zarcanum output count %d", len(proofs.bgeProofs), len(outputAssetIDs)), nil)
}
// Collect pseudo-out asset IDs from ZC signatures and expand to full points.
@ -281,7 +298,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
for i, p := range pseudoOutAssetIDs {
full, err := crypto.PointMul8(p)
if err != nil {
return fmt.Errorf("consensus: mul8 pseudo-out asset ID %d: %w", i, err)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: mul8 pseudo-out asset ID %d", i), err)
}
mul8PseudoOuts[i] = full
}
@ -292,7 +309,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
// mul8 the output's blinded asset ID.
mul8Out, err := crypto.PointMul8(outAssetID)
if err != nil {
return fmt.Errorf("consensus: mul8 output asset ID %d: %w", j, err)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: mul8 output asset ID %d", j), err)
}
// ring[i] = mul8(pseudo_out_i) - mul8(output_j)
@ -300,13 +317,13 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
for i, mul8Pseudo := range mul8PseudoOuts {
diff, err := crypto.PointSub(mul8Pseudo, mul8Out)
if err != nil {
return fmt.Errorf("consensus: BGE ring[%d][%d] sub: %w", j, i, err)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE ring[%d][%d] sub", j, i), err)
}
ring[i] = diff
}
if !crypto.VerifyBGE(context, ring, proofs.bgeProofs[j]) {
return fmt.Errorf("consensus: BGE proof verification failed for output %d", j)
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof verification failed for output %d", j), 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,7 +5,8 @@ package consensus
import (
"testing"
"forge.lthn.ai/core/go-blockchain/config"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -23,3 +24,72 @@ func TestVerifyTransactionSignatures_Bad_MissingSigs(t *testing.T) {
err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil, nil)
assert.Error(t, err)
}
// --- HTLC signature verification tests (Task 9) ---
func TestVerifyV1Signatures_MixedHTLC_Good(t *testing.T) {
// Structural check only (getRingOutputs = nil).
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
types.TxInputHTLC{Amount: 50, KeyImage: types.KeyImage{2}},
},
Signatures: [][]types.Signature{
{{1}}, // sig for TxInputToKey
{{2}}, // sig for TxInputHTLC
},
}
err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil)
require.NoError(t, err)
}
func TestVerifyV1Signatures_MixedHTLC_Bad(t *testing.T) {
// Wrong signature count.
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
types.TxInputHTLC{Amount: 50, KeyImage: types.KeyImage{2}},
},
Signatures: [][]types.Signature{
{{1}}, // only 1 sig for 2 ring inputs
},
}
err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "signature count")
}
func TestVerifyV1Signatures_HTLCOnly_Good(t *testing.T) {
// Transaction with only HTLC inputs.
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputHTLC{Amount: 50, KeyImage: types.KeyImage{1}},
types.TxInputHTLC{Amount: 30, KeyImage: types.KeyImage{2}},
},
Signatures: [][]types.Signature{
{{1}},
{{2}},
},
}
err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil)
require.NoError(t, err)
}
func TestVerifyV1Signatures_MultisigSkipped_Good(t *testing.T) {
// Multisig inputs do not participate in NLSAG signatures.
tx := &types.Transaction{
Version: types.VersionPreHF4,
Vin: []types.TxInput{
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
types.TxInputMultisig{Amount: 50},
},
Signatures: [][]types.Signature{
{{1}}, // only 1 sig, multisig is not counted
},
}
err := VerifyTransactionSignatures(tx, config.MainnetForks, 20000, nil, nil)
require.NoError(t, err)
}

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,316 @@
# HF3 Block Version Validation Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox syntax for tracking.
**Goal:** Add block major version validation to `consensus/block.go` so the Go node enforces the correct block version at every hardfork boundary (HF0 through HF4+). This satisfies the HF3 spec and also covers HF1's block version requirement (HF1 plan Task 10).
**Architecture:** Two unexported pure functions (`checkBlockVersion`, `expectedBlockMajorVersion`) in `consensus/block.go`, called from `ValidateBlock`. One new sentinel error in `consensus/errors.go`. No new dependencies, no storage, no CGo.
**Tech Stack:** Go 1.26, `go test -race`, stdlib `testing` + testify assertions
---
## File Map
### Modified files
| File | What changes |
|------|-------------|
| `consensus/errors.go` | Add `ErrBlockVersion` sentinel error to the block errors group. |
| `consensus/block.go` | Add `checkBlockVersion` and `expectedBlockMajorVersion` functions. Call `checkBlockVersion` from `ValidateBlock` before timestamp validation. |
| `consensus/block_test.go` | Add table-driven tests for `checkBlockVersion` and `expectedBlockMajorVersion` covering all hardfork boundaries on both mainnet and testnet fork schedules. |
---
## Task 1: Sentinel error + expectedBlockMajorVersion + checkBlockVersion
**Package:** `consensus/`
**Why:** The version lookup and check are pure functions with no side effects. Delivering them together with their tests in one task keeps the change atomic and reviewable.
### Step 1.1 — Add ErrBlockVersion sentinel error
- [ ] Edit `/home/claude/Code/core/go-blockchain/consensus/errors.go`
Add to the block errors group, after `ErrMinerTxProofs`:
```go
ErrBlockVersion = errors.New("consensus: invalid block major version for height")
```
### Step 1.2 — Write tests for expectedBlockMajorVersion and checkBlockVersion
- [ ] Append to `/home/claude/Code/core/go-blockchain/consensus/block_test.go`
```go
func TestExpectedBlockMajorVersion_Good(t *testing.T) {
tests := []struct {
name string
forks []config.HardFork
height uint64
want uint8
}{
// --- Mainnet ---
{
name: "mainnet/genesis",
forks: config.MainnetForks,
height: 0,
want: config.BlockMajorVersionInitial, // 0
},
{
name: "mainnet/pre_HF1",
forks: config.MainnetForks,
height: 5000,
want: config.BlockMajorVersionInitial, // 0
},
{
name: "mainnet/at_HF1_boundary",
forks: config.MainnetForks,
height: 10080,
want: config.BlockMajorVersionInitial, // 0 (fork at height > 10080)
},
{
name: "mainnet/post_HF1",
forks: config.MainnetForks,
height: 10081,
want: config.HF1BlockMajorVersion, // 1
},
{
name: "mainnet/well_past_HF1",
forks: config.MainnetForks,
height: 100000,
want: config.HF1BlockMajorVersion, // 1 (HF3 not yet active)
},
// --- Testnet (HF3 active from genesis) ---
{
name: "testnet/genesis",
forks: config.TestnetForks,
height: 0,
want: config.HF3BlockMajorVersion, // 2 (HF3 at 0)
},
{
name: "testnet/pre_HF4",
forks: config.TestnetForks,
height: 50,
want: config.HF3BlockMajorVersion, // 2 (HF4 at >100)
},
{
name: "testnet/post_HF4",
forks: config.TestnetForks,
height: 101,
want: config.CurrentBlockMajorVersion, // 3
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := expectedBlockMajorVersion(tt.height, tt.forks)
if got != tt.want {
t.Errorf("expectedBlockMajorVersion(%d) = %d, want %d", tt.height, got, tt.want)
}
})
}
}
func TestCheckBlockVersion_Good(t *testing.T) {
// Correct version at each mainnet era.
tests := []struct {
name string
version uint8
height uint64
forks []config.HardFork
}{
{"mainnet/v0_pre_HF1", config.BlockMajorVersionInitial, 5000, config.MainnetForks},
{"mainnet/v1_post_HF1", config.HF1BlockMajorVersion, 10081, config.MainnetForks},
{"testnet/v2_genesis", config.HF3BlockMajorVersion, 0, config.TestnetForks},
{"testnet/v3_post_HF4", config.CurrentBlockMajorVersion, 101, config.TestnetForks},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkBlockVersion(tt.version, tt.height, tt.forks)
require.NoError(t, err)
})
}
}
func TestCheckBlockVersion_Bad(t *testing.T) {
tests := []struct {
name string
version uint8
height uint64
forks []config.HardFork
}{
{"mainnet/v1_pre_HF1", config.HF1BlockMajorVersion, 5000, config.MainnetForks},
{"mainnet/v0_post_HF1", config.BlockMajorVersionInitial, 10081, config.MainnetForks},
{"mainnet/v2_post_HF1", config.HF3BlockMajorVersion, 10081, config.MainnetForks},
{"testnet/v1_genesis", config.HF1BlockMajorVersion, 0, config.TestnetForks},
{"testnet/v2_post_HF4", config.HF3BlockMajorVersion, 101, config.TestnetForks},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkBlockVersion(tt.version, tt.height, tt.forks)
assert.ErrorIs(t, err, ErrBlockVersion)
})
}
}
func TestCheckBlockVersion_Ugly(t *testing.T) {
// Version 255 should never be valid at any height.
err := checkBlockVersion(255, 0, config.MainnetForks)
assert.ErrorIs(t, err, ErrBlockVersion)
err = checkBlockVersion(255, 10081, config.MainnetForks)
assert.ErrorIs(t, err, ErrBlockVersion)
// Version 0 at the exact HF1 boundary (height 10080 — fork not yet active).
err = checkBlockVersion(config.BlockMajorVersionInitial, 10080, config.MainnetForks)
require.NoError(t, err)
}
```
### Step 1.3 — Run tests, verify FAIL
```bash
cd /home/claude/Code/core/go-blockchain && go test -race -run "TestExpectedBlockMajorVersion|TestCheckBlockVersion" ./consensus/...
```
**Expected:** Compilation error — `expectedBlockMajorVersion`, `checkBlockVersion`, and `ErrBlockVersion` do not exist yet.
### Step 1.4 — Implement expectedBlockMajorVersion and checkBlockVersion
- [ ] Edit `/home/claude/Code/core/go-blockchain/consensus/block.go`
Add after the `medianTimestamp` function, before `ValidateMinerTx`:
```go
// expectedBlockMajorVersion returns the required block major version for the
// given height based on the active hardfork schedule.
func expectedBlockMajorVersion(height uint64, forks []config.HardFork) uint8 {
switch {
case config.IsHardForkActive(forks, config.HF4Zarcanum, height):
return config.CurrentBlockMajorVersion // 3
case config.IsHardForkActive(forks, config.HF3, height):
return config.HF3BlockMajorVersion // 2
case config.IsHardForkActive(forks, config.HF1, height):
return config.HF1BlockMajorVersion // 1
default:
return config.BlockMajorVersionInitial // 0
}
}
// checkBlockVersion validates that the block's major version matches the
// expected version for its height in the hardfork schedule.
func checkBlockVersion(majorVersion uint8, height uint64, forks []config.HardFork) error {
expected := expectedBlockMajorVersion(height, forks)
if majorVersion != expected {
return fmt.Errorf("%w: got %d, expected %d at height %d",
ErrBlockVersion, majorVersion, expected, height)
}
return nil
}
```
### Step 1.5 — Wire checkBlockVersion into ValidateBlock
- [ ] Edit `/home/claude/Code/core/go-blockchain/consensus/block.go``ValidateBlock`
Add at the top of the function body, before the timestamp validation comment:
```go
// Block major version check.
if err := checkBlockVersion(blk.MajorVersion, height, forks); err != nil {
return err
}
```
### Step 1.6 — Run new tests, verify PASS
```bash
cd /home/claude/Code/core/go-blockchain && go test -race -run "TestExpectedBlockMajorVersion|TestCheckBlockVersion" ./consensus/...
```
**Expected:** All PASS.
### Step 1.7 — Run full consensus test suite
```bash
cd /home/claude/Code/core/go-blockchain && go test -race ./consensus/...
```
**Expected:** Existing `TestValidateBlock_Good` may FAIL because it uses `MajorVersion: 1` at height 100 (pre-HF1, where version 0 is expected). If so, fix the test block's `MajorVersion` to `0`. Repeat until all PASS.
**Likely fix** in `TestValidateBlock_Good` — change `MajorVersion: 1` to `MajorVersion: 0` (height 100 is pre-HF1 on mainnet):
```go
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 0, // height 100 is pre-HF1
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTx(height),
}
```
Also update `TestValidateBlock_Bad_Timestamp` and `TestValidateBlock_Bad_MinerTx` if they use `MajorVersion: 1` at pre-HF1 heights.
### Step 1.8 — Run vet + mod tidy
```bash
cd /home/claude/Code/core/go-blockchain && go vet ./consensus/... && go mod tidy
```
**Expected:** Clean.
### Step 1.9 — Commit
```bash
cd /home/claude/Code/core/go-blockchain
git add consensus/errors.go consensus/block.go consensus/block_test.go
git commit -m "feat(consensus): validate block major version across all hardforks
Add checkBlockVersion and expectedBlockMajorVersion to enforce the correct
block major version at every hardfork boundary (v0 pre-HF1, v1 post-HF1,
v2 post-HF3, v3 post-HF4). This covers HF3's version gate and also
satisfies HF1 plan Task 10.
Co-Authored-By: Charon <charon@lethean.io>"
```
---
## Task 2: ValidateBlock integration — verify existing callers still work
**Package:** `consensus/`, `chain/`
**Why:** `ValidateBlock` gained a new early-return path. Callers in `chain/` (block storage and sync) must still compile and pass their tests.
### Step 2.1 — Run full test suite
```bash
cd /home/claude/Code/core/go-blockchain && go test -race ./...
```
**Expected:** All PASS. If any test in `chain/` or elsewhere constructs a `types.Block` with the wrong `MajorVersion` for its height, fix the test data.
### Step 2.2 — Run vet across entire module
```bash
cd /home/claude/Code/core/go-blockchain && go vet ./...
```
**Expected:** Clean.
### Step 2.3 — Commit (only if test fixes were needed)
```bash
cd /home/claude/Code/core/go-blockchain
git add -A
git commit -m "test: fix block MajorVersion in existing tests for version validation
Update test blocks to use the correct MajorVersion for their height
now that ValidateBlock enforces version checks.
Co-Authored-By: Charon <charon@lethean.io>"
```
If no fixes were needed, skip this commit.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,398 @@
# HF6 Block Time Halving Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox syntax for tracking.
**Goal:** Correct the difficulty target gate from HF2 to HF6 so the PoW target stays at 120s until HF6 activates, then switches to 240s. Add the matching PoS difficulty function that follows the same HF6 gate.
**Architecture:** `chain/difficulty.go` already computes PoW difficulty via the LWMA algorithm in `difficulty/`. The HF2 gate is a Zano-ism -- Lethean mainnet uses 120s blocks between HF2 (height 10,080) and HF6 (height 999,999,999). The fix changes the gate constant and adds a parallel `NextPoSDifficulty` method with identical logic but using the PoS target constants.
**Tech Stack:** Go 1.26, go-store (SQLite), go test -race
---
## File Map
### Modified files
| File | What changes |
|------|-------------|
| `chain/difficulty.go` | Change HF2 gate to HF6. Add `NextPoSDifficulty` method. Add comment explaining the HF2-to-HF6 correction. |
| `chain/difficulty_test.go` | Rename `preHF2Forks` to `preHF6Forks`. Add HF6 boundary tests for both PoW and PoS (Good/Bad/Ugly). |
### Unchanged files (reference only)
| File | Role |
|------|------|
| `config/hardfork.go` | Defines `HF6` constant and `MainnetForks`/`TestnetForks` schedules. No changes needed. |
| `config/config.go` | Defines `DifficultyPowTargetHF6`, `DifficultyPosTargetHF6` constants. No changes needed. |
| `difficulty/difficulty.go` | Pure LWMA algorithm -- takes target as parameter. No changes needed. |
---
## Task 1: Fix the HF2-to-HF6 gate and add PoS difficulty
**Package:** `chain/`
**Why:** The current code gates the 240s PoW target on HF2 (block 10,080), but Lethean mainnet uses 120s blocks until HF6 (999,999,999). This means the Go node would compute incorrect difficulty for every block between 10,081 and the future HF6 activation. Additionally, there is no PoS difficulty function -- PoS blocks also need the 120s-to-240s switch at HF6.
### Step 1.1 -- Write tests for the HF6 difficulty boundary
- [ ] Edit `/home/claude/Code/core/go-blockchain/chain/difficulty_test.go`
Replace the entire file with:
```go
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package chain
import (
"testing"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
store "dappco.re/go/core/store"
"github.com/stretchr/testify/require"
)
// preHF6Forks is a fork schedule where HF6 never activates,
// so both PoW and PoS targets stay at 120s.
var preHF6Forks = []config.HardFork{
{Version: config.HF0Initial, Height: 0},
}
// hf6ActiveForks is a fork schedule where HF6 activates at height 100,
// switching both PoW and PoS targets to 240s from block 101 onwards.
var hf6ActiveForks = []config.HardFork{
{Version: config.HF0Initial, Height: 0},
{Version: config.HF1, Height: 0},
{Version: config.HF2, Height: 0},
{Version: config.HF3, Height: 0},
{Version: config.HF4Zarcanum, Height: 0},
{Version: config.HF5, Height: 0},
{Version: config.HF6, Height: 100},
}
// storeBlocks inserts genesis + n blocks with constant intervals and difficulty.
func storeBlocks(t *testing.T, c *Chain, count int, interval uint64, baseDiff uint64) {
t.Helper()
for i := uint64(0); i < uint64(count); i++ {
err := c.PutBlock(&types.Block{}, &BlockMeta{
Hash: types.Hash{byte(i + 1)},
Height: i,
Timestamp: i * interval,
Difficulty: baseDiff,
CumulativeDiff: baseDiff * (i + 1),
})
require.NoError(t, err)
}
}
func TestNextDifficulty_Genesis(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
diff, err := c.NextDifficulty(0, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(1), diff)
}
func TestNextDifficulty_FewBlocks(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
// Store genesis + 4 blocks with constant 120s intervals and difficulty 1000.
// Genesis at height 0 is excluded from the LWMA window.
storeBlocks(t, c, 5, 120, 1000)
// Next difficulty for height 5 uses blocks 1-4 (n=3 intervals).
// LWMA formula with constant D and T gives D/n = 1000/3 = 333.
diff, err := c.NextDifficulty(5, preHF6Forks)
require.NoError(t, err)
require.Greater(t, diff, uint64(0))
expected := uint64(333)
require.Equal(t, expected, diff)
}
func TestNextDifficulty_EmptyChain(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
// Height 1 with no blocks stored -- should return starter difficulty.
diff, err := c.NextDifficulty(1, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(1), diff)
}
// --- HF6 boundary tests ---
func TestNextDifficulty_HF6Boundary_Good(t *testing.T) {
// Verify that blocks at height <= 100 use the 120s target and blocks
// at height > 100 use the 240s target, given hf6ActiveForks.
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 105, 120, 1000)
// Height 100 -- HF6 activates at heights > 100, so this is pre-HF6.
diffPre, err := c.NextDifficulty(100, hf6ActiveForks)
require.NoError(t, err)
// Height 101 -- HF6 is active (height > 100), target becomes 240s.
diffPost, err := c.NextDifficulty(101, hf6ActiveForks)
require.NoError(t, err)
// With 120s actual intervals and a 240s target, LWMA should produce
// lower difficulty than with a 120s target. The post-HF6 difficulty
// should differ from the pre-HF6 difficulty because the target doubled.
require.NotEqual(t, diffPre, diffPost,
"difficulty should change across HF6 boundary (120s vs 240s target)")
}
func TestNextDifficulty_HF6Boundary_Bad(t *testing.T) {
// HF6 at height 999,999,999 (mainnet default) -- should never activate
// for realistic heights, so the target stays at 120s.
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 105, 120, 1000)
forks := config.MainnetForks
diff100, err := c.NextDifficulty(100, forks)
require.NoError(t, err)
diff101, err := c.NextDifficulty(101, forks)
require.NoError(t, err)
// Both should use the same 120s target -- no HF6 in sight.
require.Equal(t, diff100, diff101,
"difficulty should be identical when HF6 is far in the future")
}
func TestNextDifficulty_HF6Boundary_Ugly(t *testing.T) {
// HF6 at height 0 (active from genesis) -- the 240s target should
// apply from the very first difficulty calculation.
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 5, 240, 1000)
genesisHF6 := []config.HardFork{
{Version: config.HF0Initial, Height: 0},
{Version: config.HF6, Height: 0},
}
diff, err := c.NextDifficulty(4, genesisHF6)
require.NoError(t, err)
require.Greater(t, diff, uint64(0))
}
// --- PoS difficulty tests ---
func TestNextPoSDifficulty_Good(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 5, 120, 1000)
// Pre-HF6: PoS target should be 120s (same as PoW).
diff, err := c.NextPoSDifficulty(5, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(333), diff)
}
func TestNextPoSDifficulty_HF6Boundary_Good(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
storeBlocks(t, c, 105, 120, 1000)
// Height 100 -- pre-HF6.
diffPre, err := c.NextPoSDifficulty(100, hf6ActiveForks)
require.NoError(t, err)
// Height 101 -- post-HF6, target becomes 240s.
diffPost, err := c.NextPoSDifficulty(101, hf6ActiveForks)
require.NoError(t, err)
require.NotEqual(t, diffPre, diffPost,
"PoS difficulty should change across HF6 boundary")
}
func TestNextPoSDifficulty_Genesis(t *testing.T) {
s, err := store.New(":memory:")
require.NoError(t, err)
defer s.Close()
c := New(s)
diff, err := c.NextPoSDifficulty(0, preHF6Forks)
require.NoError(t, err)
require.Equal(t, uint64(1), diff)
}
```
### Step 1.2 -- Run tests, verify FAIL
```bash
cd /home/claude/Code/core/go-blockchain && go test -race -run "TestNext.*Difficulty" ./chain/...
```
**Expected:** Compilation error -- `NextPoSDifficulty` does not exist. The renamed `preHF6Forks` replaces `preHF2Forks`. The `hf6ActiveForks` and `storeBlocks` helper are new.
### Step 1.3 -- Implement the fix
- [ ] Edit `/home/claude/Code/core/go-blockchain/chain/difficulty.go`
Replace the entire file with:
```go
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package chain
import (
"math/big"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/difficulty"
)
// nextDifficultyWith computes the expected difficulty for the block at the
// given height using the LWMA algorithm, parameterised by pre/post-HF6 targets.
//
// The genesis block (height 0) is excluded from the difficulty window,
// matching the C++ daemon's load_targetdata_cache which skips index 0.
//
// The target block time depends on the hardfork schedule:
// - Pre-HF6: baseTarget (120s for both PoW and PoS on Lethean)
// - Post-HF6: hf6Target (240s -- halves block rate, halves emission)
//
// NOTE: This was originally gated on HF2, matching the Zano upstream where
// HF2 coincides with the difficulty target change. Lethean mainnet keeps 120s
// blocks between HF2 (height 10,080) and HF6 (height 999,999,999), so the
// gate was corrected to HF6 in March 2026.
func (c *Chain) nextDifficultyWith(height uint64, forks []config.HardFork, baseTarget, hf6Target uint64) (uint64, error) {
if height == 0 {
return 1, nil
}
// LWMA needs N+1 entries (N solve-time intervals).
// Start from height 1 -- genesis is excluded from the difficulty window.
maxLookback := difficulty.LWMAWindow + 1
lookback := min(height, maxLookback) // height excludes genesis since we start from 1
// Start from max(1, height - lookback) to exclude genesis.
startHeight := height - lookback
if startHeight == 0 {
startHeight = 1
lookback = height - 1
}
if lookback == 0 {
return 1, nil
}
count := int(lookback)
timestamps := make([]uint64, count)
cumulDiffs := make([]*big.Int, count)
for i := range count {
meta, err := c.getBlockMeta(startHeight + uint64(i))
if err != nil {
// Fewer blocks than expected -- use what we have.
timestamps = timestamps[:i]
cumulDiffs = cumulDiffs[:i]
break
}
timestamps[i] = meta.Timestamp
cumulDiffs[i] = new(big.Int).SetUint64(meta.CumulativeDiff)
}
// Determine the target block time based on hardfork status.
// HF6 doubles the target from 120s to 240s (corrected from HF2 gate).
target := baseTarget
if config.IsHardForkActive(forks, config.HF6, height) {
target = hf6Target
}
result := difficulty.NextDifficulty(timestamps, cumulDiffs, target)
return result.Uint64(), nil
}
// 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.
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.
func (c *Chain) NextPoSDifficulty(height uint64, forks []config.HardFork) (uint64, error) {
return c.nextDifficultyWith(height, forks, config.DifficultyPosTarget, config.DifficultyPosTargetHF6)
}
```
### Step 1.4 -- Run tests, verify PASS
```bash
cd /home/claude/Code/core/go-blockchain && go test -race -run "TestNext.*Difficulty" ./chain/...
```
**Expected:**
```
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`.
### Step 1.5 -- Run full test suite and vet
```bash
cd /home/claude/Code/core/go-blockchain && go test -race ./... && go vet ./... && go mod tidy
```
**Expected:** All tests pass, no vet warnings, no module changes.
### Step 1.6 -- Commit
```bash
cd /home/claude/Code/core/go-blockchain && git add chain/difficulty.go chain/difficulty_test.go && git commit -m "fix(chain): gate difficulty target switch on HF6, not HF2
The 240s PoW target was incorrectly gated on HF2 (block 10,080), matching
the Zano upstream where HF2 coincides with the difficulty target change.
Lethean mainnet uses 120s blocks between HF2 and HF6 (999,999,999), so
the gate is corrected to HF6.
Also adds NextPoSDifficulty with the same HF6 gate using the PoS target
constants (DifficultyPosTarget / DifficultyPosTargetHF6).
Both public methods delegate to a shared nextDifficultyWith helper to
avoid duplicating the LWMA window logic.
Co-Authored-By: Charon <charon@lethean.io>"
```

View file

@ -0,0 +1,249 @@
# HF1/HF2 Transaction Type Support
**Date:** 2026-03-16
**Author:** Charon
**Package:** `dappco.re/go/core/blockchain`
**Status:** Approved
## Context
Mainnet hardfork 1 activates at block 10,080. The Go node currently only handles genesis, to_key, and ZC input types, and bare (to_key target) and Zarcanum output types. After HF1, blocks may contain HTLC and multisig transactions. The miner tx major version also changes from 0 to 1. Without this work, the Go node will fail to deserialise blocks past HF1.
HF2 activates at the same height (10,080) and adjusts block time parameters. This is handled by the difficulty package via config constants — no new types needed.
## Scope
- Add `TxInputHTLC` and `TxInputMultisig` input types to `types/` and `wire/`
- Add `TxOutMultisig` and `TxOutHTLC` output target types to `types/` and `wire/`
- Refactor `TxOutputBare.Target` from concrete `TxOutToKey` to `TxOutTarget` interface
- Update `consensus/` validation to gate HTLC/multisig on HF1
- Update block major version validation for HF1
- Update all call sites that access `TxOutputBare.Target` fields directly
## C++ Reference (currency_basic.h)
### txin_htlc (tag 0x22)
Inherits from `txin_to_key`. Wire order: `hltc_origin` (string) serialised BEFORE parent fields.
**Note:** The C++ field is named `hltc_origin` (transposed letters). The Go field uses `HTLCOrigin` (corrected acronym) since the type is already `TxInputHTLC`.
```
FIELD(hltc_origin) // varint length + bytes
FIELDS(*static_cast<txin_to_key*>(this)) // amount, key_offsets, k_image, etc_details
```
### txin_multisig (tag 0x02)
```
VARINT_FIELD(amount)
FIELD(multisig_out_id) // 32-byte hash
VARINT_FIELD(sigs_count)
FIELD(etc_details) // variant vector (opaque)
```
### txout_multisig (target tag 0x04)
```
VARINT_FIELD(minimum_sigs)
FIELD(keys) // vector of 32-byte public keys
```
### txout_htlc (target tag 0x23)
```
FIELD(htlc_hash) // 32-byte hash
FIELD(flags) // uint8 (bit 0: 0=SHA256, 1=RIPEMD160)
VARINT_FIELD(expiration) // block height
FIELD(pkey_redeem) // 32-byte public key
FIELD(pkey_refund) // 32-byte public key
```
## Design
### types/transaction.go
#### New input types
```go
// TxInputHTLC extends TxInputToKey with an HTLC origin hash.
// Wire order: HTLCOrigin (string) serialised BEFORE parent fields (C++ quirk).
// Carries Amount, KeyOffsets, KeyImage, EtcDetails — same as TxInputToKey.
type TxInputHTLC struct {
HTLCOrigin string // C++ field: hltc_origin (transposed in source)
Amount uint64
KeyOffsets []TxOutRef
KeyImage KeyImage
EtcDetails []byte // opaque variant vector
}
func (t TxInputHTLC) InputType() uint8 { return InputTypeHTLC }
```
```go
// TxInputMultisig spends from a multisig output.
type TxInputMultisig struct {
Amount uint64
MultisigOutID Hash
SigsCount uint64
EtcDetails []byte // opaque variant vector
}
func (t TxInputMultisig) InputType() uint8 { return InputTypeMultisig }
```
#### Output target interface
Replace concrete `TxOutToKey` target with interface:
```go
type TxOutTarget interface {
TargetType() uint8
}
func (t TxOutToKey) TargetType() uint8 { return TargetTypeToKey }
```
New types:
```go
type TxOutMultisig struct {
MinimumSigs uint64
Keys []PublicKey
}
func (t TxOutMultisig) TargetType() uint8 { return TargetTypeMultisig }
```
```go
type TxOutHTLC struct {
HTLCHash Hash
Flags uint8
Expiration uint64
PKRedeem PublicKey
PKRefund PublicKey
}
func (t TxOutHTLC) TargetType() uint8 { return TargetTypeHTLC }
```
#### TxOutputBare change
```go
type TxOutputBare struct {
Amount uint64
Target TxOutTarget // was TxOutToKey, now interface
}
```
### wire/transaction.go
#### Input decoding (decodeInputs)
Add cases:
```
case InputTypeHTLC (0x22):
read hltc_origin as string (varint length + bytes)
read amount (varint), key_offsets, key_image (32 bytes), etc_details (opaque)
case InputTypeMultisig (0x02):
read amount (varint), multisig_out_id (32 bytes), sigs_count (varint), etc_details (opaque)
```
#### Input encoding (encodeInputs)
Add matching cases for `TxInputHTLC` and `TxInputMultisig`.
#### Output target decoding — BOTH decodeOutputsV1 AND decodeOutputsV2
Add target cases to both v1 and v2 output decoders:
```
case TargetTypeMultisig (0x04):
read minimum_sigs (varint), keys (varint count + 32*N bytes)
case TargetTypeHTLC (0x23):
read htlc_hash (32 bytes), flags (uint8), expiration (varint),
pkey_redeem (32 bytes), pkey_refund (32 bytes)
```
The v2 decoder (`decodeOutputsV2`) also handles `OutputTypeBare` with an inner target tag, so it needs the same target switch updates.
#### Output target encoding — BOTH encodeOutputsV1 AND encodeOutputsV2
Match on `TxOutTarget` interface type, encode accordingly. Both v1 and v2 encoders must handle all three target types.
### consensus/
#### tx.go — Function signature changes
`checkInputTypes` currently receives `hf4Active bool`. Change to receive `forks []config.HardFork` and `height uint64` (or pre-computed `hf1Active` and `hf4Active` bools from the parent `ValidateTransaction`). Same for `checkOutputs`.
#### tx.go — checkInputTypes
Accept `TxInputHTLC` and `TxInputMultisig` when `IsHardForkActive(forks, HF1, height)`. Reject pre-HF1.
#### tx.go — checkOutputs
Accept `TxOutMultisig` and `TxOutHTLC` targets when HF1 active. Reject pre-HF1. Must type-assert `TxOutputBare.Target` to check target types.
#### tx.go — checkKeyImages
Add `TxInputHTLC` to the key image uniqueness check. HTLC inputs carry a `KeyImage` field that must be checked for double-spend prevention, same as `TxInputToKey`.
#### fee.go — sumInputs
Add `TxInputHTLC` and `TxInputMultisig` to the input sum. Both carry `Amount` fields needed for fee calculation and overflow checks. Without this, transactions with HTLC/multisig inputs would appear to have zero input value.
#### block.go — ValidateBlock
Add block major version check: after HF1 height, `blk.MajorVersion` must be >= `HF1BlockMajorVersion` (1). Before HF1, must be 0. This goes in `ValidateBlock` (the block-level entry point), not in `ValidateMinerTx`.
#### block.go — ValidateBlockReward
Update output sum to handle all `TxOutTarget` types via type assertion. The `Amount` field is on `TxOutputBare` (the outer struct), so the sum logic doesn't change for different targets — but the type assertion for accessing the output is needed after the interface refactor.
#### verify.go — verifyV1Signatures
Count `TxInputHTLC` inputs alongside `TxInputToKey` when matching signatures. HTLC inputs use the same NLSAG ring signature scheme. The signature verification loop must handle both types.
### Breaking change: TxOutTarget interface
`TxOutputBare.Target` changes from `TxOutToKey` to `TxOutTarget` interface. All direct field access (`out.Target.Key`) must become type assertions.
**Complete list of affected call sites:**
| File | Line | Current access | Fix |
|------|------|---------------|-----|
| `consensus/block.go` | ValidateBlockReward output sum | `bare.Target` | Type assert to `TxOutToKey` |
| `consensus/verify.go` | Ring output key extraction | `out.Target.Key` | Type assert |
| `wire/transaction.go` | encodeOutputsV1, encodeOutputsV2 | `v.Target.Key`, `v.Target.MixAttr` | Switch on `TargetType()` |
| `chain/ring.go:38` | `out.Target.Key` | Ring output lookup | Type assert |
| `chain/sync.go:280` | Type switch on `TxOutputBare` | Output processing | Type assert target |
| `wallet/scanner.go:67` | `bare.Target.Key` | Output scanning | Type assert |
| `wallet/builder.go:217` | `Target: types.TxOutToKey{...}` | Output construction | No change (constructs TxOutToKey, which satisfies TxOutTarget) |
| `tui/explorer_model.go:328` | `v.Target.Key[:4]` | Display | Type assert |
| All `*_test.go` files | Various | Test assertions | Type assert where accessing Target fields |
### chain/ring.go — GetRingOutputs
Must handle the case where a ring references an output with a multisig or HTLC target. For `TxOutToKey` targets, return the key as before. For `TxOutMultisig`, the relevant key depends on the spending context (not needed for basic sync). For `TxOutHTLC`, return either `PKRedeem` or `PKRefund` depending on whether the HTLC has expired.
## Testing
- Wire round-trip tests: construct HTLC/multisig inputs and outputs, encode, decode, verify equality
- Testnet block parsing: testnet has HF1 at height 0, so all blocks may contain these types
- Consensus gate tests: verify HTLC/multisig rejected pre-HF1, accepted post-HF1
- Key image uniqueness tests: verify HTLC inputs checked for double-spend
- Fee calculation tests: verify sumInputs includes HTLC and multisig amounts
- Signature verification tests: verify verifyV1Signatures handles mixed TxInputToKey + TxInputHTLC
- Breaking change verification: all existing tests must pass after Target interface refactor
- Integration test: sync Go node past HF1 on testnet
## Out of scope
- HTLC redemption/refund logic (wallet layer, not consensus)
- Multisig signing coordination (wallet layer)
- HF3-HF6 changes (separate designs)
- Service attachment parsing (stays opaque)

View file

@ -0,0 +1,69 @@
# HF3 Block Version 2 Support
**Date:** 2026-03-16
**Author:** Charon
**Package:** `dappco.re/go/core/blockchain`
**Status:** Approved
## Context
HF3 increments the block major version from 1 to 2 (`HF3_BLOCK_MAJOR_VERSION`). This is a preparatory hardfork for Zarcanum (HF4). No new transaction types, no new validation rules — purely a version gate.
On mainnet, HF3 is at height 999,999,999 (future). On testnet, HF3 activates at height 0 (genesis).
## Scope
- Add block major version validation to `consensus/block.go` `ValidateBlock`
- Validate version progression: HF0→0, HF1/HF2→1, HF3→2, HF4+→3
## Design
### consensus/block.go — ValidateBlock
Add a `checkBlockVersion` function called from `ValidateBlock`:
```go
func checkBlockVersion(majorVersion uint8, height uint64, forks []config.HardFork) error {
expected := expectedBlockMajorVersion(height, forks)
if majorVersion != expected {
return fmt.Errorf("%w: got %d, expected %d at height %d",
ErrBlockVersion, majorVersion, expected, height)
}
return nil
}
func expectedBlockMajorVersion(height uint64, forks []config.HardFork) uint8 {
switch {
case config.IsHardForkActive(forks, config.HF4Zarcanum, height):
return config.CurrentBlockMajorVersion // 3
case config.IsHardForkActive(forks, config.HF3, height):
return config.HF3BlockMajorVersion // 2
case config.IsHardForkActive(forks, config.HF1, height):
return config.HF1BlockMajorVersion // 1
default:
return config.BlockMajorVersionInitial // 0
}
}
```
This covers all hardforks in one function. `ValidateBlock` signature needs `forks []config.HardFork` added (it currently receives forks via the caller).
### errors.go
Add `ErrBlockVersion` sentinel error.
### Testing
- Test version 0 valid pre-HF1, rejected post-HF1
- Test version 1 valid post-HF1, rejected pre-HF1 and post-HF3
- Test version 2 valid post-HF3, rejected pre-HF3 and post-HF4
- Test version 3 valid post-HF4
- Test with both mainnet and testnet fork schedules
## Note
This function also satisfies HF1's block version requirement (issue #8 from the HF1 spec review). Implementing this as part of HF3 means the HF1 plan doesn't need a separate version check — this single function handles all hardforks.
## Out of scope
- Block minor version validation (not consensus-critical in current chain)

View file

@ -0,0 +1,155 @@
# HF5 Confidential Assets Support
**Date:** 2026-03-16
**Author:** Charon
**Package:** `dappco.re/go/core/blockchain`
**Status:** Draft
**Depends on:** HF1 (types refactor), HF3 (block version), HF4 (Zarcanum — already implemented)
## Context
HF5 introduces confidential assets — the ability to deploy, emit, update, and burn custom asset types on the Lethean chain. This is the Zano asset system: every output has a `blinded_asset_id` that proves (via BGE surjection proofs) it corresponds to a legitimate input asset without revealing which one.
On mainnet, HF5 is at height 999,999,999 (future). On testnet, HF5 activates at height 200.
**What's already implemented:**
- BGE surjection proof verification (`crypto.VerifyBGE`) — crypto bridge done
- BGE proof parsing (`readBGEProof`, `readZCAssetSurjectionProof`) — wire done
- `verifyBGEProofs` in consensus/verify.go — verification logic done
- Transaction version 3 wire format with `hardfork_id` field — wire done
- `VersionPostHF5` constant and `decodePrefixV2` hardfork_id handling — done
**What's NOT implemented:**
- Asset operation types in extra/attachment fields
- Asset descriptor structures
- Consensus validation for asset operations
- Pre-hardfork transaction freeze (60 blocks before HF5 activation)
- Minimum build version enforcement
## Scope
### Phase A: Asset descriptor types (types/)
New types for the `asset_descriptor_operation` extra variant:
```go
// AssetDescriptorBase holds the core asset metadata.
type AssetDescriptorBase struct {
Ticker string // max 6 chars
FullName string // max 64 chars
TotalMaxSupply uint64 // maximum supply cap
CurrentSupply uint64 // current circulating supply
DecimalPoint uint8 // display precision
MetaInfo string // arbitrary metadata (JSON)
OwnerKey PublicKey // asset owner's public key
// etc: reserved variant vector for future fields
Etc []byte // opaque
}
// AssetDescriptorOperation represents a deploy/emit/update/burn operation.
type AssetDescriptorOperation struct {
Version uint8 // currently 0 or 1
OperationType uint8 // ASSET_DESCRIPTOR_OPERATION_REGISTER, _EMIT, _UPDATE, _BURN, _PUBLIC_BURN
Descriptor *AssetDescriptorBase // present for register and update
AssetID Hash // target asset ID (absent for register)
AmountToEmit uint64 // for emit operations
AmountToBurn uint64 // for burn operations
Etc []byte // opaque
}
```
Operation type constants:
```go
const (
AssetOpRegister uint8 = 0 // deploy new asset
AssetOpEmit uint8 = 1 // emit additional supply
AssetOpUpdate uint8 = 2 // update metadata
AssetOpBurn uint8 = 3 // burn supply (with proof)
AssetOpPublicBurn uint8 = 4 // burn supply (public amount)
)
```
### Phase B: Wire encoding for asset operations (wire/)
The `asset_descriptor_operation` appears as a variant element in the tx extra field (tag 40 in the C++ SET_VARIANT_TAGS).
Add to `readVariantElementData`:
```
case tagAssetDescriptorOperation (40):
read version transition header
read operation_type (uint8)
read opt_asset_id (optional hash)
read opt_descriptor (optional AssetDescriptorBase)
read amount_to_emit/burn (varint)
read etc (opaque vector)
```
This is stored as raw bytes in the extra field (same opaque pattern as everything else), but we need the wire reader to not choke on tag 40 during deserialization.
### Phase C: Asset operation proof types (wire/)
New proof variant tags for HF5:
```
tagAssetOperationProof = 49 // asset_operation_proof
tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof
tagAssetOperationOwnershipETH = 51 // asset_operation_ownership_proof_eth
```
Each needs a reader in `readVariantElementData`. The proof structures contain crypto elements (Schnorr signatures, public keys) that are fixed-size.
### Phase D: Consensus validation (consensus/)
**Transaction version enforcement:**
- After HF5: transaction version must be 3 (not 2)
- `hardfork_id` field must be present and match current hardfork
**Pre-hardfork freeze:**
- 60 blocks before HF5 activation, reject non-coinbase transactions
- `config.PreHardforkTxFreezePeriod = 60` already defined
**Asset operation validation:**
- Register: descriptor must be valid (ticker length, supply caps, owner key non-zero)
- Emit: asset_id must exist, caller must prove ownership
- Update: asset_id must exist, caller must prove ownership
- Burn: amount must not exceed current supply
**Minimum build version:**
- C++ enforces `MINIMUM_REQUIRED_BUILD_VERSION = 601` for mainnet, 2 for testnet
- Go equivalent: reject connections from peers with build version below threshold
### Phase E: Asset state tracking (chain/)
Need to track:
- Asset registry: asset_id → AssetDescriptorBase
- Current supply per asset
- Asset ownership proofs
This requires new storage groups in `chain/store.go`.
## What can be deferred
- **Full asset operation validation** — complex, needs ownership proof verification. Can accept blocks containing asset operations structurally (wire parsing) without deep validation initially, then add validation incrementally.
- **Asset state tracking** — needed for wallet/explorer, not strictly for block sync if we trust the C++ daemon's validation.
- **Wallet asset support** — separate design.
## Recommended approach
**Minimum viable HF5:** Wire parsing only. Add tag 40 and the asset proof tags to `readVariantElementData` so the Go node can deserialise HF5 blocks without crashing. Store asset operations as opaque bytes in the extra field (existing pattern). Gate transaction version 3 on HF5.
This follows the same pattern used for extra, attachment, etc_details — opaque bytes for bit-identical round-tripping. Deep validation can layer on top.
## Testing
- Wire round-trip tests with constructed v3 transactions containing asset operations
- Testnet block parsing past height 200 (HF5 activation)
- Version enforcement tests (reject v2 after HF5, accept v3)
- Pre-hardfork freeze tests (reject non-coinbase 60 blocks before activation)
## Out of scope
- Wallet asset management (deploy/emit/burn CLI)
- Asset explorer UI
- Asset whitelist management
- Cross-asset atomic swaps
- HF6 block time halving (separate spec)

View file

@ -0,0 +1,63 @@
# HF6 Block Time Halving
**Date:** 2026-03-16
**Author:** Charon
**Package:** `dappco.re/go/core/blockchain`
**Status:** Draft
**Depends on:** HF5 (confidential assets)
## Context
HF6 doubles the PoW and PoS block targets from 120s to 240s, effectively halving the emission rate without changing the per-block reward. Blocks per day drop from ~1440 to ~720.
On both mainnet and testnet, HF6 is at height 999,999,999 (future/reserved).
**What's already implemented:**
- `DifficultyPowTargetHF6 = 240` and `DifficultyPosTargetHF6 = 240` constants in config
- `DifficultyTotalTargetHF6` computed constant
- `chain/difficulty.go` already switches target based on HF2 — same pattern extends to HF6
**What's NOT implemented:**
- The difficulty switch in `chain/difficulty.go` gates on HF2 but uses the HF6 constants. This is technically correct for the Zano chain where HF2 and the difficulty change are the same thing, but for Lethean the naming is misleading.
- Minimum build version enforcement for HF6
## Scope
This is a ~10 line change.
### chain/difficulty.go
Currently:
```go
target := config.DifficultyPowTarget
if config.IsHardForkActive(forks, config.HF2, height) {
target = config.DifficultyPowTargetHF6
}
```
After HF6 support:
```go
target := config.DifficultyPowTarget // 120s
if config.IsHardForkActive(forks, config.HF6, height) {
target = config.DifficultyPowTargetHF6 // 240s
}
```
Wait — looking at this again, the current code gates the 240s target on HF2 (block 10,080), not HF6 (999,999,999). This means blocks after HF2 are already using the 240s target. Need to check whether this is intentional for Lethean or a bug from the Zano port.
**TODO:** Verify with the C++ daemon what target time blocks after height 10,080 actually use. If Lethean mainnet uses 120s until HF6, then the current code is wrong (should gate on HF6 not HF2). If Lethean follows Zano's schedule where HF2 = difficulty change, then it's correct and HF6 is a no-op.
### Consensus timestamp validation
The `BlockFutureTimeLimit` and `PosBlockFutureTimeLimit` may need adjustment for HF6 if the block time changes. Currently 2 hours for PoW and 20 minutes for PoS — these are reasonable for both 120s and 240s targets.
### Testing
- Difficulty calculation with 240s target
- Verify existing difficulty tests still pass
- Integration test: compute difficulty across HF6 boundary on testnet
## Out of scope
- PoS target adjustments (same 240s, already in config)
- Emission schedule calculations (per-block reward stays the same)

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
}

66
go.mod
View file

@ -1,30 +1,36 @@
module forge.lthn.ai/core/go-blockchain
module dappco.re/go/core/blockchain
go 1.26.0
require (
forge.lthn.ai/core/cli v0.1.0
forge.lthn.ai/core/go-p2p v0.0.0-00010101000000-000000000000
forge.lthn.ai/core/go-process v0.1.2
forge.lthn.ai/core/go-store v0.1.3
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
golang.org/x/crypto v0.48.0
golang.org/x/crypto v0.49.0
)
require (
forge.lthn.ai/core/go v0.1.0 // indirect
forge.lthn.ai/core/go-crypt v0.1.0 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
forge.lthn.ai/core/go v0.3.1 // 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.2 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // 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
@ -32,11 +38,10 @@ require (
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
@ -46,25 +51,26 @@ 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-20260218203240-3dfff04db8fa // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // 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
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.68.0 // indirect
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.1 // indirect
modernc.org/sqlite v1.47.0 // indirect
)
replace forge.lthn.ai/core/cli => /Users/snider/Code/core/cli
replace forge.lthn.ai/core/go => /Users/snider/Code/host-uk/core
replace forge.lthn.ai/core/go-crypt => /Users/snider/Code/core/go-crypt
replace forge.lthn.ai/core/go-p2p => /Users/snider/Code/core/go-p2p
replace forge.lthn.ai/core/go-process => /Users/snider/Code/core/go-process
replace forge.lthn.ai/core/go-store => /Users/snider/Code/core/go-store
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
)

111
go.sum
View file

@ -1,36 +1,52 @@
forge.lthn.ai/core/go-process v0.1.2 h1:0fdLJq/DPssilN9E5yude/xHNfZRKHghIjo++b5aXgc=
forge.lthn.ai/core/go-process v0.1.2/go.mod h1:9oxVALrZaZCqFe8YDdheIS5bRUV1SBz4tVW/MflAtxM=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
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-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-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=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@ -55,9 +71,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@ -85,25 +100,24 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -111,19 +125,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@ -132,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.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
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,11 +15,13 @@ import (
"sync/atomic"
"time"
"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"
coreerr "dappco.re/go/core/log"
"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.
@ -139,18 +141,18 @@ func (m *Miner) Start(ctx context.Context) error {
// Parse difficulty.
diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64)
if err != nil {
return fmt.Errorf("mining: invalid difficulty %q: %w", tmpl.Difficulty, err)
return coreerr.E("Miner.Start", fmt.Sprintf("mining: invalid difficulty %q", tmpl.Difficulty), err)
}
// Decode the block template blob.
blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob)
if err != nil {
return fmt.Errorf("mining: invalid template blob hex: %w", err)
return coreerr.E("Miner.Start", "mining: invalid template blob hex", err)
}
dec := wire.NewDecoder(bytes.NewReader(blobBytes))
block := wire.DecodeBlock(dec)
if dec.Err() != nil {
return fmt.Errorf("mining: decode template: %w", dec.Err())
return coreerr.E("Miner.Start", "mining: decode template", dec.Err())
}
// Update stats.
@ -202,7 +204,7 @@ func (m *Miner) mine(ctx context.Context, block *types.Block, headerHash [32]byt
powHash, err := crypto.RandomXHash(RandomXKey, input[:])
if err != nil {
return fmt.Errorf("mining: RandomX hash: %w", err)
return coreerr.E("Miner.mine", "mining: RandomX hash", err)
}
m.hashCount.Add(1)
@ -215,12 +217,12 @@ func (m *Miner) mine(ctx context.Context, block *types.Block, headerHash [32]byt
enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, block)
if enc.Err() != nil {
return fmt.Errorf("mining: encode solution: %w", enc.Err())
return coreerr.E("Miner.mine", "mining: encode solution", enc.Err())
}
hexBlob := hex.EncodeToString(buf.Bytes())
if err := m.provider.SubmitBlock(hexBlob); err != nil {
return fmt.Errorf("mining: submit block: %w", err)
return coreerr.E("Miner.mine", "mining: submit block", err)
}
m.blocksFound.Add(1)

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.
@ -26,8 +26,8 @@ func (d *CoreSyncData) MarshalSection() levin.Section {
"current_height": levin.Uint64Val(d.CurrentHeight),
"top_id": levin.StringVal(d.TopID[:]),
"last_checkpoint_height": levin.Uint64Val(d.LastCheckpointHeight),
"core_time": levin.Uint64Val(d.CoreTime),
"client_version": levin.StringVal([]byte(d.ClientVersion)),
"core_time": levin.Uint64Val(d.CoreTime),
"client_version": levin.StringVal([]byte(d.ClientVersion)),
"non_pruning_mode_enabled": levin.BoolVal(d.NonPruningMode),
}
}

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

@ -5,19 +5,23 @@
package rpc
import "fmt"
import (
"fmt"
coreerr "dappco.re/go/core/log"
)
// GetLastBlockHeader returns the header of the most recent block.
func (c *Client) GetLastBlockHeader() (*BlockHeader, error) {
var resp struct {
BlockHeader BlockHeader `json:"block_header"`
Status string `json:"status"`
Status string `json:"status"`
}
if err := c.call("getlastblockheader", struct{}{}, &resp); err != nil {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getlastblockheader: status %q", resp.Status)
return nil, coreerr.E("Client.GetLastBlockHeader", fmt.Sprintf("getlastblockheader: status %q", resp.Status), nil)
}
return &resp.BlockHeader, nil
}
@ -29,13 +33,13 @@ func (c *Client) GetBlockHeaderByHeight(height uint64) (*BlockHeader, error) {
}{Height: height}
var resp struct {
BlockHeader BlockHeader `json:"block_header"`
Status string `json:"status"`
Status string `json:"status"`
}
if err := c.call("getblockheaderbyheight", params, &resp); err != nil {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getblockheaderbyheight: status %q", resp.Status)
return nil, coreerr.E("Client.GetBlockHeaderByHeight", fmt.Sprintf("getblockheaderbyheight: status %q", resp.Status), nil)
}
return &resp.BlockHeader, nil
}
@ -47,13 +51,13 @@ func (c *Client) GetBlockHeaderByHash(hash string) (*BlockHeader, error) {
}{Hash: hash}
var resp struct {
BlockHeader BlockHeader `json:"block_header"`
Status string `json:"status"`
Status string `json:"status"`
}
if err := c.call("getblockheaderbyhash", params, &resp); err != nil {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getblockheaderbyhash: status %q", resp.Status)
return nil, coreerr.E("Client.GetBlockHeaderByHash", fmt.Sprintf("getblockheaderbyhash: status %q", resp.Status), nil)
}
return &resp.BlockHeader, nil
}
@ -73,7 +77,7 @@ func (c *Client) GetBlocksDetails(heightStart, count uint64) ([]BlockDetails, er
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("get_blocks_details: status %q", resp.Status)
return nil, coreerr.E("Client.GetBlocksDetails", fmt.Sprintf("get_blocks_details: status %q", resp.Status), nil)
}
return resp.Blocks, nil
}

View file

@ -14,6 +14,8 @@ import (
"net/http"
"net/url"
"time"
coreerr "dappco.re/go/core/log"
)
// Client is a Lethean daemon RPC client.
@ -66,10 +68,10 @@ type jsonRPCRequest struct {
}
type jsonRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Result json.RawMessage `json:"result"`
Error *jsonRPCError `json:"error,omitempty"`
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Result json.RawMessage `json:"result"`
Error *jsonRPCError `json:"error,omitempty"`
}
type jsonRPCError struct {
@ -86,27 +88,27 @@ func (c *Client) call(method string, params any, result any) error {
Params: params,
})
if err != nil {
return fmt.Errorf("marshal request: %w", err)
return coreerr.E("Client.call", "marshal request", err)
}
resp, err := c.httpClient.Post(c.url, "application/json", bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("post %s: %w", method, err)
return coreerr.E("Client.call", fmt.Sprintf("post %s", method), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http %d from %s", resp.StatusCode, method)
return coreerr.E("Client.call", fmt.Sprintf("http %d from %s", resp.StatusCode, method), nil)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
return coreerr.E("Client.call", "read response", err)
}
var rpcResp jsonRPCResponse
if err := json.Unmarshal(body, &rpcResp); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
return coreerr.E("Client.call", "unmarshal response", err)
}
if rpcResp.Error != nil {
@ -115,7 +117,7 @@ func (c *Client) call(method string, params any, result any) error {
if result != nil && len(rpcResp.Result) > 0 {
if err := json.Unmarshal(rpcResp.Result, result); err != nil {
return fmt.Errorf("unmarshal result: %w", err)
return coreerr.E("Client.call", "unmarshal result", err)
}
}
return nil
@ -125,28 +127,28 @@ func (c *Client) call(method string, params any, result any) error {
func (c *Client) legacyCall(path string, params any, result any) error {
reqBody, err := json.Marshal(params)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
return coreerr.E("Client.legacyCall", "marshal request", err)
}
url := c.baseURL + path
resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("post %s: %w", path, err)
return coreerr.E("Client.legacyCall", fmt.Sprintf("post %s", path), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http %d from %s", resp.StatusCode, path)
return coreerr.E("Client.legacyCall", fmt.Sprintf("http %d from %s", resp.StatusCode, path), nil)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
return coreerr.E("Client.legacyCall", "read response", err)
}
if result != nil {
if err := json.Unmarshal(body, result); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
return coreerr.E("Client.legacyCall", "unmarshal response", err)
}
}
return nil

View file

@ -5,7 +5,11 @@
package rpc
import "fmt"
import (
"fmt"
coreerr "dappco.re/go/core/log"
)
// GetInfo returns the daemon status.
// Uses flags=0 for the cheapest query (no expensive calculations).
@ -21,7 +25,7 @@ func (c *Client) GetInfo() (*DaemonInfo, error) {
return nil, err
}
if resp.Status != "OK" {
return nil, fmt.Errorf("getinfo: status %q", resp.Status)
return nil, coreerr.E("Client.GetInfo", fmt.Sprintf("getinfo: status %q", resp.Status), nil)
}
return &resp.DaemonInfo, nil
}
@ -37,7 +41,7 @@ func (c *Client) GetHeight() (uint64, error) {
return 0, err
}
if resp.Status != "OK" {
return 0, fmt.Errorf("getheight: status %q", resp.Status)
return 0, coreerr.E("Client.GetHeight", fmt.Sprintf("getheight: status %q", resp.Status), nil)
}
return resp.Height, nil
}
@ -52,7 +56,7 @@ func (c *Client) GetBlockCount() (uint64, error) {
return 0, err
}
if resp.Status != "OK" {
return 0, fmt.Errorf("getblockcount: status %q", resp.Status)
return 0, coreerr.E("Client.GetBlockCount", fmt.Sprintf("getblockcount: status %q", resp.Status), nil)
}
return resp.Count, nil
}

Some files were not shown because too many files have changed in this diff Show more