Compare commits
88 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76488e0beb | ||
|
|
caf83faf39 | ||
|
|
123047bebd | ||
|
|
330ee2a146 | ||
|
|
d5070cce15 | ||
|
|
9c5b179375 | ||
|
|
602c886400 | ||
|
|
474fa2f07d | ||
|
|
99720fff5e | ||
|
|
b7428496bd | ||
|
|
bdbefa7d4a | ||
|
|
bb941ebcc5 | ||
|
|
95edac1d15 | ||
|
|
8802b94ee5 | ||
|
|
0512861330 | ||
|
|
51d5ce9f14 | ||
|
|
bc3e208691 | ||
|
|
219aeae540 | ||
|
|
2e92407233 | ||
|
|
0993b081c7 | ||
|
|
cb43082d18 | ||
|
|
2bebe323b8 | ||
|
|
0ab8bfbd01 | ||
|
|
b34afa827f | ||
|
|
7e01df15fe | ||
|
|
2f3f46e8c5 | ||
|
|
92628cec35 | ||
|
|
92cb5a8fbb | ||
|
|
e25e3e73e7 | ||
|
|
c787990b9a | ||
|
|
3686a82b33 | ||
|
|
d6f31dbe57 | ||
|
|
41f2d52979 | ||
|
|
050d530b29 | ||
|
|
c1b68523c6 | ||
|
|
be99c5e93a | ||
|
|
d2caf68d94 | ||
|
|
ccdcfbaacf | ||
|
|
f1738527bc | ||
|
|
21c5d49ef9 | ||
|
|
0ba5bbe49c | ||
|
|
01f4e5cd0a | ||
|
|
d3143d3f88 | ||
|
|
f7ee451fc4 | ||
|
|
8e6dc326df | ||
|
|
a2df164822 | ||
|
|
243749a6d8 | ||
| d004158022 | |||
|
|
34128d8e98 | ||
|
|
6370d96c31 | ||
|
|
2b145d6ebf | ||
|
|
abb1e2b748 | ||
|
|
97c5510184 | ||
|
|
772cd1b0fd | ||
|
|
f19054f7b1 | ||
|
|
70fab6f7d0 | ||
|
|
89b0375e18 | ||
|
|
71f0a5c1d5 | ||
|
|
8d41b76db3 | ||
|
|
efbf050c1b | ||
|
|
939ad198fe | ||
|
|
d8e12a1539 | ||
|
|
3e79f34a65 | ||
|
|
9631efa5a8 | ||
|
|
18ceb7fa26 | ||
|
|
d7917234ed | ||
|
|
6a1f516f5f | ||
|
|
b1a0e9637b | ||
|
|
f88d582c64 | ||
|
|
192d681ecd | ||
|
|
ba29b55644 | ||
|
|
830aa6055e | ||
|
|
14a2da9396 | ||
|
|
1ca75f9e3f | ||
|
|
30d174eaac | ||
|
|
cc99c92c42 | ||
|
|
0408d2f3fa | ||
|
|
6eabe2a64d | ||
|
|
023f4b813c | ||
|
|
00e83582b7 | ||
|
|
5a53c719de | ||
|
|
c7c169dd67 | ||
|
|
ef232fcb80 | ||
|
|
a71976f259 | ||
|
|
57b8bbce2d | ||
|
|
dfb8467bb2 | ||
|
|
b4b532ceb9 | ||
|
|
2e81130b66 |
149 changed files with 11624 additions and 1275 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1 +1,5 @@
|
||||||
crypto/build/
|
crypto/build/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.log
|
||||||
|
.core/
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
Go implementation of the Lethean blockchain protocol (CryptoNote/Zano-fork) with a CGo crypto bridge.
|
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)
|
Licence: EUPL-1.2 (every source file carries the copyright header)
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
@ -42,7 +42,7 @@ go test -tags integration ./... # integration tests (need C++ te
|
||||||
- Co-Author trailer: `Co-Authored-By: Charon <charon@lethean.io>`
|
- Co-Author trailer: `Co-Authored-By: Charon <charon@lethean.io>`
|
||||||
- Error strings: `package: description` format (e.g. `types: invalid hex for hash`)
|
- Error strings: `package: description` format (e.g. `types: invalid hex for hash`)
|
||||||
- Error wrapping: `fmt.Errorf("package: description: %w", err)`
|
- 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
|
- No emojis in code or comments
|
||||||
|
|
||||||
## Test Conventions
|
## 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)`.
|
- 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/`).
|
- 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
|
## Docs
|
||||||
|
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Pure Go implementation of the Lethean blockchain protocol. Provides chain configuration, core cryptographic data types, CryptoNote wire serialisation, and LWMA difficulty adjustment for the Lethean CryptoNote/Zano-fork chain. Follows ADR-001: protocol logic in Go, cryptographic primitives deferred to a C++ bridge in later phases. Lineage: CryptoNote to IntenseCoin (2017) to Lethean to Zano rebase.
|
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
|
**Licence**: EUPL-1.2
|
||||||
**Language**: Go 1.25
|
**Language**: Go 1.25
|
||||||
|
|
||||||
|
|
@ -10,10 +10,10 @@ Pure Go implementation of the Lethean blockchain protocol. Provides chain config
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
"forge.lthn.ai/core/go-blockchain/difficulty"
|
"dappco.re/go/core/blockchain/difficulty"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Query the active hardfork version at a given block height
|
// Query the active hardfork version at a given block height
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,9 @@
|
||||||
package chain
|
package chain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"fmt"
|
coreerr "dappco.re/go/core/log"
|
||||||
|
store "dappco.re/go/core/store"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
|
||||||
store "forge.lthn.ai/core/go-store"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chain manages blockchain storage and indexing.
|
// Chain manages blockchain storage and indexing.
|
||||||
|
|
@ -21,28 +19,35 @@ type Chain struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a Chain backed by the given store.
|
// 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 {
|
func New(s *store.Store) *Chain {
|
||||||
return &Chain{store: s}
|
return &Chain{store: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Height returns the number of stored blocks (0 if empty).
|
// Height returns the number of stored blocks (0 if empty).
|
||||||
|
//
|
||||||
|
// h, err := blockchain.Height()
|
||||||
func (c *Chain) Height() (uint64, error) {
|
func (c *Chain) Height() (uint64, error) {
|
||||||
n, err := c.store.Count(groupBlocks)
|
n, err := c.store.Count(groupBlocks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("chain: height: %w", err)
|
return 0, coreerr.E("Chain.Height", "chain: height", err)
|
||||||
}
|
}
|
||||||
return uint64(n), nil
|
return uint64(n), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TopBlock returns the highest stored block and its metadata.
|
// TopBlock returns the highest stored block and its metadata.
|
||||||
// Returns an error if the chain is empty.
|
// Returns an error if the chain is empty.
|
||||||
|
//
|
||||||
|
// blk, meta, err := blockchain.TopBlock()
|
||||||
func (c *Chain) TopBlock() (*types.Block, *BlockMeta, error) {
|
func (c *Chain) TopBlock() (*types.Block, *BlockMeta, error) {
|
||||||
h, err := c.Height()
|
h, err := c.Height()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if h == 0 {
|
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)
|
return c.GetBlockByHeight(h - 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ package chain
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
store "forge.lthn.ai/core/go-store"
|
store "dappco.re/go/core/store"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestChain(t *testing.T) *Chain {
|
func newTestChain(t *testing.T) *Chain {
|
||||||
|
|
@ -219,8 +219,9 @@ func TestChain_GetBlockByHeight_NotFound(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("GetBlockByHeight(99): expected error, got nil")
|
t.Fatal("GetBlockByHeight(99): expected error, got nil")
|
||||||
}
|
}
|
||||||
if got := err.Error(); got != "chain: block 99 not found" {
|
want := "Chain.GetBlockByHeight: chain: block 99 not found"
|
||||||
t.Errorf("error message: got %q, want %q", got, "chain: block 99 not found")
|
if got := err.Error(); got != want {
|
||||||
|
t.Errorf("error message: got %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,31 @@ package chain
|
||||||
import (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/difficulty"
|
"dappco.re/go/core/blockchain/difficulty"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NextDifficulty computes the expected difficulty for the block at the given
|
// nextDifficultyWith computes the expected difficulty for the block at the
|
||||||
// height, using the LWMA algorithm over stored block history.
|
// given height using the LWMA algorithm, parameterised by pre/post-HF6 targets.
|
||||||
//
|
//
|
||||||
// The genesis block (height 0) is excluded from the difficulty window,
|
// The genesis block (height 0) is excluded from the difficulty window,
|
||||||
// matching the C++ daemon's load_targetdata_cache which skips index 0.
|
// matching the C++ daemon's load_targetdata_cache which skips index 0.
|
||||||
//
|
//
|
||||||
// The target block time depends on the hardfork schedule: 120s pre-HF2,
|
// The target block time depends on the hardfork schedule:
|
||||||
// 240s post-HF2 (matching DIFFICULTY_POW_TARGET_HF6 in the C++ source).
|
// - Pre-HF6: baseTarget (120s for both PoW and PoS on Lethean)
|
||||||
func (c *Chain) NextDifficulty(height uint64, forks []config.HardFork) (uint64, error) {
|
// - 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 {
|
if height == 0 {
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LWMA needs N+1 entries (N solve-time intervals).
|
// 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
|
maxLookback := difficulty.LWMAWindow + 1
|
||||||
lookback := min(height, maxLookback) // height excludes genesis since we start from 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 {
|
for i := range count {
|
||||||
meta, err := c.getBlockMeta(startHeight + uint64(i))
|
meta, err := c.getBlockMeta(startHeight + uint64(i))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fewer blocks than expected — use what we have.
|
// Fewer blocks than expected -- use what we have.
|
||||||
timestamps = timestamps[:i]
|
timestamps = timestamps[:i]
|
||||||
cumulDiffs = cumulDiffs[:i]
|
cumulDiffs = cumulDiffs[:i]
|
||||||
break
|
break
|
||||||
|
|
@ -58,12 +64,28 @@ func (c *Chain) NextDifficulty(height uint64, forks []config.HardFork) (uint64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the target block time based on hardfork status.
|
// Determine the target block time based on hardfork status.
|
||||||
// HF2 doubles the target from 120s to 240s.
|
// HF6 doubles the target from 120s to 240s (corrected from HF2 gate).
|
||||||
target := config.DifficultyPowTarget
|
target := baseTarget
|
||||||
if config.IsHardForkActive(forks, config.HF2, height) {
|
if config.IsHardForkActive(forks, config.HF6, height) {
|
||||||
target = config.DifficultyPowTargetHF6
|
target = hf6Target
|
||||||
}
|
}
|
||||||
|
|
||||||
result := difficulty.NextDifficulty(timestamps, cumulDiffs, target)
|
result := difficulty.NextDifficulty(timestamps, cumulDiffs, target)
|
||||||
return result.Uint64(), nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,52 @@ package chain
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
store "forge.lthn.ai/core/go-store"
|
store "dappco.re/go/core/store"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// preHF2Forks is a fork schedule where HF2 never activates,
|
// preHF6Forks is a fork schedule where HF6 never activates,
|
||||||
// so the target stays at 120s.
|
// so both PoW and PoS targets stay at 120s.
|
||||||
var preHF2Forks = []config.HardFork{
|
var preHF6Forks = []config.HardFork{
|
||||||
{Version: config.HF0Initial, Height: 0},
|
{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) {
|
func TestNextDifficulty_Genesis(t *testing.T) {
|
||||||
s, err := store.New(":memory:")
|
s, err := store.New(":memory:")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
c := New(s)
|
c := New(s)
|
||||||
diff, err := c.NextDifficulty(0, preHF2Forks)
|
diff, err := c.NextDifficulty(0, preHF6Forks)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uint64(1), diff)
|
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.
|
// Store genesis + 4 blocks with constant 120s intervals and difficulty 1000.
|
||||||
// Genesis at height 0 is excluded from the LWMA window.
|
// Genesis at height 0 is excluded from the LWMA window.
|
||||||
baseDiff := uint64(1000)
|
storeBlocks(t, c, 5, 120, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next difficulty for height 5 uses blocks 1-4 (n=3 intervals).
|
// 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.
|
// LWMA formula with constant D and T gives D/n = 1000/3 = 333.
|
||||||
diff, err := c.NextDifficulty(5, preHF2Forks)
|
diff, err := c.NextDifficulty(5, preHF6Forks)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Greater(t, diff, uint64(0))
|
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)
|
expected := uint64(333)
|
||||||
require.Equal(t, expected, diff)
|
require.Equal(t, expected, diff)
|
||||||
}
|
}
|
||||||
|
|
@ -71,8 +86,124 @@ func TestNextDifficulty_EmptyChain(t *testing.T) {
|
||||||
|
|
||||||
c := New(s)
|
c := New(s)
|
||||||
|
|
||||||
// Height 1 with no blocks stored — should return starter difficulty.
|
// Height 1 with no blocks stored -- should return starter difficulty.
|
||||||
diff, err := c.NextDifficulty(1, preHF2Forks)
|
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.NoError(t, err)
|
||||||
require.Equal(t, uint64(1), diff)
|
require.Equal(t, uint64(1), diff)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
package chain
|
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
|
// SparseChainHistory builds the exponentially-spaced block hash list used by
|
||||||
// NOTIFY_REQUEST_CHAIN. Matches the C++ get_short_chain_history() algorithm:
|
// NOTIFY_REQUEST_CHAIN. Matches the C++ get_short_chain_history() algorithm:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ package chain
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,32 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
store "forge.lthn.ai/core/go-store"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
|
||||||
|
"dappco.re/go/core/blockchain/types"
|
||||||
|
store "dappco.re/go/core/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarkSpent records a key image as spent at the given block height.
|
// 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 {
|
func (c *Chain) MarkSpent(ki types.KeyImage, height uint64) error {
|
||||||
if err := c.store.Set(groupSpentKeys, ki.String(), strconv.FormatUint(height, 10)); err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSpent checks whether a key image has been spent.
|
// IsSpent checks whether a key image has been spent.
|
||||||
|
//
|
||||||
|
// spent, err := blockchain.IsSpent(keyImage)
|
||||||
func (c *Chain) IsSpent(ki types.KeyImage) (bool, error) {
|
func (c *Chain) IsSpent(ki types.KeyImage) (bool, error) {
|
||||||
_, err := c.store.Get(groupSpentKeys, ki.String())
|
_, err := c.store.Get(groupSpentKeys, ki.String())
|
||||||
if errors.Is(err, store.ErrNotFound) {
|
if errors.Is(err, store.ErrNotFound) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if err != 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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +52,7 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
|
||||||
grp := outputGroup(amount)
|
grp := outputGroup(amount)
|
||||||
count, err := c.store.Count(grp)
|
count, err := c.store.Count(grp)
|
||||||
if err != nil {
|
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)
|
gindex := uint64(count)
|
||||||
|
|
||||||
|
|
@ -56,12 +62,12 @@ func (c *Chain) PutOutput(amount uint64, txID types.Hash, outNo uint32) (uint64,
|
||||||
}
|
}
|
||||||
val, err := json.Marshal(entry)
|
val, err := json.Marshal(entry)
|
||||||
if err != nil {
|
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)
|
key := strconv.FormatUint(gindex, 10)
|
||||||
if err := c.store.Set(grp, key, string(val)); err != nil {
|
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
|
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)
|
val, err := c.store.Get(grp, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, store.ErrNotFound) {
|
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
|
var entry outputEntry
|
||||||
if err := json.Unmarshal([]byte(val), &entry); err != nil {
|
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)
|
hash, err := types.HashFromHex(entry.TxID)
|
||||||
if err != nil {
|
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
|
return hash, entry.OutNo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutputCount returns the number of outputs indexed for the given amount.
|
// 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) {
|
func (c *Chain) OutputCount(amount uint64) (uint64, error) {
|
||||||
n, err := c.store.Count(outputGroup(amount))
|
n, err := c.store.Count(outputGroup(amount))
|
||||||
if err != nil {
|
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
|
return uint64(n), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/p2p"
|
"dappco.re/go/core/blockchain/p2p"
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/rpc"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
levin "forge.lthn.ai/core/go-p2p/node/levin"
|
levin "dappco.re/go/core/p2p/node/levin"
|
||||||
store "forge.lthn.ai/core/go-store"
|
store "dappco.re/go/core/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testnetRPCAddr = "http://localhost:46941"
|
const testnetRPCAddr = "http://localhost:46941"
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,10 @@
|
||||||
package chain
|
package chain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
corelog "dappco.re/go/core/log"
|
||||||
"log"
|
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/p2p"
|
"dappco.re/go/core/blockchain/p2p"
|
||||||
levinpkg "forge.lthn.ai/core/go-p2p/node/levin"
|
levinpkg "dappco.re/go/core/p2p/node/levin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LevinP2PConn adapts a Levin connection to the P2PConnection interface.
|
// 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}
|
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 }
|
func (c *LevinP2PConn) PeerHeight() uint64 { return c.peerHeight }
|
||||||
|
|
||||||
// handleMessage processes non-target messages received while waiting for
|
// 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}
|
resp := p2p.TimedSyncRequest{PayloadData: c.localSync}
|
||||||
payload, err := resp.Encode()
|
payload, err := resp.Encode()
|
||||||
if err != nil {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
// Silently skip other messages (new_block notifications, etc.)
|
// Silently skip other messages (new_block notifications, etc.)
|
||||||
return nil
|
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) {
|
func (c *LevinP2PConn) RequestChain(blockIDs [][]byte) (uint64, [][]byte, error) {
|
||||||
req := p2p.RequestChain{BlockIDs: blockIDs}
|
req := p2p.RequestChain{BlockIDs: blockIDs}
|
||||||
payload, err := req.Encode()
|
payload, err := req.Encode()
|
||||||
if err != nil {
|
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.
|
// Send as notification (expectResponse=false) per CryptoNote protocol.
|
||||||
if err := c.conn.WritePacket(p2p.CommandRequestChain, payload, false); err != nil {
|
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.
|
// Read until we get RESPONSE_CHAIN_ENTRY.
|
||||||
for {
|
for {
|
||||||
hdr, data, err := c.conn.ReadPacket()
|
hdr, data, err := c.conn.ReadPacket()
|
||||||
if err != nil {
|
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 {
|
if hdr.Command == p2p.CommandResponseChain {
|
||||||
var resp p2p.ResponseChainEntry
|
var resp p2p.ResponseChainEntry
|
||||||
if err := resp.Decode(data); err != nil {
|
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
|
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) {
|
func (c *LevinP2PConn) RequestObjects(blockHashes [][]byte) ([]BlockBlobEntry, error) {
|
||||||
req := p2p.RequestGetObjects{Blocks: blockHashes}
|
req := p2p.RequestGetObjects{Blocks: blockHashes}
|
||||||
payload, err := req.Encode()
|
payload, err := req.Encode()
|
||||||
if err != nil {
|
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 {
|
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.
|
// Read until we get RESPONSE_GET_OBJECTS.
|
||||||
for {
|
for {
|
||||||
hdr, data, err := c.conn.ReadPacket()
|
hdr, data, err := c.conn.ReadPacket()
|
||||||
if err != nil {
|
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 {
|
if hdr.Command == p2p.CommandResponseObjects {
|
||||||
var resp p2p.ResponseGetObjects
|
var resp p2p.ResponseGetObjects
|
||||||
if err := resp.Decode(data); err != nil {
|
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))
|
entries := make([]BlockBlobEntry, len(resp.Blocks))
|
||||||
for i, b := range resp.Blocks {
|
for i, b := range resp.Blocks {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
package chain
|
package chain
|
||||||
|
|
||||||
import "forge.lthn.ai/core/go-blockchain/types"
|
import "dappco.re/go/core/blockchain/types"
|
||||||
|
|
||||||
// BlockMeta holds metadata stored alongside each block.
|
// BlockMeta holds metadata stored alongside each block.
|
||||||
type BlockMeta struct {
|
type BlockMeta struct {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ package chain
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
corelog "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// P2PConnection abstracts the P2P communication needed for block sync.
|
// 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()
|
localHeight, err := c.Height()
|
||||||
if err != nil {
|
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()
|
peerHeight := conn.PeerHeight()
|
||||||
|
|
@ -55,7 +56,7 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
|
||||||
// Build sparse chain history.
|
// Build sparse chain history.
|
||||||
history, err := c.SparseChainHistory()
|
history, err := c.SparseChainHistory()
|
||||||
if err != nil {
|
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.
|
// Convert Hash to []byte for P2P.
|
||||||
|
|
@ -69,14 +70,14 @@ func (c *Chain) P2PSync(ctx context.Context, conn P2PConnection, opts SyncOption
|
||||||
// Request chain entry.
|
// Request chain entry.
|
||||||
startHeight, blockIDs, err := conn.RequestChain(historyBytes)
|
startHeight, blockIDs, err := conn.RequestChain(historyBytes)
|
||||||
if err != nil {
|
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 {
|
if len(blockIDs) == 0 {
|
||||||
return nil // nothing to sync
|
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.
|
// The daemon returns the fork-point block as the first entry.
|
||||||
// Skip blocks we already have.
|
// 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)
|
entries, err := conn.RequestObjects(batch)
|
||||||
if err != nil {
|
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)
|
currentHeight := fetchStart + uint64(i)
|
||||||
for j, entry := range entries {
|
for j, entry := range entries {
|
||||||
blockHeight := currentHeight + uint64(j)
|
blockHeight := currentHeight + uint64(j)
|
||||||
if blockHeight > 0 && blockHeight%100 == 0 {
|
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)
|
blockDiff, err := c.NextDifficulty(blockHeight, opts.Forks)
|
||||||
if err != nil {
|
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,
|
if err := c.processBlockBlobs(entry.Block, entry.Txs,
|
||||||
blockHeight, blockDiff, opts); err != nil {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
store "forge.lthn.ai/core/go-store"
|
store "dappco.re/go/core/store"
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,39 +8,74 @@ package chain
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
|
||||||
|
"dappco.re/go/core/blockchain/consensus"
|
||||||
|
"dappco.re/go/core/blockchain/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetRingOutputs fetches the public keys for the given global output indices
|
// GetRingOutputs fetches the public keys for the given global output indices
|
||||||
// at the specified amount. This implements the consensus.RingOutputsFn
|
// at the specified spending height and amount. This implements the
|
||||||
// signature for use during signature verification.
|
// 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))
|
// 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 {
|
for i, gidx := range offsets {
|
||||||
txHash, outNo, err := c.GetOutput(amount, gidx)
|
txHash, outNo, err := c.GetOutput(amount, gidx)
|
||||||
if err != nil {
|
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)
|
tx, _, err := c.GetTransaction(txHash)
|
||||||
if err != nil {
|
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) {
|
if int(outNo) >= len(tx.Vout) {
|
||||||
return nil, fmt.Errorf("ring output %d: tx %s has %d outputs, want index %d",
|
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)
|
||||||
i, txHash, len(tx.Vout), outNo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch out := tx.Vout[outNo].(type) {
|
switch out := tx.Vout[outNo].(type) {
|
||||||
case types.TxOutputBare:
|
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:
|
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,
|
// 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.
|
// consensus.ZCRingOutputsFn signature for post-HF4 CLSAG GGX verification.
|
||||||
//
|
//
|
||||||
// ZC outputs are indexed at amount=0 (confidential amounts).
|
// 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) {
|
func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, error) {
|
||||||
members := make([]consensus.ZCRingMember, len(offsets))
|
members := make([]consensus.ZCRingMember, len(offsets))
|
||||||
for i, gidx := range offsets {
|
for i, gidx := range offsets {
|
||||||
txHash, outNo, err := c.GetOutput(0, gidx)
|
txHash, outNo, err := c.GetOutput(0, gidx)
|
||||||
if err != nil {
|
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)
|
tx, _, err := c.GetTransaction(txHash)
|
||||||
if err != nil {
|
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) {
|
if int(outNo) >= len(tx.Vout) {
|
||||||
return nil, fmt.Errorf("ZC ring output %d: tx %s has %d outputs, want index %d",
|
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)
|
||||||
i, txHash, len(tx.Vout), outNo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch out := tx.Vout[outNo].(type) {
|
switch out := tx.Vout[outNo].(type) {
|
||||||
|
|
@ -74,7 +110,7 @@ func (c *Chain) GetZCRingOutputs(offsets []uint64) ([]consensus.ZCRingMember, er
|
||||||
BlindedAssetID: [32]byte(out.BlindedAssetID),
|
BlindedAssetID: [32]byte(out.BlindedAssetID),
|
||||||
}
|
}
|
||||||
default:
|
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
|
return members, nil
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ package chain
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetRingOutputs_Good(t *testing.T) {
|
func TestGetRingOutputs_Good(t *testing.T) {
|
||||||
|
|
@ -43,7 +43,7 @@ func TestGetRingOutputs_Good(t *testing.T) {
|
||||||
t.Fatalf("gidx: got %d, want 0", gidx)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("GetRingOutputs: %v", err)
|
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) {
|
func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
|
||||||
c := newTestChain(t)
|
c := newTestChain(t)
|
||||||
|
|
||||||
|
|
@ -97,7 +225,7 @@ func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
|
||||||
t.Fatalf("PutOutput(tx2): %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("GetRingOutputs: %v", err)
|
t.Fatalf("GetRingOutputs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +243,7 @@ func TestGetRingOutputs_Good_MultipleOutputs(t *testing.T) {
|
||||||
func TestGetRingOutputs_Bad_OutputNotFound(t *testing.T) {
|
func TestGetRingOutputs_Bad_OutputNotFound(t *testing.T) {
|
||||||
c := newTestChain(t)
|
c := newTestChain(t)
|
||||||
|
|
||||||
_, err := c.GetRingOutputs(1000, []uint64{99})
|
_, err := c.GetRingOutputs(1000, 1000, []uint64{99})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("GetRingOutputs: expected error for missing output, got 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)
|
t.Fatalf("PutOutput: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := c.GetRingOutputs(1000, []uint64{0})
|
_, err := c.GetRingOutputs(1000, 1000, []uint64{0})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("GetRingOutputs: expected error for missing tx, got 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)
|
t.Fatalf("PutOutput: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := c.GetRingOutputs(1000, []uint64{0})
|
_, err := c.GetRingOutputs(1000, 1000, []uint64{0})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("GetRingOutputs: expected error for out-of-range index, got 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) {
|
func TestGetRingOutputs_Good_EmptyOffsets(t *testing.T) {
|
||||||
c := newTestChain(t)
|
c := newTestChain(t)
|
||||||
|
|
||||||
pubs, err := c.GetRingOutputs(1000, []uint64{})
|
pubs, err := c.GetRingOutputs(1000, 1000, []uint64{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetRingOutputs: %v", err)
|
t.Fatalf("GetRingOutputs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
store "forge.lthn.ai/core/go-store"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/types"
|
||||||
|
"dappco.re/go/core/blockchain/wire"
|
||||||
|
store "dappco.re/go/core/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Storage group constants matching the design schema.
|
// Storage group constants matching the design schema.
|
||||||
|
|
@ -39,12 +41,14 @@ type blockRecord struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBlock stores a block and updates the block_index.
|
// 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 {
|
func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
enc := wire.NewEncoder(&buf)
|
enc := wire.NewEncoder(&buf)
|
||||||
wire.EncodeBlock(enc, b)
|
wire.EncodeBlock(enc, b)
|
||||||
if err := enc.Err(); err != nil {
|
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{
|
rec := blockRecord{
|
||||||
|
|
@ -53,46 +57,50 @@ func (c *Chain) PutBlock(b *types.Block, meta *BlockMeta) error {
|
||||||
}
|
}
|
||||||
val, err := json.Marshal(rec)
|
val, err := json.Marshal(rec)
|
||||||
if err != nil {
|
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 {
|
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.
|
// Update hash -> height index.
|
||||||
hashHex := meta.Hash.String()
|
hashHex := meta.Hash.String()
|
||||||
if err := c.store.Set(groupBlockIndex, hashHex, strconv.FormatUint(meta.Height, 10)); err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockByHeight retrieves a block by its height.
|
// GetBlockByHeight retrieves a block by its height.
|
||||||
|
//
|
||||||
|
// blk, meta, err := blockchain.GetBlockByHeight(1000)
|
||||||
func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error) {
|
func (c *Chain) GetBlockByHeight(height uint64) (*types.Block, *BlockMeta, error) {
|
||||||
val, err := c.store.Get(groupBlocks, heightKey(height))
|
val, err := c.store.Get(groupBlocks, heightKey(height))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, store.ErrNotFound) {
|
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)
|
return decodeBlockRecord(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockByHash retrieves a block by its hash.
|
// GetBlockByHash retrieves a block by its hash.
|
||||||
|
//
|
||||||
|
// blk, meta, err := blockchain.GetBlockByHash(blockHash)
|
||||||
func (c *Chain) GetBlockByHash(hash types.Hash) (*types.Block, *BlockMeta, error) {
|
func (c *Chain) GetBlockByHash(hash types.Hash) (*types.Block, *BlockMeta, error) {
|
||||||
heightStr, err := c.store.Get(groupBlockIndex, hash.String())
|
heightStr, err := c.store.Get(groupBlockIndex, hash.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, store.ErrNotFound) {
|
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)
|
height, err := strconv.ParseUint(heightStr, 10, 64)
|
||||||
if err != nil {
|
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)
|
return c.GetBlockByHeight(height)
|
||||||
}
|
}
|
||||||
|
|
@ -104,12 +112,14 @@ type txRecord struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutTransaction stores a transaction with metadata.
|
// 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 {
|
func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxMeta) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
enc := wire.NewEncoder(&buf)
|
enc := wire.NewEncoder(&buf)
|
||||||
wire.EncodeTransaction(enc, tx)
|
wire.EncodeTransaction(enc, tx)
|
||||||
if err := enc.Err(); err != nil {
|
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{
|
rec := txRecord{
|
||||||
|
|
@ -118,42 +128,46 @@ func (c *Chain) PutTransaction(hash types.Hash, tx *types.Transaction, meta *TxM
|
||||||
}
|
}
|
||||||
val, err := json.Marshal(rec)
|
val, err := json.Marshal(rec)
|
||||||
if err != nil {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTransaction retrieves a transaction by hash.
|
// GetTransaction retrieves a transaction by hash.
|
||||||
|
//
|
||||||
|
// tx, meta, err := blockchain.GetTransaction(txHash)
|
||||||
func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, error) {
|
func (c *Chain) GetTransaction(hash types.Hash) (*types.Transaction, *TxMeta, error) {
|
||||||
val, err := c.store.Get(groupTx, hash.String())
|
val, err := c.store.Get(groupTx, hash.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, store.ErrNotFound) {
|
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
|
var rec txRecord
|
||||||
if err := json.Unmarshal([]byte(val), &rec); err != nil {
|
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)
|
blob, err := hex.DecodeString(rec.Blob)
|
||||||
if err != nil {
|
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))
|
dec := wire.NewDecoder(bytes.NewReader(blob))
|
||||||
tx := wire.DecodeTransaction(dec)
|
tx := wire.DecodeTransaction(dec)
|
||||||
if err := dec.Err(); err != nil {
|
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
|
return &tx, &rec.Meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasTransaction checks whether a transaction exists in the store.
|
// HasTransaction checks whether a transaction exists in the store.
|
||||||
|
//
|
||||||
|
// if blockchain.HasTransaction(txHash) { /* already stored */ }
|
||||||
func (c *Chain) HasTransaction(hash types.Hash) bool {
|
func (c *Chain) HasTransaction(hash types.Hash) bool {
|
||||||
_, err := c.store.Get(groupTx, hash.String())
|
_, err := c.store.Get(groupTx, hash.String())
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|
@ -164,11 +178,11 @@ func (c *Chain) HasTransaction(hash types.Hash) bool {
|
||||||
func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
|
func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
|
||||||
val, err := c.store.Get(groupBlocks, heightKey(height))
|
val, err := c.store.Get(groupBlocks, heightKey(height))
|
||||||
if err != nil {
|
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
|
var rec blockRecord
|
||||||
if err := json.Unmarshal([]byte(val), &rec); err != nil {
|
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
|
return &rec.Meta, nil
|
||||||
}
|
}
|
||||||
|
|
@ -176,16 +190,16 @@ func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
|
||||||
func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) {
|
func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) {
|
||||||
var rec blockRecord
|
var rec blockRecord
|
||||||
if err := json.Unmarshal([]byte(val), &rec); err != nil {
|
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)
|
blob, err := hex.DecodeString(rec.Blob)
|
||||||
if err != nil {
|
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))
|
dec := wire.NewDecoder(bytes.NewReader(blob))
|
||||||
blk := wire.DecodeBlock(dec)
|
blk := wire.DecodeBlock(dec)
|
||||||
if err := dec.Err(); err != nil {
|
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
|
return &blk, &rec.Meta, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
corelog "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/consensus"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const syncBatchSize = 10
|
const syncBatchSize = 10
|
||||||
|
|
@ -51,12 +51,12 @@ func DefaultSyncOptions() SyncOptions {
|
||||||
func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions) error {
|
func (c *Chain) Sync(ctx context.Context, client *rpc.Client, opts SyncOptions) error {
|
||||||
localHeight, err := c.Height()
|
localHeight, err := c.Height()
|
||||||
if err != nil {
|
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()
|
remoteHeight, err := client.GetHeight()
|
||||||
if err != nil {
|
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 {
|
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)
|
blocks, err := client.GetBlocksDetails(localHeight, batch)
|
||||||
if err != nil {
|
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 {
|
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 {
|
for _, bd := range blocks {
|
||||||
if err := c.processBlock(bd, opts); err != nil {
|
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()
|
localHeight, err = c.Height()
|
||||||
if err != nil {
|
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 {
|
func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
|
||||||
if bd.Height > 0 && bd.Height%100 == 0 {
|
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)
|
blockBlob, err := hex.DecodeString(bd.Blob)
|
||||||
if err != nil {
|
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.
|
// 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))
|
dec := wire.NewDecoder(bytes.NewReader(blockBlob))
|
||||||
blk := wire.DecodeBlock(dec)
|
blk := wire.DecodeBlock(dec)
|
||||||
if err := dec.Err(); err != nil {
|
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))
|
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)
|
txBlobBytes, err := hex.DecodeString(txInfo.Blob)
|
||||||
if err != nil {
|
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)
|
txBlobs = append(txBlobs, txBlobBytes)
|
||||||
}
|
}
|
||||||
|
|
@ -136,11 +136,10 @@ func (c *Chain) processBlock(bd rpc.BlockDetails, opts SyncOptions) error {
|
||||||
computedHash := wire.BlockHash(&blk)
|
computedHash := wire.BlockHash(&blk)
|
||||||
daemonHash, err := types.HashFromHex(bd.ID)
|
daemonHash, err := types.HashFromHex(bd.ID)
|
||||||
if err != nil {
|
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 {
|
if computedHash != daemonHash {
|
||||||
return fmt.Errorf("block hash mismatch: computed %s, daemon says %s",
|
return corelog.E("Chain.processBlock", fmt.Sprintf("block hash mismatch: computed %s, daemon says %s", computedHash, daemonHash), nil)
|
||||||
computedHash, daemonHash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.processBlockBlobs(blockBlob, txBlobs, bd.Height, diff, opts)
|
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))
|
dec := wire.NewDecoder(bytes.NewReader(blockBlob))
|
||||||
blk := wire.DecodeBlock(dec)
|
blk := wire.DecodeBlock(dec)
|
||||||
if err := dec.Err(); err != nil {
|
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.
|
// Compute the block hash.
|
||||||
|
|
@ -165,22 +164,21 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
||||||
if height == 0 {
|
if height == 0 {
|
||||||
genesisHash, err := types.HashFromHex(GenesisHash)
|
genesisHash, err := types.HashFromHex(GenesisHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse genesis hash: %w", err)
|
return corelog.E("Chain.processBlockBlobs", "parse genesis hash", err)
|
||||||
}
|
}
|
||||||
if blockHash != genesisHash {
|
if blockHash != genesisHash {
|
||||||
return fmt.Errorf("genesis hash %s does not match expected %s",
|
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("genesis hash %s does not match expected %s", blockHash, GenesisHash), nil)
|
||||||
blockHash, GenesisHash)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate header.
|
// Validate header.
|
||||||
if err := c.ValidateHeader(&blk, height); err != nil {
|
if err := c.ValidateHeader(&blk, height, opts.Forks); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate miner transaction structure.
|
// Validate miner transaction structure.
|
||||||
if err := consensus.ValidateMinerTx(&blk.MinerTx, height, opts.Forks); err != nil {
|
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.
|
// Calculate cumulative difficulty.
|
||||||
|
|
@ -188,7 +186,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
||||||
if height > 0 {
|
if height > 0 {
|
||||||
_, prevMeta, err := c.TopBlock()
|
_, prevMeta, err := c.TopBlock()
|
||||||
if err != nil {
|
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
|
cumulDiff = prevMeta.CumulativeDiff + difficulty
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -199,13 +197,13 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
||||||
minerTxHash := wire.TransactionHash(&blk.MinerTx)
|
minerTxHash := wire.TransactionHash(&blk.MinerTx)
|
||||||
minerGindexes, err := c.indexOutputs(minerTxHash, &blk.MinerTx)
|
minerGindexes, err := c.indexOutputs(minerTxHash, &blk.MinerTx)
|
||||||
if err != nil {
|
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{
|
if err := c.PutTransaction(minerTxHash, &blk.MinerTx, &TxMeta{
|
||||||
KeeperBlock: height,
|
KeeperBlock: height,
|
||||||
GlobalOutputIndexes: minerGindexes,
|
GlobalOutputIndexes: minerGindexes,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("store miner tx: %w", err)
|
return corelog.E("Chain.processBlockBlobs", "store miner tx", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process regular transactions from txBlobs.
|
// Process regular transactions from txBlobs.
|
||||||
|
|
@ -213,27 +211,27 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
||||||
txDec := wire.NewDecoder(bytes.NewReader(txBlobData))
|
txDec := wire.NewDecoder(bytes.NewReader(txBlobData))
|
||||||
tx := wire.DecodeTransaction(txDec)
|
tx := wire.DecodeTransaction(txDec)
|
||||||
if err := txDec.Err(); err != nil {
|
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)
|
txHash := wire.TransactionHash(&tx)
|
||||||
|
|
||||||
// Validate transaction semantics.
|
// Validate transaction semantics, including the HF5 freeze window.
|
||||||
if err := consensus.ValidateTransaction(&tx, txBlobData, opts.Forks, height); err != nil {
|
if err := consensus.ValidateTransactionInBlock(&tx, txBlobData, opts.Forks, height); err != nil {
|
||||||
return fmt.Errorf("validate tx %s: %w", txHash, err)
|
return corelog.E("Chain.processBlockBlobs", fmt.Sprintf("validate tx %s", txHash), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally verify signatures using the chain's output index.
|
// Optionally verify signatures using the chain's output index.
|
||||||
if opts.VerifySignatures {
|
if opts.VerifySignatures {
|
||||||
if err := consensus.VerifyTransactionSignatures(&tx, opts.Forks, height, c.GetRingOutputs, c.GetZCRingOutputs); err != nil {
|
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.
|
// Index outputs.
|
||||||
gindexes, err := c.indexOutputs(txHash, &tx)
|
gindexes, err := c.indexOutputs(txHash, &tx)
|
||||||
if err != nil {
|
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.
|
// Mark key images as spent.
|
||||||
|
|
@ -241,11 +239,15 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
||||||
switch inp := vin.(type) {
|
switch inp := vin.(type) {
|
||||||
case types.TxInputToKey:
|
case types.TxInputToKey:
|
||||||
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
|
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:
|
case types.TxInputZC:
|
||||||
if err := c.MarkSpent(inp.KeyImage, height); err != nil {
|
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,
|
KeeperBlock: height,
|
||||||
GlobalOutputIndexes: gindexes,
|
GlobalOutputIndexes: gindexes,
|
||||||
}); err != nil {
|
}); 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.
|
// Batch-fetch tx blobs.
|
||||||
txHexes, missed, err := client.GetTransactions(allHashes)
|
txHexes, missed, err := client.GetTransactions(allHashes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fetch tx blobs: %w", err)
|
return corelog.E("resolveBlockBlobs", "fetch tx blobs", err)
|
||||||
}
|
}
|
||||||
if len(missed) > 0 {
|
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) {
|
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.
|
// Index fetched blobs by hash.
|
||||||
|
|
@ -364,16 +366,16 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
|
||||||
// Parse header from object_in_json.
|
// Parse header from object_in_json.
|
||||||
hdr, err := parseBlockHeader(bd.ObjectInJSON)
|
hdr, err := parseBlockHeader(bd.ObjectInJSON)
|
||||||
if err != nil {
|
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].
|
// Miner tx blob is transactions_details[0].
|
||||||
if len(bd.Transactions) == 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)
|
minerTxBlob, err := hex.DecodeString(bd.Transactions[0].Blob)
|
||||||
if err != nil {
|
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.
|
// Collect regular tx hashes.
|
||||||
|
|
@ -381,7 +383,7 @@ func resolveBlockBlobs(blocks []rpc.BlockDetails, client *rpc.Client) error {
|
||||||
for _, txInfo := range bd.Transactions[1:] {
|
for _, txInfo := range bd.Transactions[1:] {
|
||||||
h, err := types.HashFromHex(txInfo.ID)
|
h, err := types.HashFromHex(txInfo.ID)
|
||||||
if err != nil {
|
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)
|
txHashes = append(txHashes, h)
|
||||||
}
|
}
|
||||||
|
|
@ -411,17 +413,17 @@ var aggregatedRE = regexp.MustCompile(`"AGGREGATED"\s*:\s*\{([^}]+)\}`)
|
||||||
func parseBlockHeader(objectInJSON string) (*types.BlockHeader, error) {
|
func parseBlockHeader(objectInJSON string) (*types.BlockHeader, error) {
|
||||||
m := aggregatedRE.FindStringSubmatch(objectInJSON)
|
m := aggregatedRE.FindStringSubmatch(objectInJSON)
|
||||||
if m == nil {
|
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
|
var hj blockHeaderJSON
|
||||||
if err := json.Unmarshal([]byte("{"+m[1]+"}"), &hj); err != nil {
|
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)
|
prevID, err := types.HashFromHex(hj.PrevID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse prev_id: %w", err)
|
return nil, corelog.E("parseBlockHeader", "parse prev_id", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.BlockHeader{
|
return &types.BlockHeader{
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,17 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/rpc"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"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.
|
// makeGenesisBlockBlob creates a minimal genesis block and returns its hex blob and hash.
|
||||||
|
|
@ -735,6 +736,87 @@ func TestSync_Bad_InvalidBlockBlob(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSync_Bad_PreHardforkFreeze(t *testing.T) {
|
||||||
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
||||||
|
genesisBytes, err := hex.DecodeString(genesisBlob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode genesis blob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
regularTx := types.Transaction{
|
||||||
|
Version: 1,
|
||||||
|
Vin: []types.TxInput{
|
||||||
|
types.TxInputToKey{
|
||||||
|
Amount: 1000000000000,
|
||||||
|
KeyOffsets: []types.TxOutRef{{
|
||||||
|
Tag: types.RefTypeGlobalIndex,
|
||||||
|
GlobalIndex: 0,
|
||||||
|
}},
|
||||||
|
KeyImage: types.KeyImage{0xaa, 0xbb, 0xcc},
|
||||||
|
EtcDetails: wire.EncodeVarint(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Vout: []types.TxOutput{
|
||||||
|
types.TxOutputBare{
|
||||||
|
Amount: 900000000000,
|
||||||
|
Target: types.TxOutToKey{Key: types.PublicKey{0x02}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extra: wire.EncodeVarint(0),
|
||||||
|
Attachment: wire.EncodeVarint(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
var txBuf bytes.Buffer
|
||||||
|
txEnc := wire.NewEncoder(&txBuf)
|
||||||
|
wire.EncodeTransaction(txEnc, ®ularTx)
|
||||||
|
regularTxBlob := txBuf.Bytes()
|
||||||
|
regularTxHash := wire.TransactionHash(®ularTx)
|
||||||
|
|
||||||
|
minerTx1 := testCoinbaseTx(1)
|
||||||
|
block1 := types.Block{
|
||||||
|
BlockHeader: types.BlockHeader{
|
||||||
|
MajorVersion: 1,
|
||||||
|
Nonce: 42,
|
||||||
|
PrevID: genesisHash,
|
||||||
|
Timestamp: 1770897720,
|
||||||
|
},
|
||||||
|
MinerTx: minerTx1,
|
||||||
|
TxHashes: []types.Hash{regularTxHash},
|
||||||
|
}
|
||||||
|
|
||||||
|
var blk1Buf bytes.Buffer
|
||||||
|
blk1Enc := wire.NewEncoder(&blk1Buf)
|
||||||
|
wire.EncodeBlock(blk1Enc, &block1)
|
||||||
|
block1Blob := blk1Buf.Bytes()
|
||||||
|
|
||||||
|
orig := GenesisHash
|
||||||
|
GenesisHash = genesisHash.String()
|
||||||
|
t.Cleanup(func() { GenesisHash = orig })
|
||||||
|
|
||||||
|
s, _ := store.New(":memory:")
|
||||||
|
defer s.Close()
|
||||||
|
c := New(s)
|
||||||
|
|
||||||
|
opts := SyncOptions{
|
||||||
|
Forks: []config.HardFork{
|
||||||
|
{Version: config.HF0Initial, Height: 0, Mandatory: true},
|
||||||
|
{Version: config.HF5, Height: 2, Mandatory: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.processBlockBlobs(genesisBytes, nil, 0, 1, opts); err != nil {
|
||||||
|
t.Fatalf("process genesis: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.processBlockBlobs(block1Blob, [][]byte{regularTxBlob}, 1, 100, opts)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected freeze rejection, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "freeze") {
|
||||||
|
t.Fatalf("expected freeze error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
|
// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
|
||||||
func testCoinbaseTxV2(height uint64) types.Transaction {
|
func testCoinbaseTxV2(height uint64) types.Transaction {
|
||||||
return 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")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,32 +7,36 @@ package chain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/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.
|
// ValidateHeader checks a block header before storage.
|
||||||
// expectedHeight is the height at which this block would be stored.
|
// 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()
|
currentHeight, err := c.Height()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate: get height: %w", err)
|
return coreerr.E("Chain.ValidateHeader", "validate: get height", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Height sequence check.
|
// Height sequence check.
|
||||||
if expectedHeight != currentHeight {
|
if expectedHeight != currentHeight {
|
||||||
return fmt.Errorf("validate: expected height %d but chain is at %d",
|
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: expected height %d but chain is at %d", expectedHeight, currentHeight), nil)
|
||||||
expectedHeight, currentHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genesis block: prev_id must be zero.
|
// Genesis block: prev_id must be zero.
|
||||||
if expectedHeight == 0 {
|
if expectedHeight == 0 {
|
||||||
if !b.PrevID.IsZero() {
|
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
|
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.
|
// Non-genesis: prev_id must match top block hash.
|
||||||
_, topMeta, err := c.TopBlock()
|
_, topMeta, err := c.TopBlock()
|
||||||
if err != nil {
|
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 {
|
if b.PrevID != topMeta.Hash {
|
||||||
return fmt.Errorf("validate: prev_id %s does not match top block %s",
|
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: prev_id %s does not match top block %s", b.PrevID, topMeta.Hash), nil)
|
||||||
b.PrevID, topMeta.Hash)
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Block size check.
|
||||||
|
|
@ -52,8 +60,7 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
|
||||||
enc := wire.NewEncoder(&buf)
|
enc := wire.NewEncoder(&buf)
|
||||||
wire.EncodeBlock(enc, b)
|
wire.EncodeBlock(enc, b)
|
||||||
if enc.Err() == nil && uint64(buf.Len()) > config.MaxBlockSize {
|
if enc.Err() == nil && uint64(buf.Len()) > config.MaxBlockSize {
|
||||||
return fmt.Errorf("validate: block size %d exceeds max %d",
|
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: block size %d exceeds max %d", buf.Len(), config.MaxBlockSize), nil)
|
||||||
buf.Len(), config.MaxBlockSize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ package chain
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
store "forge.lthn.ai/core/go-store"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
|
store "dappco.re/go/core/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateHeader_Good_Genesis(t *testing.T) {
|
func TestValidateHeader_Good_Genesis(t *testing.T) {
|
||||||
|
|
@ -19,13 +20,13 @@ func TestValidateHeader_Good_Genesis(t *testing.T) {
|
||||||
|
|
||||||
blk := &types.Block{
|
blk := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{
|
BlockHeader: types.BlockHeader{
|
||||||
MajorVersion: 1,
|
MajorVersion: 0,
|
||||||
Timestamp: 1770897600,
|
Timestamp: 1770897600,
|
||||||
},
|
},
|
||||||
MinerTx: testCoinbaseTx(0),
|
MinerTx: testCoinbaseTx(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.ValidateHeader(blk, 0)
|
err := c.ValidateHeader(blk, 0, config.MainnetForks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ValidateHeader genesis: %v", err)
|
t.Fatalf("ValidateHeader genesis: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +39,7 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
|
||||||
|
|
||||||
// Store block 0.
|
// Store block 0.
|
||||||
blk0 := &types.Block{
|
blk0 := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
|
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
|
||||||
MinerTx: testCoinbaseTx(0),
|
MinerTx: testCoinbaseTx(0),
|
||||||
}
|
}
|
||||||
hash0 := types.Hash{0x01}
|
hash0 := types.Hash{0x01}
|
||||||
|
|
@ -47,14 +48,14 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
|
||||||
// Validate block 1.
|
// Validate block 1.
|
||||||
blk1 := &types.Block{
|
blk1 := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{
|
BlockHeader: types.BlockHeader{
|
||||||
MajorVersion: 1,
|
MajorVersion: 0,
|
||||||
Timestamp: 1770897720,
|
Timestamp: 1770897720,
|
||||||
PrevID: hash0,
|
PrevID: hash0,
|
||||||
},
|
},
|
||||||
MinerTx: testCoinbaseTx(1),
|
MinerTx: testCoinbaseTx(1),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.ValidateHeader(blk1, 1)
|
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ValidateHeader block 1: %v", err)
|
t.Fatalf("ValidateHeader block 1: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -66,21 +67,21 @@ func TestValidateHeader_Bad_WrongPrevID(t *testing.T) {
|
||||||
c := New(s)
|
c := New(s)
|
||||||
|
|
||||||
blk0 := &types.Block{
|
blk0 := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
|
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
|
||||||
MinerTx: testCoinbaseTx(0),
|
MinerTx: testCoinbaseTx(0),
|
||||||
}
|
}
|
||||||
c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0})
|
c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0})
|
||||||
|
|
||||||
blk1 := &types.Block{
|
blk1 := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{
|
BlockHeader: types.BlockHeader{
|
||||||
MajorVersion: 1,
|
MajorVersion: 0,
|
||||||
Timestamp: 1770897720,
|
Timestamp: 1770897720,
|
||||||
PrevID: types.Hash{0xFF}, // wrong
|
PrevID: types.Hash{0xFF}, // wrong
|
||||||
},
|
},
|
||||||
MinerTx: testCoinbaseTx(1),
|
MinerTx: testCoinbaseTx(1),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.ValidateHeader(blk1, 1)
|
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for wrong prev_id")
|
t.Fatal("expected error for wrong prev_id")
|
||||||
}
|
}
|
||||||
|
|
@ -92,12 +93,12 @@ func TestValidateHeader_Bad_WrongHeight(t *testing.T) {
|
||||||
c := New(s)
|
c := New(s)
|
||||||
|
|
||||||
blk := &types.Block{
|
blk := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
|
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
|
||||||
MinerTx: testCoinbaseTx(0),
|
MinerTx: testCoinbaseTx(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chain is empty (height 0), but we pass expectedHeight=5.
|
// 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 {
|
if err == nil {
|
||||||
t.Fatal("expected error for wrong height")
|
t.Fatal("expected error for wrong height")
|
||||||
}
|
}
|
||||||
|
|
@ -110,14 +111,33 @@ func TestValidateHeader_Bad_GenesisNonZeroPrev(t *testing.T) {
|
||||||
|
|
||||||
blk := &types.Block{
|
blk := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{
|
BlockHeader: types.BlockHeader{
|
||||||
MajorVersion: 1,
|
MajorVersion: 0,
|
||||||
PrevID: types.Hash{0xFF}, // genesis must have zero prev_id
|
PrevID: types.Hash{0xFF}, // genesis must have zero prev_id
|
||||||
},
|
},
|
||||||
MinerTx: testCoinbaseTx(0),
|
MinerTx: testCoinbaseTx(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.ValidateHeader(blk, 0)
|
err := c.ValidateHeader(blk, 0, config.MainnetForks)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for genesis with non-zero prev_id")
|
t.Fatal("expected error for genesis with non-zero prev_id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateHeader_Bad_WrongVersion(t *testing.T) {
|
||||||
|
s, _ := store.New(":memory:")
|
||||||
|
defer s.Close()
|
||||||
|
c := New(s)
|
||||||
|
|
||||||
|
blk := &types.Block{
|
||||||
|
BlockHeader: types.BlockHeader{
|
||||||
|
MajorVersion: 1,
|
||||||
|
Timestamp: 1770897600,
|
||||||
|
},
|
||||||
|
MinerTx: testCoinbaseTx(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.ValidateHeader(blk, 0, config.MainnetForks)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for wrong block version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
92
chain_commands.go
Normal file
92
chain_commands.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package blockchain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
coreio "dappco.re/go/core/io"
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
|
|
||||||
|
"dappco.re/go/core/blockchain/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultChainSeed = "seeds.lthn.io:36942"
|
||||||
|
|
||||||
|
// AddChainCommands registers the `chain` command group on a Cobra root.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// cli.WithCommands("chain", blockchain.AddChainCommands)
|
||||||
|
//
|
||||||
|
// The command group owns the explorer and sync subcommands, so the
|
||||||
|
// command path documents the node features directly.
|
||||||
|
func AddChainCommands(root *cobra.Command) {
|
||||||
|
var (
|
||||||
|
chainDataDir string
|
||||||
|
seedPeerAddress string
|
||||||
|
useTestnet bool
|
||||||
|
)
|
||||||
|
|
||||||
|
chainCmd := &cobra.Command{
|
||||||
|
Use: "chain",
|
||||||
|
Short: "Lethean blockchain node",
|
||||||
|
Long: "Manage the Lethean blockchain — sync, explore, and mine.",
|
||||||
|
}
|
||||||
|
|
||||||
|
chainCmd.PersistentFlags().StringVar(&chainDataDir, "data-dir", defaultChainDataDirPath(), "blockchain data directory")
|
||||||
|
chainCmd.PersistentFlags().StringVar(&seedPeerAddress, "seed", defaultChainSeed, "seed peer address (host:port)")
|
||||||
|
chainCmd.PersistentFlags().BoolVar(&useTestnet, "testnet", false, "use testnet")
|
||||||
|
|
||||||
|
chainCmd.AddCommand(
|
||||||
|
newChainExplorerCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
|
||||||
|
newChainSyncCommand(&chainDataDir, &seedPeerAddress, &useTestnet),
|
||||||
|
)
|
||||||
|
|
||||||
|
root.AddCommand(chainCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainConfigForSeed(useTestnet bool, seedPeerAddress string) (config.ChainConfig, []config.HardFork, string) {
|
||||||
|
if useTestnet {
|
||||||
|
if seedPeerAddress == defaultChainSeed {
|
||||||
|
seedPeerAddress = "localhost:46942"
|
||||||
|
}
|
||||||
|
return config.Testnet, config.TestnetForks, seedPeerAddress
|
||||||
|
}
|
||||||
|
return config.Mainnet, config.MainnetForks, seedPeerAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultChainDataDirPath() string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return ".lethean"
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".lethean", "chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureChainDataDirExists(dataDir string) error {
|
||||||
|
if err := coreio.Local.EnsureDir(dataDir); err != nil {
|
||||||
|
return coreerr.E("ensureChainDataDirExists", "create data dir", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateChainOptions(chainDataDir, seedPeerAddress string) error {
|
||||||
|
if chainDataDir == "" {
|
||||||
|
return coreerr.E("validateChainOptions", "data dir is required", nil)
|
||||||
|
}
|
||||||
|
if seedPeerAddress == "" {
|
||||||
|
return coreerr.E("validateChainOptions", "seed is required", nil)
|
||||||
|
}
|
||||||
|
if _, _, err := net.SplitHostPort(seedPeerAddress); err != nil {
|
||||||
|
return coreerr.E("validateChainOptions", fmt.Sprintf("seed %q must be host:port", seedPeerAddress), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
cli "forge.lthn.ai/core/cli/pkg/cli"
|
cli "dappco.re/go/core/cli/pkg/cli"
|
||||||
blockchain "forge.lthn.ai/core/go-blockchain"
|
blockchain "dappco.re/go/core/blockchain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
142
cmd_sync.go
142
cmd_sync.go
|
|
@ -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
|
|
||||||
}
|
|
||||||
67
commands.go
67
commands.go
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -46,3 +46,68 @@ func TestAddChainCommands_Good_PersistentFlags(t *testing.T) {
|
||||||
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("seed"))
|
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("seed"))
|
||||||
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("testnet"))
|
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("testnet"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateChainOptions_Good(t *testing.T) {
|
||||||
|
err := validateChainOptions("/tmp/lethean", "seed.example:36942")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateChainOptions_Bad(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dataDir string
|
||||||
|
seed string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "missing data dir", dataDir: "", seed: "seed.example:36942", want: "data dir is required"},
|
||||||
|
{name: "missing seed", dataDir: "/tmp/lethean", seed: "", want: "seed is required"},
|
||||||
|
{name: "malformed seed", dataDir: "/tmp/lethean", seed: "seed.example", want: "must be host:port"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateChainOptions(tt.dataDir, tt.seed)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChainSyncCommand_BadMutuallyExclusiveFlags(t *testing.T) {
|
||||||
|
dataDir := t.TempDir()
|
||||||
|
seed := "seed.example:36942"
|
||||||
|
testnet := false
|
||||||
|
|
||||||
|
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
|
||||||
|
cmd.SetArgs([]string{"--daemon", "--stop"})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "cannot be combined")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChainSyncCommand_BadArgsRejected(t *testing.T) {
|
||||||
|
dataDir := t.TempDir()
|
||||||
|
seed := "seed.example:36942"
|
||||||
|
testnet := false
|
||||||
|
|
||||||
|
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
|
||||||
|
cmd.SetArgs([]string{"extra"})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown command")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChainExplorerCommand_BadSeedRejected(t *testing.T) {
|
||||||
|
dataDir := t.TempDir()
|
||||||
|
seed := "bad-seed"
|
||||||
|
testnet := false
|
||||||
|
|
||||||
|
cmd := newChainExplorerCommand(&dataDir, &seed, &testnet)
|
||||||
|
cmd.SetArgs(nil)
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "must be host:port")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ var TestnetForks = []HardFork{
|
||||||
//
|
//
|
||||||
// A fork with Height=0 is active from genesis (height 0).
|
// A fork with Height=0 is active from genesis (height 0).
|
||||||
// A fork with Height=N is active at heights > N.
|
// 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 {
|
func VersionAtHeight(forks []HardFork, height uint64) uint8 {
|
||||||
var version uint8
|
var version uint8
|
||||||
for _, hf := range forks {
|
for _, hf := range forks {
|
||||||
|
|
@ -85,6 +87,8 @@ func VersionAtHeight(forks []HardFork, height uint64) uint8 {
|
||||||
|
|
||||||
// IsHardForkActive reports whether the specified hardfork version is active
|
// IsHardForkActive reports whether the specified hardfork version is active
|
||||||
// at the given block height.
|
// 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 {
|
func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool {
|
||||||
for _, hf := range forks {
|
for _, hf := range forks {
|
||||||
if hf.Version == version {
|
if hf.Version == version {
|
||||||
|
|
@ -93,3 +97,17 @@ func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool {
|
||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
49
config/hardfork_activation_test.go
Normal file
49
config/hardfork_activation_test.go
Normal 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
20
consensus/balance.go
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package consensus
|
||||||
|
|
||||||
|
import "dappco.re/go/core/blockchain/crypto"
|
||||||
|
|
||||||
|
// VerifyBalanceProof verifies a generic double-Schnorr proof against the
|
||||||
|
// provided public points.
|
||||||
|
//
|
||||||
|
// The caller is responsible for constructing the balance context point(s)
|
||||||
|
// from transaction inputs, outputs, fees, and any asset-operation terms.
|
||||||
|
// This helper only performs the cryptographic check.
|
||||||
|
//
|
||||||
|
// ok := consensus.VerifyBalanceProof(txHash, false, pointA, pointB, proofBytes)
|
||||||
|
func VerifyBalanceProof(hash [32]byte, aIsX bool, a [32]byte, b [32]byte, proof []byte) bool {
|
||||||
|
return crypto.VerifyDoubleSchnorr(hash, aIsX, a, b, proof)
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
|
||||||
|
"dappco.re/go/core/blockchain/config"
|
||||||
|
"dappco.re/go/core/blockchain/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsPoS returns true if the block flags indicate a Proof-of-Stake block.
|
// 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
|
// CheckTimestamp validates a block's timestamp against future limits and
|
||||||
// the median of recent timestamps.
|
// 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 {
|
func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, recentTimestamps []uint64) error {
|
||||||
// Future time limit.
|
// Future time limit.
|
||||||
limit := config.BlockFutureTimeLimit
|
limit := config.BlockFutureTimeLimit
|
||||||
|
|
@ -28,8 +32,8 @@ func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, rec
|
||||||
limit = config.PosBlockFutureTimeLimit
|
limit = config.PosBlockFutureTimeLimit
|
||||||
}
|
}
|
||||||
if blockTimestamp > adjustedTime+limit {
|
if blockTimestamp > adjustedTime+limit {
|
||||||
return fmt.Errorf("%w: %d > %d + %d", ErrTimestampFuture,
|
return coreerr.E("CheckTimestamp", fmt.Sprintf("%d > %d + %d",
|
||||||
blockTimestamp, adjustedTime, limit)
|
blockTimestamp, adjustedTime, limit), ErrTimestampFuture)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Median check — only when we have enough history.
|
// Median check — only when we have enough history.
|
||||||
|
|
@ -39,8 +43,8 @@ func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, rec
|
||||||
|
|
||||||
median := medianTimestamp(recentTimestamps)
|
median := medianTimestamp(recentTimestamps)
|
||||||
if blockTimestamp < median {
|
if blockTimestamp < median {
|
||||||
return fmt.Errorf("%w: %d < median %d", ErrTimestampOld,
|
return coreerr.E("CheckTimestamp", fmt.Sprintf("%d < median %d",
|
||||||
blockTimestamp, median)
|
blockTimestamp, median), ErrTimestampOld)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -59,21 +63,49 @@ func medianTimestamp(timestamps []uint64) uint64 {
|
||||||
return sorted[n/2]
|
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.
|
// ValidateMinerTx checks the structure of a coinbase (miner) transaction.
|
||||||
// For PoW blocks: exactly 1 input (TxInputGenesis). For PoS blocks: exactly
|
// For PoW blocks: exactly 1 input (TxInputGenesis). For PoS blocks: exactly
|
||||||
// 2 inputs (TxInputGenesis + stake input).
|
// 2 inputs (TxInputGenesis + stake input).
|
||||||
|
//
|
||||||
|
// consensus.ValidateMinerTx(&blk.MinerTx, height, config.MainnetForks)
|
||||||
func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFork) error {
|
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 {
|
if len(tx.Vin) == 0 {
|
||||||
return fmt.Errorf("%w: no inputs", ErrMinerTxInputs)
|
return coreerr.E("ValidateMinerTx", "no inputs", ErrMinerTxInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// First input must be TxInputGenesis.
|
// First input must be TxInputGenesis.
|
||||||
gen, ok := tx.Vin[0].(types.TxInputGenesis)
|
gen, ok := tx.Vin[0].(types.TxInputGenesis)
|
||||||
if !ok {
|
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 {
|
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.
|
// 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) {
|
switch tx.Vin[1].(type) {
|
||||||
case types.TxInputToKey:
|
case types.TxInputToKey:
|
||||||
// Pre-HF4 PoS.
|
// Pre-HF4 PoS.
|
||||||
default:
|
case types.TxInputZC:
|
||||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||||
if !hf4Active {
|
if !hardForkFourActive {
|
||||||
return fmt.Errorf("%w: invalid PoS stake input type", ErrMinerTxInputs)
|
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 {
|
} 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
|
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
|
// ValidateBlockReward checks that the miner transaction outputs do not
|
||||||
// exceed the expected reward (base reward + fees for pre-HF4).
|
// 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 {
|
func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSize, totalFees uint64, forks []config.HardFork) error {
|
||||||
base := BaseReward(height)
|
base := BaseReward(height)
|
||||||
reward, err := BlockReward(base, blockSize, medianSize)
|
reward, err := BlockReward(base, blockSize, medianSize)
|
||||||
|
|
@ -107,31 +145,80 @@ func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||||
expected := MinerReward(reward, totalFees, hf4Active)
|
expected := MinerReward(reward, totalFees, hardForkFourActive)
|
||||||
|
|
||||||
// Sum miner tx outputs.
|
// Sum miner tx outputs.
|
||||||
var outputSum uint64
|
var outputSum uint64
|
||||||
for _, vout := range minerTx.Vout {
|
for _, vout := range minerTx.Vout {
|
||||||
if bare, ok := vout.(types.TxOutputBare); ok {
|
switch out := vout.(type) {
|
||||||
outputSum += bare.Amount
|
case types.TxOutputBare:
|
||||||
|
outputSum += out.Amount
|
||||||
|
case types.TxOutputZarcanum:
|
||||||
|
outputSum += out.EncryptedAmount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputSum > expected {
|
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
|
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
|
// ValidateBlock performs full consensus validation on a block. It checks
|
||||||
// the timestamp, miner transaction structure, and reward. Transaction
|
// the block version, timestamp, miner transaction structure, and reward.
|
||||||
// semantic validation for regular transactions should be done separately
|
// Transaction semantic validation for regular transactions should be done
|
||||||
// via ValidateTransaction for each tx in the block.
|
// 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,
|
func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, adjustedTime uint64,
|
||||||
recentTimestamps []uint64, forks []config.HardFork) error {
|
recentTimestamps []uint64, forks []config.HardFork) error {
|
||||||
|
|
||||||
|
// Block major version check.
|
||||||
|
if err := checkBlockVersion(blk.MajorVersion, forks, height); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Timestamp validation.
|
// Timestamp validation.
|
||||||
if err := CheckTimestamp(blk.Timestamp, blk.Flags, adjustedTime, recentTimestamps); err != nil {
|
if err := CheckTimestamp(blk.Timestamp, blk.Flags, adjustedTime, recentTimestamps); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -149,3 +236,51 @@ func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, a
|
||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestValidateMinerTx_Good(t *testing.T) {
|
||||||
tx := validMinerTx(100)
|
tx := validMinerTx(100)
|
||||||
err := ValidateMinerTx(tx, 100, config.MainnetForks)
|
err := ValidateMinerTx(tx, 100, config.MainnetForks)
|
||||||
|
|
@ -87,14 +96,14 @@ func TestValidateMinerTx_Bad_WrongHeight(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateMinerTx_Bad_NoInputs(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)
|
err := ValidateMinerTx(tx, 100, config.MainnetForks)
|
||||||
assert.ErrorIs(t, err, ErrMinerTxInputs)
|
assert.ErrorIs(t, err, ErrMinerTxInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
|
func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
|
||||||
tx := &types.Transaction{
|
tx := &types.Transaction{
|
||||||
Version: types.VersionInitial,
|
Version: types.VersionPreHF4,
|
||||||
Vin: []types.TxInput{types.TxInputToKey{Amount: 1}},
|
Vin: []types.TxInput{types.TxInputToKey{Amount: 1}},
|
||||||
}
|
}
|
||||||
err := ValidateMinerTx(tx, 100, config.MainnetForks)
|
err := ValidateMinerTx(tx, 100, config.MainnetForks)
|
||||||
|
|
@ -103,7 +112,7 @@ func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) {
|
||||||
|
|
||||||
func TestValidateMinerTx_Good_PoS(t *testing.T) {
|
func TestValidateMinerTx_Good_PoS(t *testing.T) {
|
||||||
tx := &types.Transaction{
|
tx := &types.Transaction{
|
||||||
Version: types.VersionInitial,
|
Version: types.VersionPreHF4,
|
||||||
Vin: []types.TxInput{
|
Vin: []types.TxInput{
|
||||||
types.TxInputGenesis{Height: 100},
|
types.TxInputGenesis{Height: 100},
|
||||||
types.TxInputToKey{Amount: 1}, // PoS stake input
|
types.TxInputToKey{Amount: 1}, // PoS stake input
|
||||||
|
|
@ -117,6 +126,148 @@ func TestValidateMinerTx_Good_PoS(t *testing.T) {
|
||||||
require.NoError(t, err)
|
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) {
|
func TestValidateBlockReward_Good(t *testing.T) {
|
||||||
height := uint64(100)
|
height := uint64(100)
|
||||||
tx := validMinerTx(height)
|
tx := validMinerTx(height)
|
||||||
|
|
@ -127,7 +278,7 @@ func TestValidateBlockReward_Good(t *testing.T) {
|
||||||
func TestValidateBlockReward_Bad_TooMuch(t *testing.T) {
|
func TestValidateBlockReward_Bad_TooMuch(t *testing.T) {
|
||||||
height := uint64(100)
|
height := uint64(100)
|
||||||
tx := &types.Transaction{
|
tx := &types.Transaction{
|
||||||
Version: types.VersionInitial,
|
Version: types.VersionPreHF4,
|
||||||
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
|
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
|
||||||
Vout: []types.TxOutput{
|
Vout: []types.TxOutput{
|
||||||
types.TxOutputBare{Amount: config.BlockReward + 1, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
|
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)
|
height := uint64(100)
|
||||||
fees := uint64(50_000_000_000)
|
fees := uint64(50_000_000_000)
|
||||||
tx := &types.Transaction{
|
tx := &types.Transaction{
|
||||||
Version: types.VersionInitial,
|
Version: types.VersionPreHF4,
|
||||||
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
|
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
|
||||||
Vout: []types.TxOutput{
|
Vout: []types.TxOutput{
|
||||||
types.TxOutputBare{Amount: config.BlockReward + fees, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
|
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)
|
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) {
|
func TestValidateBlock_Good(t *testing.T) {
|
||||||
now := uint64(time.Now().Unix())
|
now := uint64(time.Now().Unix())
|
||||||
height := uint64(100)
|
height := uint64(100)
|
||||||
blk := &types.Block{
|
blk := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{
|
BlockHeader: types.BlockHeader{
|
||||||
MajorVersion: 1,
|
MajorVersion: 0, // pre-HF1 on mainnet
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
Flags: 0, // PoW
|
Flags: 0, // PoW
|
||||||
},
|
},
|
||||||
|
|
@ -172,7 +370,7 @@ func TestValidateBlock_Bad_Timestamp(t *testing.T) {
|
||||||
height := uint64(100)
|
height := uint64(100)
|
||||||
blk := &types.Block{
|
blk := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{
|
BlockHeader: types.BlockHeader{
|
||||||
MajorVersion: 1,
|
MajorVersion: 0, // pre-HF1 on mainnet
|
||||||
Timestamp: now + config.BlockFutureTimeLimit + 100,
|
Timestamp: now + config.BlockFutureTimeLimit + 100,
|
||||||
Flags: 0,
|
Flags: 0,
|
||||||
},
|
},
|
||||||
|
|
@ -188,7 +386,7 @@ func TestValidateBlock_Bad_MinerTx(t *testing.T) {
|
||||||
height := uint64(100)
|
height := uint64(100)
|
||||||
blk := &types.Block{
|
blk := &types.Block{
|
||||||
BlockHeader: types.BlockHeader{
|
BlockHeader: types.BlockHeader{
|
||||||
MajorVersion: 1,
|
MajorVersion: 0, // pre-HF1 on mainnet
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
Flags: 0,
|
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)
|
err := ValidateBlock(blk, height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
|
||||||
assert.ErrorIs(t, err, ErrMinerTxHeight)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
// - Cryptographic: PoW hash verification (RandomX via CGo),
|
// - Cryptographic: PoW hash verification (RandomX via CGo),
|
||||||
// ring signature verification, proof verification.
|
// ring signature verification, proof verification.
|
||||||
//
|
//
|
||||||
// All functions take *config.ChainConfig and a block height for
|
// All validation functions take a hardfork schedule ([]config.HardFork)
|
||||||
// hardfork-aware validation. The package has no dependency on chain/.
|
// and a block height for hardfork-aware gating. The package has no
|
||||||
|
// dependency on chain/ or any storage layer.
|
||||||
package consensus
|
package consensus
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ var (
|
||||||
ErrInvalidOutput = errors.New("consensus: invalid output")
|
ErrInvalidOutput = errors.New("consensus: invalid output")
|
||||||
ErrDuplicateKeyImage = errors.New("consensus: duplicate key image in transaction")
|
ErrDuplicateKeyImage = errors.New("consensus: duplicate key image in transaction")
|
||||||
ErrInvalidExtra = errors.New("consensus: invalid extra field")
|
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.
|
// Transaction economic errors.
|
||||||
ErrInputOverflow = errors.New("consensus: input amount overflow")
|
ErrInputOverflow = errors.New("consensus: input amount overflow")
|
||||||
|
|
@ -27,12 +29,19 @@ var (
|
||||||
ErrNegativeFee = errors.New("consensus: outputs exceed inputs")
|
ErrNegativeFee = errors.New("consensus: outputs exceed inputs")
|
||||||
|
|
||||||
// Block errors.
|
// Block errors.
|
||||||
ErrBlockTooLarge = errors.New("consensus: block exceeds max size")
|
ErrBlockTooLarge = errors.New("consensus: block exceeds max size")
|
||||||
ErrTimestampFuture = errors.New("consensus: block timestamp too far in future")
|
ErrBlockMajorVersion = errors.New("consensus: invalid block major version for height")
|
||||||
ErrTimestampOld = errors.New("consensus: block timestamp below median")
|
ErrTimestampFuture = errors.New("consensus: block timestamp too far in future")
|
||||||
ErrMinerTxInputs = errors.New("consensus: invalid miner transaction inputs")
|
ErrTimestampOld = errors.New("consensus: block timestamp below median")
|
||||||
ErrMinerTxHeight = errors.New("consensus: miner transaction height mismatch")
|
ErrMinerTxInputs = errors.New("consensus: invalid miner transaction inputs")
|
||||||
ErrMinerTxUnlock = errors.New("consensus: miner transaction unlock time invalid")
|
ErrMinerTxHeight = errors.New("consensus: miner transaction height mismatch")
|
||||||
ErrRewardMismatch = errors.New("consensus: block reward mismatch")
|
ErrMinerTxVersion = errors.New("consensus: invalid miner transaction version for current hardfork")
|
||||||
ErrMinerTxProofs = errors.New("consensus: miner transaction proof count invalid")
|
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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,16 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"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.
|
// TxFee calculates the transaction fee for pre-HF4 (v0/v1) transactions.
|
||||||
// Coinbase transactions return 0. For standard transactions, fee equals
|
// Coinbase transactions return 0. For standard transactions, fee equals
|
||||||
// the difference between total input amounts and total output amounts.
|
// the difference between total input amounts and total output amounts.
|
||||||
|
//
|
||||||
|
// fee, err := consensus.TxFee(&tx)
|
||||||
func TxFee(tx *types.Transaction) (uint64, error) {
|
func TxFee(tx *types.Transaction) (uint64, error) {
|
||||||
if isCoinbase(tx) {
|
if isCoinbase(tx) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|
@ -31,7 +35,7 @@ func TxFee(tx *types.Transaction) (uint64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputSum > inputSum {
|
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
|
return inputSum - outputSum, nil
|
||||||
|
|
@ -46,18 +50,26 @@ func isCoinbase(tx *types.Transaction) bool {
|
||||||
return ok
|
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) {
|
func sumInputs(tx *types.Transaction) (uint64, error) {
|
||||||
var total uint64
|
var total uint64
|
||||||
for _, vin := range tx.Vin {
|
for _, vin := range tx.Vin {
|
||||||
toKey, ok := vin.(types.TxInputToKey)
|
var amount uint64
|
||||||
if !ok {
|
switch v := vin.(type) {
|
||||||
|
case types.TxInputToKey:
|
||||||
|
amount = v.Amount
|
||||||
|
case types.TxInputHTLC:
|
||||||
|
amount = v.Amount
|
||||||
|
case types.TxInputMultisig:
|
||||||
|
amount = v.Amount
|
||||||
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if total > math.MaxUint64-toKey.Amount {
|
if total > math.MaxUint64-amount {
|
||||||
return 0, ErrInputOverflow
|
return 0, ErrInputOverflow
|
||||||
}
|
}
|
||||||
total += toKey.Amount
|
total += amount
|
||||||
}
|
}
|
||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ package consensus
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
@ -71,3 +71,52 @@ func TestTxFee_Ugly(t *testing.T) {
|
||||||
_, err := TxFee(tx)
|
_, err := TxFee(tx)
|
||||||
assert.ErrorIs(t, err, ErrInputOverflow)
|
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
133
consensus/freeze_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,11 @@ package consensus
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
store "forge.lthn.ai/core/go-store"
|
store "dappco.re/go/core/store"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/chain"
|
"dappco.re/go/core/blockchain/chain"
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConsensusIntegration(t *testing.T) {
|
func TestConsensusIntegration(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
"dappco.re/go/core/blockchain/crypto"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxTarget is 2^256, used for difficulty comparison.
|
// 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.
|
// CheckDifficulty returns true if hash meets the given difficulty target.
|
||||||
// The hash (interpreted as a 256-bit little-endian number) must be less
|
// The hash (interpreted as a 256-bit little-endian number) must be less
|
||||||
// than maxTarget / difficulty.
|
// than maxTarget / difficulty.
|
||||||
|
//
|
||||||
|
// if consensus.CheckDifficulty(powHash, currentDifficulty) { /* valid PoW solution */ }
|
||||||
func CheckDifficulty(hash types.Hash, difficulty uint64) bool {
|
func CheckDifficulty(hash types.Hash, difficulty uint64) bool {
|
||||||
if difficulty == 0 {
|
if difficulty == 0 {
|
||||||
return true
|
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
|
// CheckPoWHash computes the RandomX hash of a block header hash + nonce
|
||||||
// and checks it against the difficulty target.
|
// and checks it against the difficulty target.
|
||||||
|
//
|
||||||
|
// valid, err := consensus.CheckPoWHash(headerHash, nonce, difficulty)
|
||||||
func CheckPoWHash(headerHash types.Hash, nonce, difficulty uint64) (bool, error) {
|
func CheckPoWHash(headerHash types.Hash, nonce, difficulty uint64) (bool, error) {
|
||||||
// Build input: header_hash (32 bytes) || nonce (8 bytes LE).
|
// Build input: header_hash (32 bytes) || nonce (8 bytes LE).
|
||||||
var input [40]byte
|
var input [40]byte
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ package consensus
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,19 @@
|
||||||
package consensus
|
package consensus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/bits"
|
"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.
|
// BaseReward returns the base block reward at the given height.
|
||||||
// Height 0 (genesis) returns the premine amount. All other heights
|
// Height 0 (genesis) returns the premine amount. All other heights
|
||||||
// return the fixed block reward (1 LTHN).
|
// return the fixed block reward (1 LTHN).
|
||||||
|
//
|
||||||
|
// reward := consensus.BaseReward(15000) // 1_000_000_000_000 (1 LTHN)
|
||||||
func BaseReward(height uint64) uint64 {
|
func BaseReward(height uint64) uint64 {
|
||||||
if height == 0 {
|
if height == 0 {
|
||||||
return config.Premine
|
return config.Premine
|
||||||
|
|
@ -32,6 +35,8 @@ func BaseReward(height uint64) uint64 {
|
||||||
// reward = baseReward * (2*median - size) * size / median²
|
// reward = baseReward * (2*median - size) * size / median²
|
||||||
//
|
//
|
||||||
// Uses math/bits.Mul64 for 128-bit intermediate products to avoid overflow.
|
// 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) {
|
func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
|
||||||
effectiveMedian := medianSize
|
effectiveMedian := medianSize
|
||||||
if effectiveMedian < config.BlockGrantedFullRewardZone {
|
if effectiveMedian < config.BlockGrantedFullRewardZone {
|
||||||
|
|
@ -43,7 +48,7 @@ func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if blockSize > 2*effectiveMedian {
|
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²
|
// 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:
|
// Since hi1 should be 0 for reasonable block sizes, simplify:
|
||||||
if hi1 > 0 {
|
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)
|
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
|
// MinerReward calculates the total miner payout. Pre-HF4, transaction
|
||||||
// fees are added to the base reward. Post-HF4 (postHF4=true), fees are
|
// fees are added to the base reward. Post-HF4 (postHF4=true), fees are
|
||||||
// burned and the miner receives only the base reward.
|
// 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 {
|
func MinerReward(baseReward, totalFees uint64, postHF4 bool) uint64 {
|
||||||
if postHF4 {
|
if postHF4 {
|
||||||
return baseReward
|
return baseReward
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ package consensus
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
171
consensus/tx.go
171
consensus/tx.go
|
|
@ -8,18 +8,44 @@ package consensus
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
|
||||||
|
"dappco.re/go/core/blockchain/config"
|
||||||
|
"dappco.re/go/core/blockchain/types"
|
||||||
|
"dappco.re/go/core/blockchain/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type transactionForkState struct {
|
||||||
|
activeHardForkVersion uint8
|
||||||
|
hardForkOneActive bool
|
||||||
|
hardForkFourActive bool
|
||||||
|
hardForkFiveActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTransactionForkState(forks []config.HardFork, height uint64) transactionForkState {
|
||||||
|
return transactionForkState{
|
||||||
|
activeHardForkVersion: config.VersionAtHeight(forks, height),
|
||||||
|
hardForkOneActive: config.IsHardForkActive(forks, config.HF1, height),
|
||||||
|
hardForkFourActive: config.IsHardForkActive(forks, config.HF4Zarcanum, height),
|
||||||
|
hardForkFiveActive: config.IsHardForkActive(forks, config.HF5, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateTransaction performs semantic validation on a regular (non-coinbase)
|
// ValidateTransaction performs semantic validation on a regular (non-coinbase)
|
||||||
// transaction. Checks are ordered to match the C++ validate_tx_semantic().
|
// 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 {
|
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.
|
// 1. Blob size.
|
||||||
if uint64(len(txBlob)) >= config.MaxTransactionBlobSize {
|
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.
|
// 2. Input count.
|
||||||
|
|
@ -27,16 +53,21 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
||||||
return ErrNoInputs
|
return ErrNoInputs
|
||||||
}
|
}
|
||||||
if uint64(len(tx.Vin)) > config.TxMaxAllowedInputs {
|
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.
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Output validation.
|
// 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
|
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).
|
// 7. Balance check (pre-HF4 only — post-HF4 uses commitment proofs).
|
||||||
if !hf4Active {
|
if !state.hardForkFourActive {
|
||||||
if _, err := TxFee(tx); err != nil {
|
if _, err := TxFee(tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -63,44 +94,99 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
||||||
return nil
|
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 {
|
for _, vin := range tx.Vin {
|
||||||
switch vin.(type) {
|
switch vin.(type) {
|
||||||
case types.TxInputToKey:
|
case types.TxInputToKey:
|
||||||
// Always valid.
|
// Always valid.
|
||||||
case types.TxInputGenesis:
|
case types.TxInputGenesis:
|
||||||
return fmt.Errorf("%w: txin_gen in regular transaction", ErrInvalidInputType)
|
return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType)
|
||||||
default:
|
case types.TxInputHTLC, types.TxInputMultisig:
|
||||||
// Future types (multisig, HTLC, ZC) — accept if HF4+.
|
// HTLC and multisig inputs require at least HF1.
|
||||||
if !hf4Active {
|
if !state.hardForkOneActive {
|
||||||
return fmt.Errorf("%w: tag %d pre-HF4", ErrInvalidInputType, vin.InputType())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOutputs(tx *types.Transaction, hf4Active bool) error {
|
func checkOutputs(tx *types.Transaction, state transactionForkState) error {
|
||||||
if len(tx.Vout) == 0 {
|
if len(tx.Vout) == 0 {
|
||||||
return ErrNoOutputs
|
return ErrNoOutputs
|
||||||
}
|
}
|
||||||
|
|
||||||
if hf4Active && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
|
if state.hardForkFourActive && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
|
||||||
return fmt.Errorf("%w: %d (min %d)", ErrTooFewOutputs, 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 {
|
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 {
|
for i, vout := range tx.Vout {
|
||||||
switch o := vout.(type) {
|
switch o := vout.(type) {
|
||||||
case types.TxOutputBare:
|
case types.TxOutputBare:
|
||||||
if o.Amount == 0 {
|
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:
|
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 {
|
func checkKeyImages(tx *types.Transaction) error {
|
||||||
seen := make(map[types.KeyImage]struct{})
|
seen := make(map[types.KeyImage]struct{})
|
||||||
for _, vin := range tx.Vin {
|
for _, vin := range tx.Vin {
|
||||||
toKey, ok := vin.(types.TxInputToKey)
|
var ki types.KeyImage
|
||||||
if !ok {
|
switch v := vin.(type) {
|
||||||
|
case types.TxInputToKey:
|
||||||
|
ki = v.KeyImage
|
||||||
|
case types.TxInputHTLC:
|
||||||
|
ki = v.KeyImage
|
||||||
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := seen[toKey.KeyImage]; exists {
|
if _, exists := seen[ki]; exists {
|
||||||
return fmt.Errorf("%w: %s", ErrDuplicateKeyImage, toKey.KeyImage)
|
return coreerr.E("checkKeyImages", ki.String(), ErrDuplicateKeyImage)
|
||||||
}
|
}
|
||||||
seen[toKey.KeyImage] = struct{}{}
|
seen[ki] = struct{}{}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkAssetOperations(extra []byte, hardForkFiveActive bool) error {
|
||||||
|
if len(extra) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
elements, err := wire.DecodeVariantVector(extra)
|
||||||
|
if err != nil {
|
||||||
|
return coreerr.E("checkAssetOperations", "parse extra", ErrInvalidExtra)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, elem := range elements {
|
||||||
|
if elem.Tag != types.AssetDescriptorOperationTag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !hardForkFiveActive {
|
||||||
|
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]: asset descriptor operation pre-HF5", i), ErrInvalidExtra)
|
||||||
|
}
|
||||||
|
|
||||||
|
op, err := wire.DecodeAssetDescriptorOperation(elem.Data)
|
||||||
|
if err != nil {
|
||||||
|
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]: decode asset descriptor operation", i), ErrInvalidExtra)
|
||||||
|
}
|
||||||
|
if err := op.Validate(); err != nil {
|
||||||
|
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]", i), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@
|
||||||
package consensus
|
package consensus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
|
"dappco.re/go/core/blockchain/wire"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestValidateTransaction_Good(t *testing.T) {
|
||||||
tx := validV1Tx()
|
tx := validV1Tx()
|
||||||
blob := make([]byte, 100) // small blob
|
blob := make([]byte, 100) // small blob
|
||||||
|
|
@ -133,3 +139,333 @@ func TestValidateTransaction_NegativeFee(t *testing.T) {
|
||||||
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
|
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
|
||||||
assert.ErrorIs(t, err, ErrNegativeFee)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
148
consensus/tx_version_test.go
Normal file
148
consensus/tx_version_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,10 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
|
||||||
|
"dappco.re/go/core/blockchain/types"
|
||||||
|
"dappco.re/go/core/blockchain/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// zcSigData holds the parsed components of a ZC_sig variant element
|
// 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))
|
dec := wire.NewDecoder(bytes.NewReader(raw))
|
||||||
count := dec.ReadVarint()
|
count := dec.ReadVarint()
|
||||||
if dec.Err() != nil {
|
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)
|
entries := make([]v2SigEntry, 0, count)
|
||||||
for i := uint64(0); i < count; i++ {
|
for i := uint64(0); i < count; i++ {
|
||||||
tag := dec.ReadUint8()
|
tag := dec.ReadUint8()
|
||||||
if dec.Err() != nil {
|
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}
|
entry := v2SigEntry{tag: tag}
|
||||||
|
|
@ -54,7 +56,7 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
|
||||||
case types.SigTypeZC:
|
case types.SigTypeZC:
|
||||||
zc, err := parseZCSig(dec)
|
zc, err := parseZCSig(dec)
|
||||||
if err != nil {
|
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
|
entry.zcSig = zc
|
||||||
|
|
||||||
|
|
@ -74,11 +76,11 @@ func parseV2Signatures(raw []byte) ([]v2SigEntry, error) {
|
||||||
skipZarcanumSig(dec)
|
skipZarcanumSig(dec)
|
||||||
|
|
||||||
default:
|
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 {
|
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)
|
entries = append(entries, entry)
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +119,7 @@ func parseZCSig(dec *wire.Decoder) (*zcSigData, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if rgCount != rxCount {
|
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)
|
zc.ringSize = int(rgCount)
|
||||||
|
|
||||||
|
|
@ -155,9 +157,9 @@ func skipZarcanumSig(dec *wire.Decoder) {
|
||||||
_ = dec.ReadBytes(32)
|
_ = dec.ReadBytes(32)
|
||||||
|
|
||||||
// CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32).
|
// CLSAG_GGXXG: c(32) + vec(r_g) + vec(r_x) + K1(32) + K2(32) + K3(32) + K4(32).
|
||||||
_ = dec.ReadBytes(32) // c
|
_ = dec.ReadBytes(32) // c
|
||||||
skipVecOfPoints(dec) // r_g
|
skipVecOfPoints(dec) // r_g
|
||||||
skipVecOfPoints(dec) // r_x
|
skipVecOfPoints(dec) // r_x
|
||||||
_ = dec.ReadBytes(128) // K1+K2+K3+K4
|
_ = dec.ReadBytes(128) // K1+K2+K3+K4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,48 +198,48 @@ func parseV2Proofs(raw []byte) (*v2ProofData, error) {
|
||||||
dec := wire.NewDecoder(bytes.NewReader(raw))
|
dec := wire.NewDecoder(bytes.NewReader(raw))
|
||||||
count := dec.ReadVarint()
|
count := dec.ReadVarint()
|
||||||
if dec.Err() != nil {
|
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
|
var data v2ProofData
|
||||||
for i := uint64(0); i < count; i++ {
|
for i := uint64(0); i < count; i++ {
|
||||||
tag := dec.ReadUint8()
|
tag := dec.ReadUint8()
|
||||||
if dec.Err() != nil {
|
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 {
|
switch tag {
|
||||||
case 46: // zc_asset_surjection_proof: varint(nBGE) + nBGE * BGE_proof
|
case 46: // zc_asset_surjection_proof: varint(nBGE) + nBGE * BGE_proof
|
||||||
nBGE := dec.ReadVarint()
|
nBGE := dec.ReadVarint()
|
||||||
if dec.Err() != nil {
|
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)
|
data.bgeProofs = make([][]byte, nBGE)
|
||||||
for j := uint64(0); j < nBGE; j++ {
|
for j := uint64(0); j < nBGE; j++ {
|
||||||
data.bgeProofs[j] = readBGEProofBytes(dec)
|
data.bgeProofs[j] = readBGEProofBytes(dec)
|
||||||
if dec.Err() != nil {
|
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
|
case 47: // zc_outs_range_proof: bpp_serialized + aggregation_proof
|
||||||
data.bppProofBytes = readBPPBytes(dec)
|
data.bppProofBytes = readBPPBytes(dec)
|
||||||
if dec.Err() != nil {
|
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)
|
data.bppCommitments = readAggregationCommitments(dec)
|
||||||
if dec.Err() != nil {
|
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)
|
case 48: // zc_balance_proof: 96 bytes (c, y0, y1)
|
||||||
data.balanceProof = dec.ReadBytes(96)
|
data.balanceProof = dec.ReadBytes(96)
|
||||||
if dec.Err() != nil {
|
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:
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,25 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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.
|
// loadTestTx loads and decodes a hex-encoded transaction from testdata.
|
||||||
func loadTestTx(t *testing.T, filename string) *types.Transaction {
|
func loadTestTx(t *testing.T, filename string) *types.Transaction {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
@ -147,6 +159,20 @@ func TestVerifyV2Signatures_BadSigCount(t *testing.T) {
|
||||||
assert.Error(t, err, "should fail with mismatched sig count")
|
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) {
|
func TestVerifyV2Signatures_TxHash(t *testing.T) {
|
||||||
// Verify the known tx hash matches.
|
// Verify the known tx hash matches.
|
||||||
tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex")
|
tx := loadTestTx(t, "../testdata/v2_spending_tx_mixin0.hex")
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,19 @@
|
||||||
package consensus
|
package consensus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"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
|
// RingOutputsFn fetches the public keys for a ring at the given spending
|
||||||
// and offsets. Used to decouple consensus/ from chain storage.
|
// height, amount, and offsets. Used to decouple consensus/ from chain storage.
|
||||||
type RingOutputsFn func(amount uint64, offsets []uint64) ([]types.PublicKey, error)
|
type RingOutputsFn func(height, amount uint64, offsets []uint64) ([]types.PublicKey, error)
|
||||||
|
|
||||||
// ZCRingMember holds the three public keys per ring entry needed for
|
// ZCRingMember holds the three public keys per ring entry needed for
|
||||||
// CLSAG GGX verification (HF4+). All fields are premultiplied by 1/8
|
// 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.
|
// getRingOutputs is used for pre-HF4 (V1) signature verification.
|
||||||
// getZCRingOutputs is used for post-HF4 (V2) CLSAG GGX verification.
|
// getZCRingOutputs is used for post-HF4 (V2) CLSAG GGX verification.
|
||||||
// Either may be nil for structural-only checks.
|
// 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,
|
func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork,
|
||||||
height uint64, getRingOutputs RingOutputsFn, getZCRingOutputs ZCRingOutputsFn) error {
|
height uint64, getRingOutputs RingOutputsFn, getZCRingOutputs ZCRingOutputsFn) error {
|
||||||
|
|
||||||
|
|
@ -48,28 +52,29 @@ func VerifyTransactionSignatures(tx *types.Transaction, forks []config.HardFork,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
hardForkFourActive := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||||
|
|
||||||
if !hf4Active {
|
if !hardForkFourActive {
|
||||||
return verifyV1Signatures(tx, getRingOutputs)
|
return verifyV1Signatures(tx, height, getRingOutputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyV2Signatures(tx, getZCRingOutputs)
|
return verifyV2Signatures(tx, getZCRingOutputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions.
|
// verifyV1Signatures checks NLSAG ring signatures for pre-HF4 transactions.
|
||||||
func verifyV1Signatures(tx *types.Transaction, getRingOutputs RingOutputsFn) error {
|
func verifyV1Signatures(tx *types.Transaction, height uint64, getRingOutputs RingOutputsFn) error {
|
||||||
// Count key inputs.
|
// Count ring-signing inputs (TxInputToKey and TxInputHTLC contribute
|
||||||
var keyInputCount int
|
// ring signatures; TxInputMultisig does not).
|
||||||
|
var ringInputCount int
|
||||||
for _, vin := range tx.Vin {
|
for _, vin := range tx.Vin {
|
||||||
if _, ok := vin.(types.TxInputToKey); ok {
|
switch vin.(type) {
|
||||||
keyInputCount++
|
case types.TxInputToKey, types.TxInputHTLC:
|
||||||
|
ringInputCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tx.Signatures) != keyInputCount {
|
if len(tx.Signatures) != ringInputCount {
|
||||||
return fmt.Errorf("consensus: signature count %d != input count %d",
|
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: signature count %d != input count %d", len(tx.Signatures), ringInputCount), nil)
|
||||||
len(tx.Signatures), keyInputCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actual NLSAG verification requires the crypto bridge and ring outputs.
|
// 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
|
var sigIdx int
|
||||||
for _, vin := range tx.Vin {
|
for _, vin := range tx.Vin {
|
||||||
inp, ok := vin.(types.TxInputToKey)
|
// Extract amount and key offsets from ring-signing input types.
|
||||||
if !ok {
|
var amount uint64
|
||||||
continue
|
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.
|
// Extract absolute global indices from key offsets.
|
||||||
offsets := make([]uint64, len(inp.KeyOffsets))
|
offsets := make([]uint64, len(keyOffsets))
|
||||||
for i, ref := range inp.KeyOffsets {
|
for i, ref := range keyOffsets {
|
||||||
offsets[i] = ref.GlobalIndex
|
offsets[i] = ref.GlobalIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
ringKeys, err := getRingOutputs(inp.Amount, offsets)
|
ringKeys, err := getRingOutputs(height, amount, offsets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("consensus: failed to fetch ring outputs for input %d: %w",
|
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: failed to fetch ring outputs for input %d", sigIdx), err)
|
||||||
sigIdx, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ringSigs := tx.Signatures[sigIdx]
|
ringSigs := tx.Signatures[sigIdx]
|
||||||
if len(ringSigs) != len(ringKeys) {
|
if len(ringSigs) != len(ringKeys) {
|
||||||
return fmt.Errorf("consensus: input %d has %d signatures but ring size %d",
|
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: input %d has %d signatures but ring size %d", sigIdx, len(ringSigs), len(ringKeys)), nil)
|
||||||
sigIdx, len(ringSigs), len(ringKeys))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert typed slices to raw byte arrays for the crypto bridge.
|
// 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)
|
sigs[i] = [64]byte(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(inp.KeyImage), pubs, sigs) {
|
if !crypto.CheckRingSignature([32]byte(prefixHash), [32]byte(keyImage), pubs, sigs) {
|
||||||
return fmt.Errorf("consensus: ring signature verification failed for input %d", sigIdx)
|
return coreerr.E("verifyV1Signatures", fmt.Sprintf("consensus: ring signature verification failed for input %d", sigIdx), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
sigIdx++
|
sigIdx++
|
||||||
|
|
@ -131,27 +147,29 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
|
||||||
// Parse the signature variant vector.
|
// Parse the signature variant vector.
|
||||||
sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
|
sigEntries, err := parseV2Signatures(tx.SignaturesRaw)
|
||||||
if err != nil {
|
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.
|
// Match signatures to inputs: each input must have a corresponding signature.
|
||||||
if len(sigEntries) != len(tx.Vin) {
|
if len(sigEntries) != len(tx.Vin) {
|
||||||
return fmt.Errorf("consensus: V2 signature count %d != input count %d",
|
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: V2 signature count %d != input count %d", len(sigEntries), len(tx.Vin)), nil)
|
||||||
len(sigEntries), len(tx.Vin))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
for i, vin := range tx.Vin {
|
||||||
switch vin.(type) {
|
switch vin.(type) {
|
||||||
case types.TxInputZC:
|
case types.TxInputZC:
|
||||||
if sigEntries[i].tag != types.SigTypeZC {
|
if sigEntries[i].tag != types.SigTypeZC {
|
||||||
return fmt.Errorf("consensus: input %d is ZC but signature tag is 0x%02x",
|
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is ZC but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
|
||||||
i, sigEntries[i].tag)
|
|
||||||
}
|
}
|
||||||
case types.TxInputToKey:
|
case types.TxInputToKey:
|
||||||
if sigEntries[i].tag != types.SigTypeNLSAG && sigEntries[i].tag != types.SigTypeVoid {
|
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",
|
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d is to_key but signature tag is 0x%02x", i, sigEntries[i].tag), nil)
|
||||||
i, sigEntries[i].tag)
|
}
|
||||||
|
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
|
zc := sigEntries[i].zcSig
|
||||||
if zc == nil {
|
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.
|
// Extract absolute global indices from key offsets.
|
||||||
|
|
@ -186,12 +204,11 @@ func verifyV2Signatures(tx *types.Transaction, getZCRingOutputs ZCRingOutputsFn)
|
||||||
|
|
||||||
ringMembers, err := getZCRingOutputs(offsets)
|
ringMembers, err := getZCRingOutputs(offsets)
|
||||||
if err != nil {
|
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 {
|
if len(ringMembers) != zc.ringSize {
|
||||||
return fmt.Errorf("consensus: input %d: ring size %d from chain != %d from sig",
|
return coreerr.E("verifyV2Signatures", fmt.Sprintf("consensus: input %d: ring size %d from chain != %d from sig", i, len(ringMembers), zc.ringSize), nil)
|
||||||
i, len(ringMembers), zc.ringSize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build flat ring: [stealth(32) | commitment(32) | blinded_asset_id(32)] per entry.
|
// 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),
|
[32]byte(zcIn.KeyImage),
|
||||||
zc.clsagFlatSig,
|
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.
|
// Parse and verify proofs.
|
||||||
proofs, err := parseV2Proofs(tx.Proofs)
|
proofs, err := parseV2Proofs(tx.Proofs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("consensus: %w", err)
|
return coreerr.E("verifyV2Signatures", "consensus", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify BPP range proof if present.
|
// Verify BPP range proof if present.
|
||||||
if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 {
|
if len(proofs.bppProofBytes) > 0 && len(proofs.bppCommitments) > 0 {
|
||||||
if !crypto.VerifyBPP(proofs.bppProofBytes, proofs.bppCommitments) {
|
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).
|
// Balance proofs are verified by the generic double-Schnorr helper in
|
||||||
// Requires computing commitment_to_zero and a new bridge function.
|
// consensus.VerifyBalanceProof once the transaction-specific public
|
||||||
|
// points have been constructed.
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -264,8 +282,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(proofs.bgeProofs) != len(outputAssetIDs) {
|
if len(proofs.bgeProofs) != len(outputAssetIDs) {
|
||||||
return fmt.Errorf("consensus: BGE proof count %d != Zarcanum output count %d",
|
return coreerr.E("verifyBGEProofs", fmt.Sprintf("consensus: BGE proof count %d != Zarcanum output count %d", len(proofs.bgeProofs), len(outputAssetIDs)), nil)
|
||||||
len(proofs.bgeProofs), len(outputAssetIDs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect pseudo-out asset IDs from ZC signatures and expand to full points.
|
// 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 {
|
for i, p := range pseudoOutAssetIDs {
|
||||||
full, err := crypto.PointMul8(p)
|
full, err := crypto.PointMul8(p)
|
||||||
if err != nil {
|
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
|
mul8PseudoOuts[i] = full
|
||||||
}
|
}
|
||||||
|
|
@ -292,7 +309,7 @@ func verifyBGEProofs(tx *types.Transaction, sigEntries []v2SigEntry,
|
||||||
// mul8 the output's blinded asset ID.
|
// mul8 the output's blinded asset ID.
|
||||||
mul8Out, err := crypto.PointMul8(outAssetID)
|
mul8Out, err := crypto.PointMul8(outAssetID)
|
||||||
if err != nil {
|
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)
|
// 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 {
|
for i, mul8Pseudo := range mul8PseudoOuts {
|
||||||
diff, err := crypto.PointSub(mul8Pseudo, mul8Out)
|
diff, err := crypto.PointSub(mul8Pseudo, mul8Out)
|
||||||
if err != nil {
|
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
|
ring[i] = diff
|
||||||
}
|
}
|
||||||
|
|
||||||
if !crypto.VerifyBGE(context, ring, proofs.bgeProofs[j]) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ package consensus
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
"dappco.re/go/core/blockchain/crypto"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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 = [][]types.Signature{make([]types.Signature, 1)}
|
||||||
tx.Signatures[0][0] = types.Signature(sigs[0])
|
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
|
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
|
return []types.PublicKey{types.PublicKey(pub)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ package consensus
|
||||||
import (
|
import (
|
||||||
"testing"
|
"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/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
@ -23,3 +24,72 @@ func TestVerifyTransactionSignatures_Bad_MissingSigs(t *testing.T) {
|
||||||
err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil, nil)
|
err := VerifyTransactionSignatures(tx, config.MainnetForks, 100, nil, nil)
|
||||||
assert.Error(t, err)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,6 @@ set(CXX_SOURCES
|
||||||
set(RANDOMX_SOURCES
|
set(RANDOMX_SOURCES
|
||||||
randomx/aes_hash.cpp
|
randomx/aes_hash.cpp
|
||||||
randomx/argon2_ref.c
|
randomx/argon2_ref.c
|
||||||
randomx/argon2_ssse3.c
|
|
||||||
randomx/argon2_avx2.c
|
|
||||||
randomx/bytecode_machine.cpp
|
randomx/bytecode_machine.cpp
|
||||||
randomx/cpu.cpp
|
randomx/cpu.cpp
|
||||||
randomx/dataset.cpp
|
randomx/dataset.cpp
|
||||||
|
|
@ -58,23 +56,47 @@ set(RANDOMX_SOURCES
|
||||||
randomx/virtual_memory.c
|
randomx/virtual_memory.c
|
||||||
randomx/vm_interpreted.cpp
|
randomx/vm_interpreted.cpp
|
||||||
randomx/allocator.cpp
|
randomx/allocator.cpp
|
||||||
randomx/assembly_generator_x86.cpp
|
|
||||||
randomx/instruction.cpp
|
randomx/instruction.cpp
|
||||||
randomx/randomx.cpp
|
randomx/randomx.cpp
|
||||||
randomx/superscalar.cpp
|
randomx/superscalar.cpp
|
||||||
randomx/vm_compiled.cpp
|
|
||||||
randomx/vm_interpreted_light.cpp
|
randomx/vm_interpreted_light.cpp
|
||||||
randomx/argon2_core.c
|
randomx/argon2_core.c
|
||||||
randomx/blake2_generator.cpp
|
randomx/blake2_generator.cpp
|
||||||
randomx/instructions_portable.cpp
|
randomx/instructions_portable.cpp
|
||||||
randomx/reciprocal.c
|
randomx/reciprocal.c
|
||||||
randomx/virtual_machine.cpp
|
randomx/virtual_machine.cpp
|
||||||
|
randomx/vm_compiled.cpp
|
||||||
randomx/vm_compiled_light.cpp
|
randomx/vm_compiled_light.cpp
|
||||||
randomx/blake2/blake2b.c
|
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})
|
add_library(randomx STATIC ${RANDOMX_SOURCES})
|
||||||
target_include_directories(randomx PRIVATE
|
target_include_directories(randomx PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/randomx
|
${CMAKE_CURRENT_SOURCE_DIR}/randomx
|
||||||
|
|
@ -85,15 +107,18 @@ set_property(TARGET randomx PROPERTY CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
# Platform-specific flags for RandomX
|
# Platform-specific flags for RandomX
|
||||||
enable_language(ASM)
|
enable_language(ASM)
|
||||||
target_compile_options(randomx PRIVATE -maes)
|
|
||||||
|
|
||||||
check_c_compiler_flag(-mssse3 HAVE_SSSE3)
|
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
|
||||||
if(HAVE_SSSE3)
|
target_compile_options(randomx PRIVATE -maes)
|
||||||
set_source_files_properties(randomx/argon2_ssse3.c PROPERTIES COMPILE_FLAGS -mssse3)
|
|
||||||
endif()
|
check_c_compiler_flag(-mssse3 HAVE_SSSE3)
|
||||||
check_c_compiler_flag(-mavx2 HAVE_AVX2)
|
if(HAVE_SSSE3)
|
||||||
if(HAVE_AVX2)
|
set_source_files_properties(randomx/argon2_ssse3.c PROPERTIES COMPILE_FLAGS -mssse3)
|
||||||
set_source_files_properties(randomx/argon2_avx2.c PROPERTIES COMPILE_FLAGS -mavx2)
|
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()
|
endif()
|
||||||
|
|
||||||
target_compile_options(randomx PRIVATE
|
target_compile_options(randomx PRIVATE
|
||||||
|
|
@ -106,7 +131,6 @@ target_compile_options(randomx PRIVATE
|
||||||
|
|
||||||
# --- Find system dependencies ---
|
# --- Find system dependencies ---
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
find_package(Boost REQUIRED)
|
|
||||||
|
|
||||||
# --- Static library ---
|
# --- Static library ---
|
||||||
add_library(cryptonote STATIC ${C_SOURCES} ${CXX_SOURCES})
|
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}/compat
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/randomx
|
${CMAKE_CURRENT_SOURCE_DIR}/randomx
|
||||||
${OPENSSL_INCLUDE_DIR}
|
${OPENSSL_INCLUDE_DIR}
|
||||||
${Boost_INCLUDE_DIRS}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(cryptonote PRIVATE
|
target_link_libraries(cryptonote PRIVATE
|
||||||
|
|
|
||||||
|
|
@ -104,37 +104,91 @@ bool deserialise_bpp(const uint8_t *buf, size_t len, crypto::bpp_signature &sig)
|
||||||
return off == len; // must consume all bytes
|
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).
|
// Deserialise a bppe_signature from wire bytes (Bulletproofs++ Enhanced, 2 deltas).
|
||||||
// Layout: varint(len(L)) + L[]*32 + varint(len(R)) + R[]*32
|
// 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)
|
// + 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) {
|
bool deserialise_bppe(const uint8_t *buf, size_t len, crypto::bppe_signature &sig) {
|
||||||
size_t off = 0;
|
size_t off = 0;
|
||||||
if (!read_pubkey_vec(buf, len, &off, sig.L)) return false;
|
if (!read_bppe_at(buf, len, &off, sig)) return false;
|
||||||
if (!read_pubkey_vec(buf, len, &off, sig.R)) return false;
|
|
||||||
if (!read_pubkey(buf, len, &off, sig.A0)) return false;
|
|
||||||
if (!read_pubkey(buf, len, &off, sig.A)) return false;
|
|
||||||
if (!read_pubkey(buf, len, &off, sig.B)) return false;
|
|
||||||
if (!read_scalar(buf, len, &off, sig.r)) return false;
|
|
||||||
if (!read_scalar(buf, len, &off, sig.s)) return false;
|
|
||||||
if (!read_scalar(buf, len, &off, sig.delta_1)) return false;
|
|
||||||
if (!read_scalar(buf, len, &off, sig.delta_2)) return false;
|
|
||||||
return off == len; // must consume all bytes
|
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.
|
// Deserialise a BGE_proof from wire bytes.
|
||||||
// Layout: A(32) + B(32) + varint(len(Pk)) + Pk[]*32
|
// Layout: A(32) + B(32) + varint(len(Pk)) + Pk[]*32
|
||||||
// + varint(len(f)) + f[]*32 + y(32) + z(32)
|
// + varint(len(f)) + f[]*32 + y(32) + z(32)
|
||||||
bool deserialise_bge(const uint8_t *buf, size_t len, crypto::BGE_proof &proof) {
|
bool deserialise_bge(const uint8_t *buf, size_t len, crypto::BGE_proof &proof) {
|
||||||
size_t off = 0;
|
size_t off = 0;
|
||||||
if (!read_pubkey(buf, len, &off, proof.A)) return false;
|
if (!read_bge_at(buf, len, &off, proof)) return false;
|
||||||
if (!read_pubkey(buf, len, &off, proof.B)) return false;
|
|
||||||
if (!read_pubkey_vec(buf, len, &off, proof.Pk)) return false;
|
|
||||||
if (!read_scalar_vec(buf, len, &off, proof.f)) return false;
|
|
||||||
if (!read_scalar(buf, len, &off, proof.y)) return false;
|
|
||||||
if (!read_scalar(buf, len, &off, proof.z)) return false;
|
|
||||||
return off == len;
|
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
|
} // anonymous namespace
|
||||||
|
|
||||||
extern "C" {
|
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 PoS ────────────────────────────────────────
|
||||||
// Zarcanum verification requires many parameters beyond what the current
|
// Compatibility wrapper for the historical proof-only API.
|
||||||
// bridge API exposes (kernel_hash, ring, last_pow_block_id, stake_ki,
|
|
||||||
// pos_difficulty). Returns -1 until the API is extended.
|
|
||||||
int cn_zarcanum_verify(const uint8_t /*hash*/[32], const uint8_t * /*proof*/,
|
int cn_zarcanum_verify(const uint8_t /*hash*/[32], const uint8_t * /*proof*/,
|
||||||
size_t /*proof_len*/) {
|
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 ──────────────────────────────────
|
// ── RandomX PoW Hashing ──────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -125,12 +125,42 @@ int cn_bppe_verify(const uint8_t *proof, size_t proof_len,
|
||||||
int cn_bge_verify(const uint8_t context[32], const uint8_t *ring,
|
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);
|
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 ──────────────────────────────────────────
|
// ── Zarcanum PoS ──────────────────────────────────────────
|
||||||
// TODO: extend API to accept kernel_hash, ring, last_pow_block_id,
|
// Legacy compatibility wrapper for the historical proof-only API.
|
||||||
// stake_ki, pos_difficulty. Currently returns -1 (not implemented).
|
|
||||||
int cn_zarcanum_verify(const uint8_t hash[32], const uint8_t *proof,
|
int cn_zarcanum_verify(const uint8_t hash[32], const uint8_t *proof,
|
||||||
size_t proof_len);
|
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 ──────────────────────────────────
|
// ── RandomX PoW Hashing ──────────────────────────────────
|
||||||
// key/key_size: RandomX cache key (e.g. "LetheanRandomXv1")
|
// key/key_size: RandomX cache key (e.g. "LetheanRandomXv1")
|
||||||
// input/input_size: block header hash (32 bytes) + nonce (8 bytes LE)
|
// input/input_size: block header hash (32 bytes) + nonce (8 bytes LE)
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ package crypto
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PointMul8 multiplies a curve point by the cofactor 8.
|
// 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])),
|
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
||||||
)
|
)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return result, errors.New("crypto: point_mul8 failed")
|
return result, coreerr.E("PointMul8", "point_mul8 failed", nil)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +35,7 @@ func PointDiv8(pk [32]byte) ([32]byte, error) {
|
||||||
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
||||||
)
|
)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return result, errors.New("crypto: point_div8 failed")
|
return result, coreerr.E("PointDiv8", "point_div8 failed", nil)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +49,7 @@ func PointSub(a, b [32]byte) ([32]byte, error) {
|
||||||
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
(*C.uint8_t)(unsafe.Pointer(&result[0])),
|
||||||
)
|
)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return result, errors.New("crypto: point_sub failed")
|
return result, coreerr.E("PointSub", "point_sub failed", nil)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +82,7 @@ func GenerateCLSAGGG(hash [32]byte, ring []byte, ringSize int,
|
||||||
(*C.uint8_t)(unsafe.Pointer(&sig[0])),
|
(*C.uint8_t)(unsafe.Pointer(&sig[0])),
|
||||||
)
|
)
|
||||||
if rc != 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
|
return sig, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
crypto/compat/boost/multiprecision/cpp_int.hpp
Normal file
67
crypto/compat/boost/multiprecision/cpp_int.hpp
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace boost {
|
||||||
|
namespace multiprecision {
|
||||||
|
|
||||||
|
using limb_type = std::uint64_t;
|
||||||
|
|
||||||
|
enum cpp_integer_type {
|
||||||
|
signed_magnitude,
|
||||||
|
unsigned_magnitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum cpp_int_check_type {
|
||||||
|
unchecked,
|
||||||
|
checked,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum expression_template_option {
|
||||||
|
et_off,
|
||||||
|
et_on,
|
||||||
|
};
|
||||||
|
|
||||||
|
template <unsigned MinBits = 0, unsigned MaxBits = 0,
|
||||||
|
cpp_integer_type SignType = signed_magnitude,
|
||||||
|
cpp_int_check_type Checked = unchecked,
|
||||||
|
class Allocator = void>
|
||||||
|
class cpp_int_backend {};
|
||||||
|
|
||||||
|
template <class Backend, expression_template_option ExpressionTemplates = et_off>
|
||||||
|
class number {
|
||||||
|
public:
|
||||||
|
number() = default;
|
||||||
|
number(unsigned long long) {}
|
||||||
|
|
||||||
|
class backend_type {
|
||||||
|
public:
|
||||||
|
std::size_t size() const { return 0; }
|
||||||
|
static constexpr std::size_t limb_bits = sizeof(limb_type) * 8;
|
||||||
|
|
||||||
|
limb_type *limbs() { return nullptr; }
|
||||||
|
const limb_type *limbs() const { return nullptr; }
|
||||||
|
|
||||||
|
void resize(unsigned, unsigned) {}
|
||||||
|
void normalize() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
backend_type &backend() { return backend_; }
|
||||||
|
const backend_type &backend() const { return backend_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
backend_type backend_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
using uint128_t = number<cpp_int_backend<128, 128, unsigned_magnitude, unchecked, void>>;
|
||||||
|
using uint256_t = number<cpp_int_backend<256, 256, unsigned_magnitude, unchecked, void>>;
|
||||||
|
using uint512_t = number<cpp_int_backend<512, 512, unsigned_magnitude, unchecked, void>>;
|
||||||
|
|
||||||
|
} // namespace multiprecision
|
||||||
|
} // namespace boost
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
"dappco.re/go/core/blockchain/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFastHash_Good_KnownVector(t *testing.T) {
|
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) {
|
func TestZarcanumCompatibilityWrapper_Bad_EmptyProof(t *testing.T) {
|
||||||
// Zarcanum bridge API needs extending — verify it returns false.
|
|
||||||
hash := [32]byte{0x01}
|
hash := [32]byte{0x01}
|
||||||
if crypto.VerifyZarcanum(hash, []byte{0x00}) {
|
if crypto.VerifyZarcanum(hash, []byte{0x00}) {
|
||||||
t.Fatal("Zarcanum stub should return false")
|
t.Fatal("compatibility wrapper should reject malformed proof data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZarcanumWithContext_Bad_MinimalProof(t *testing.T) {
|
||||||
|
var ctx crypto.ZarcanumVerificationContext
|
||||||
|
ctx.ContextHash = [32]byte{0x01}
|
||||||
|
ctx.KernelHash = [32]byte{0x02}
|
||||||
|
ctx.LastPowBlockIDHashed = [32]byte{0x03}
|
||||||
|
ctx.StakeKeyImage = [32]byte{0x04}
|
||||||
|
ctx.PosDifficulty = 1
|
||||||
|
ctx.Ring = []crypto.ZarcanumRingMember{{
|
||||||
|
StealthAddress: [32]byte{0x11},
|
||||||
|
AmountCommitment: [32]byte{0x22},
|
||||||
|
BlindedAssetID: [32]byte{0x33},
|
||||||
|
ConcealingPoint: [32]byte{0x44},
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Minimal structurally valid proof blob:
|
||||||
|
// 10 scalars/points + empty BPPE + pseudo_out_amount_commitment +
|
||||||
|
// CLSAG_GGXXG with one ring entry and zeroed scalars.
|
||||||
|
proof := make([]byte, 0, 10*32+2+32+2+32+1+128)
|
||||||
|
proof = append(proof, make([]byte, 10*32)...)
|
||||||
|
proof = append(proof, 0x00) // BPPE L length
|
||||||
|
proof = append(proof, 0x00) // BPPE R length
|
||||||
|
proof = append(proof, make([]byte, 7*32)...)
|
||||||
|
proof = append(proof, make([]byte, 32)...)
|
||||||
|
proof = append(proof, 0x01) // CLSAG_GGXXG r_g length
|
||||||
|
proof = append(proof, make([]byte, 32)...)
|
||||||
|
proof = append(proof, 0x01) // CLSAG_GGXXG r_x length
|
||||||
|
proof = append(proof, make([]byte, 32)...)
|
||||||
|
proof = append(proof, make([]byte, 128)...)
|
||||||
|
ctx.Proof = proof
|
||||||
|
|
||||||
|
if crypto.VerifyZarcanumWithContext(ctx) {
|
||||||
|
t.Fatal("minimal Zarcanum proof should fail verification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoubleSchnorr_Bad_EmptyProof(t *testing.T) {
|
||||||
|
var hash, a, b [32]byte
|
||||||
|
if crypto.VerifyDoubleSchnorr(hash, true, a, b, nil) {
|
||||||
|
t.Fatal("empty double-Schnorr proof should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoubleSchnorr_Good_Roundtrip(t *testing.T) {
|
||||||
|
hash := crypto.FastHash([]byte("double-schnorr"))
|
||||||
|
|
||||||
|
_, secretA, err := crypto.GenerateKeys()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateKeys(secretA): %v", err)
|
||||||
|
}
|
||||||
|
pubA, err := crypto.SecretToPublic(secretA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SecretToPublic(secretA): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, secretB, err := crypto.GenerateKeys()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateKeys(secretB): %v", err)
|
||||||
|
}
|
||||||
|
pubB, err := crypto.SecretToPublic(secretB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SecretToPublic(secretB): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proof, err := crypto.GenerateDoubleSchnorr(hash, false, secretA, secretB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateDoubleSchnorr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !crypto.VerifyDoubleSchnorr(hash, false, pubA, pubB, proof[:]) {
|
||||||
|
t.Fatal("generated double-Schnorr proof failed verification")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@ package crypto
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateKeys creates a new random key pair.
|
// 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])),
|
(*C.uint8_t)(unsafe.Pointer(&sec[0])),
|
||||||
)
|
)
|
||||||
if rc != 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
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +34,7 @@ func SecretToPublic(sec [32]byte) ([32]byte, error) {
|
||||||
(*C.uint8_t)(unsafe.Pointer(&pub[0])),
|
(*C.uint8_t)(unsafe.Pointer(&pub[0])),
|
||||||
)
|
)
|
||||||
if rc != 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
|
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])),
|
(*C.uint8_t)(unsafe.Pointer(&d[0])),
|
||||||
)
|
)
|
||||||
if rc != 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
|
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])),
|
(*C.uint8_t)(unsafe.Pointer(&derived[0])),
|
||||||
)
|
)
|
||||||
if rc != 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
|
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])),
|
(*C.uint8_t)(unsafe.Pointer(&derived[0])),
|
||||||
)
|
)
|
||||||
if rc != 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
|
return derived, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ package crypto
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateKeyImage computes the key image for a public/secret key pair.
|
// 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])),
|
(*C.uint8_t)(unsafe.Pointer(&ki[0])),
|
||||||
)
|
)
|
||||||
if rc != 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
|
return ki, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import "C"
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RandomXHash computes the RandomX PoW hash. The key is the cache
|
// 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])),
|
(*C.uint8_t)(unsafe.Pointer(&output[0])),
|
||||||
)
|
)
|
||||||
if ret != 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
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
125
crypto/proof.go
125
crypto/proof.go
|
|
@ -7,7 +7,59 @@ package crypto
|
||||||
*/
|
*/
|
||||||
import "C"
|
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).
|
// VerifyBPP verifies a Bulletproofs++ range proof (1 delta).
|
||||||
// Used for zc_outs_range_proof in post-HF4 transactions.
|
// 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
|
) == 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.
|
// VerifyZarcanum verifies a Zarcanum PoS proof.
|
||||||
// Currently returns false — bridge API needs extending to pass kernel_hash,
|
// This compatibility wrapper remains for the historical proof blob API.
|
||||||
// ring, last_pow_block_id, stake_ki, and pos_difficulty.
|
// Use VerifyZarcanumWithContext for full verification.
|
||||||
func VerifyZarcanum(hash [32]byte, proof []byte) bool {
|
func VerifyZarcanum(hash [32]byte, proof []byte) bool {
|
||||||
if len(proof) == 0 {
|
if len(proof) == 0 {
|
||||||
return false
|
return false
|
||||||
|
|
@ -87,3 +166,43 @@ func VerifyZarcanum(hash [32]byte, proof []byte) bool {
|
||||||
C.size_t(len(proof)),
|
C.size_t(len(proof)),
|
||||||
) == 0
|
) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyZarcanumWithContext verifies a Zarcanum PoS proof with the full
|
||||||
|
// consensus context required by the upstream verifier.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// crypto.VerifyZarcanumWithContext(crypto.ZarcanumVerificationContext{
|
||||||
|
// ContextHash: txHash,
|
||||||
|
// KernelHash: kernelHash,
|
||||||
|
// Ring: ring,
|
||||||
|
// LastPowBlockIDHashed: lastPowHash,
|
||||||
|
// StakeKeyImage: stakeKeyImage,
|
||||||
|
// PosDifficulty: posDifficulty,
|
||||||
|
// Proof: proofBlob,
|
||||||
|
// })
|
||||||
|
func VerifyZarcanumWithContext(ctx ZarcanumVerificationContext) bool {
|
||||||
|
if len(ctx.Ring) == 0 || len(ctx.Proof) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
flat := make([]byte, len(ctx.Ring)*128)
|
||||||
|
for i, member := range ctx.Ring {
|
||||||
|
copy(flat[i*128:], member.StealthAddress[:])
|
||||||
|
copy(flat[i*128+32:], member.AmountCommitment[:])
|
||||||
|
copy(flat[i*128+64:], member.BlindedAssetID[:])
|
||||||
|
copy(flat[i*128+96:], member.ConcealingPoint[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.cn_zarcanum_verify_full(
|
||||||
|
(*C.uint8_t)(unsafe.Pointer(&ctx.ContextHash[0])),
|
||||||
|
(*C.uint8_t)(unsafe.Pointer(&ctx.KernelHash[0])),
|
||||||
|
(*C.uint8_t)(unsafe.Pointer(&flat[0])),
|
||||||
|
C.size_t(len(ctx.Ring)),
|
||||||
|
(*C.uint8_t)(unsafe.Pointer(&ctx.LastPowBlockIDHashed[0])),
|
||||||
|
(*C.uint8_t)(unsafe.Pointer(&ctx.StakeKeyImage[0])),
|
||||||
|
C.uint64_t(ctx.PosDifficulty),
|
||||||
|
(*C.uint8_t)(unsafe.Pointer(&ctx.Proof[0])),
|
||||||
|
C.size_t(len(ctx.Proof)),
|
||||||
|
) == 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ package crypto
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateSignature creates a standard (non-ring) signature.
|
// 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])),
|
(*C.uint8_t)(unsafe.Pointer(&sig[0])),
|
||||||
)
|
)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return sig, errors.New("crypto: generate_signature failed")
|
return sig, coreerr.E("GenerateSignature", "generate_signature failed", nil)
|
||||||
}
|
}
|
||||||
return sig, 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])),
|
(*C.uint8_t)(unsafe.Pointer(&flatSigs[0])),
|
||||||
)
|
)
|
||||||
if rc != 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)
|
sigs := make([][64]byte, n)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
//
|
//
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <stdexcept>
|
||||||
#include <boost/multiprecision/cpp_int.hpp>
|
#include <boost/multiprecision/cpp_int.hpp>
|
||||||
#include "crypto.h"
|
#include "crypto.h"
|
||||||
#include "eth_signature.h"
|
#include "eth_signature.h"
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ var StarterDifficulty = big.NewInt(1)
|
||||||
//
|
//
|
||||||
// where each solve-time interval i is weighted by its position (1..n),
|
// where each solve-time interval i is weighted by its position (1..n),
|
||||||
// giving more influence to recent blocks.
|
// giving more influence to recent blocks.
|
||||||
|
//
|
||||||
|
// nextDiff := difficulty.NextDifficulty(timestamps, cumulativeDiffs, 120)
|
||||||
func NextDifficulty(timestamps []uint64, cumulativeDiffs []*big.Int, target uint64) *big.Int {
|
func NextDifficulty(timestamps []uint64, cumulativeDiffs []*big.Int, target uint64) *big.Int {
|
||||||
// Need at least 2 entries to compute one solve-time interval.
|
// Need at least 2 entries to compute one solve-time interval.
|
||||||
if len(timestamps) < 2 || len(cumulativeDiffs) < 2 {
|
if len(timestamps) < 2 || len(cumulativeDiffs) < 2 {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNextDifficulty_Good(t *testing.T) {
|
func TestNextDifficulty_Good(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ Do not use American spellings in identifiers, comments, or documentation.
|
||||||
- Error wrapping uses `fmt.Errorf("types: description: %w", err)`
|
- Error wrapping uses `fmt.Errorf("types: description: %w", err)`
|
||||||
- Every source file carries the EUPL-1.2 copyright header
|
- Every source file carries the EUPL-1.2 copyright header
|
||||||
- No emojis in code or comments
|
- 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
|
separated by a blank line
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ description: Pure Go implementation of the Lethean CryptoNote/Zano-fork blockcha
|
||||||
|
|
||||||
`go-blockchain` is a Go reimplementation of the Lethean blockchain protocol. It provides pure-Go implementations of chain logic, data structures, consensus rules, wallet operations, and networking, delegating only mathematically complex cryptographic operations (ring signatures, Bulletproofs+, Zarcanum proofs) to a cleaned C++ library via CGo.
|
`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)
|
**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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/rpc"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Query the daemon
|
// Query the daemon
|
||||||
|
|
@ -109,7 +109,7 @@ When CGo is disabled, stub implementations return errors, allowing the rest of t
|
||||||
|
|
||||||
## Development Phases
|
## 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 |
|
| Phase | Scope | Status |
|
||||||
|-------|-------|--------|
|
|-------|-------|--------|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ The Lethean node exposes two RPC interfaces: a **daemon** API for blockchain que
|
||||||
The `rpc/` package provides a typed Go client:
|
The `rpc/` package provides a typed Go client:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "forge.lthn.ai/core/go-blockchain/rpc"
|
import "dappco.re/go/core/blockchain/rpc"
|
||||||
|
|
||||||
// Create a client (appends /json_rpc automatically)
|
// Create a client (appends /json_rpc automatically)
|
||||||
client := rpc.NewClient("http://localhost:36941")
|
client := rpc.NewClient("http://localhost:36941")
|
||||||
|
|
|
||||||
1983
docs/superpowers/plans/2026-03-16-hf1-transaction-types.md
Normal file
1983
docs/superpowers/plans/2026-03-16-hf1-transaction-types.md
Normal file
File diff suppressed because it is too large
Load diff
316
docs/superpowers/plans/2026-03-16-hf3-block-version.md
Normal file
316
docs/superpowers/plans/2026-03-16-hf3-block-version.md
Normal 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.
|
||||||
1539
docs/superpowers/plans/2026-03-16-hf5-confidential-assets.md
Normal file
1539
docs/superpowers/plans/2026-03-16-hf5-confidential-assets.md
Normal file
File diff suppressed because it is too large
Load diff
398
docs/superpowers/plans/2026-03-16-hf6-block-time-halving.md
Normal file
398
docs/superpowers/plans/2026-03-16-hf6-block-time-halving.md
Normal 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>"
|
||||||
|
```
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
87
explorer_command.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package blockchain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
corelog "dappco.re/go/core/log"
|
||||||
|
|
||||||
|
cli "dappco.re/go/core/cli/pkg/cli"
|
||||||
|
store "dappco.re/go/core/store"
|
||||||
|
|
||||||
|
"dappco.re/go/core/blockchain/chain"
|
||||||
|
"dappco.re/go/core/blockchain/tui"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newChainExplorerCommand builds the interactive `chain explorer` command.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// chain explorer --data-dir ~/.lethean/chain
|
||||||
|
//
|
||||||
|
// Use it alongside `AddChainCommands` to expose the TUI node view.
|
||||||
|
func newChainExplorerCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "explorer",
|
||||||
|
Short: "TUI block explorer",
|
||||||
|
Long: "Interactive terminal block explorer with live sync status.",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return validateChainOptions(*chainDataDir, *seedPeerAddress)
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runChainExplorer(*chainDataDir, *seedPeerAddress, *useTestnet)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runChainExplorer(chainDataDir, seedPeerAddress string, useTestnet bool) error {
|
||||||
|
if err := ensureChainDataDirExists(chainDataDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(chainDataDir, "chain.db")
|
||||||
|
chainStore, err := store.New(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return corelog.E("runChainExplorer", "open store", err)
|
||||||
|
}
|
||||||
|
defer chainStore.Close()
|
||||||
|
|
||||||
|
blockchain := chain.New(chainStore)
|
||||||
|
chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress)
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed)
|
||||||
|
}()
|
||||||
|
|
||||||
|
node := tui.NewNode(blockchain)
|
||||||
|
status := tui.NewStatusModel(node)
|
||||||
|
explorer := tui.NewExplorerModel(blockchain)
|
||||||
|
hints := tui.NewKeyHintsModel()
|
||||||
|
|
||||||
|
frame := cli.NewFrame("HCF")
|
||||||
|
frame.Header(status)
|
||||||
|
frame.Content(explorer)
|
||||||
|
frame.Footer(hints)
|
||||||
|
corelog.Info("running chain explorer", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet)
|
||||||
|
frame.Run()
|
||||||
|
|
||||||
|
cancel() // Signal the sync loop to stop.
|
||||||
|
wg.Wait() // Wait for it before closing store.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
66
go.mod
66
go.mod
|
|
@ -1,30 +1,36 @@
|
||||||
module forge.lthn.ai/core/go-blockchain
|
module dappco.re/go/core/blockchain
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
forge.lthn.ai/core/cli v0.1.0
|
dappco.re/go/core/cli v0.3.1
|
||||||
forge.lthn.ai/core/go-p2p v0.0.0-00010101000000-000000000000
|
dappco.re/go/core/io v0.2.0
|
||||||
forge.lthn.ai/core/go-process v0.1.2
|
dappco.re/go/core/log v0.1.0
|
||||||
forge.lthn.ai/core/go-store v0.1.3
|
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/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.49.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
forge.lthn.ai/core/go v0.1.0 // indirect
|
forge.lthn.ai/core/go v0.3.1 // indirect
|
||||||
forge.lthn.ai/core/go-crypt v0.1.0 // indirect
|
forge.lthn.ai/core/go-crypt v0.1.6 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // 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/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/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // 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/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.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/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // 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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
|
@ -46,25 +51,26 @@ require (
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/term v0.40.0 // indirect
|
golang.org/x/term v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // 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 (
|
||||||
|
dappco.re/go/core => forge.lthn.ai/core/go v0.5.0
|
||||||
replace forge.lthn.ai/core/go => /Users/snider/Code/host-uk/core
|
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
|
||||||
replace forge.lthn.ai/core/go-crypt => /Users/snider/Code/core/go-crypt
|
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
|
||||||
replace forge.lthn.ai/core/go-p2p => /Users/snider/Code/core/go-p2p
|
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
|
||||||
replace forge.lthn.ai/core/go-process => /Users/snider/Code/core/go-process
|
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
|
||||||
replace forge.lthn.ai/core/go-store => /Users/snider/Code/core/go-store
|
dappco.re/go/core/store => forge.lthn.ai/core/go-store v0.1.6
|
||||||
|
)
|
||||||
|
|
|
||||||
111
go.sum
111
go.sum
|
|
@ -1,36 +1,52 @@
|
||||||
forge.lthn.ai/core/go-process v0.1.2 h1:0fdLJq/DPssilN9E5yude/xHNfZRKHghIjo++b5aXgc=
|
forge.lthn.ai/core/cli v0.3.1 h1:ZpHhaDrdbaV98JDxj/f0E5nytYk9tTMRu3qohGyK4M0=
|
||||||
forge.lthn.ai/core/go-process v0.1.2/go.mod h1:9oxVALrZaZCqFe8YDdheIS5bRUV1SBz4tVW/MflAtxM=
|
forge.lthn.ai/core/cli v0.3.1/go.mod h1:28cOl9eK0H033Otkjrv9f/QCmtHcJl+IIx4om8JskOg=
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM=
|
||||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
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 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
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.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
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/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.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
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.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
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 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
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.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
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/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
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/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
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 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
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/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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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=
|
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-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 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
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.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-runewidth v0.0.20/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 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
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=
|
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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
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=
|
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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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=
|
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 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
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.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
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.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
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 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
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.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
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/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ package mining
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
"dappco.re/go/core/blockchain/consensus"
|
||||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
"dappco.re/go/core/blockchain/crypto"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RandomXKey is the cache initialisation key for RandomX hashing.
|
// RandomXKey is the cache initialisation key for RandomX hashing.
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testnetGenesisHeader() types.BlockHeader {
|
func testnetGenesisHeader() types.BlockHeader {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/rpc"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/consensus"
|
coreerr "dappco.re/go/core/log"
|
||||||
"forge.lthn.ai/core/go-blockchain/crypto"
|
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/consensus"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/crypto"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplateProvider abstracts the RPC methods needed by the miner.
|
// TemplateProvider abstracts the RPC methods needed by the miner.
|
||||||
|
|
@ -139,18 +141,18 @@ func (m *Miner) Start(ctx context.Context) error {
|
||||||
// Parse difficulty.
|
// Parse difficulty.
|
||||||
diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64)
|
diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64)
|
||||||
if err != nil {
|
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.
|
// Decode the block template blob.
|
||||||
blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob)
|
blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob)
|
||||||
if err != nil {
|
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))
|
dec := wire.NewDecoder(bytes.NewReader(blobBytes))
|
||||||
block := wire.DecodeBlock(dec)
|
block := wire.DecodeBlock(dec)
|
||||||
if dec.Err() != nil {
|
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.
|
// 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[:])
|
powHash, err := crypto.RandomXHash(RandomXKey, input[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("mining: RandomX hash: %w", err)
|
return coreerr.E("Miner.mine", "mining: RandomX hash", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.hashCount.Add(1)
|
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)
|
enc := wire.NewEncoder(&buf)
|
||||||
wire.EncodeBlock(enc, block)
|
wire.EncodeBlock(enc, block)
|
||||||
if enc.Err() != nil {
|
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())
|
hexBlob := hex.EncodeToString(buf.Bytes())
|
||||||
if err := m.provider.SubmitBlock(hexBlob); err != nil {
|
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)
|
m.blocksFound.Add(1)
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/rpc"
|
"dappco.re/go/core/blockchain/rpc"
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-blockchain/wire"
|
"dappco.re/go/core/blockchain/wire"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
// Package p2p implements the CryptoNote P2P protocol for the Lethean blockchain.
|
// Package p2p implements the CryptoNote P2P protocol for the Lethean blockchain.
|
||||||
package p2p
|
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.
|
// Re-export command IDs from the levin package for convenience.
|
||||||
const (
|
const (
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ package p2p
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"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).
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateHandshakeResponse verifies that a remote peer's handshake response
|
||||||
|
// matches the expected network and satisfies the minimum build version gate.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// err := ValidateHandshakeResponse(&resp, config.NetworkIDMainnet, false)
|
||||||
|
func ValidateHandshakeResponse(resp *HandshakeResponse, expectedNetworkID [16]byte, isTestnet bool) error {
|
||||||
|
if resp.NodeData.NetworkID != expectedNetworkID {
|
||||||
|
return fmt.Errorf("p2p: peer network id %x does not match expected %x",
|
||||||
|
resp.NodeData.NetworkID, expectedNetworkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildVersion, ok := PeerBuildVersion(resp.PayloadData.ClientVersion)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("p2p: peer build %q is malformed", resp.PayloadData.ClientVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !MeetsMinimumBuildVersion(resp.PayloadData.ClientVersion, isTestnet) {
|
||||||
|
minBuild := MinimumRequiredBuildVersion(isTestnet)
|
||||||
|
return fmt.Errorf("p2p: peer build %q parsed as %d below minimum %d",
|
||||||
|
resp.PayloadData.ClientVersion, buildVersion, minBuild)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@ package p2p
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
"dappco.re/go/core/p2p/node/levin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEncodeHandshakeRequest_Good_Roundtrip(t *testing.T) {
|
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))
|
t.Errorf("empty peerlist: got %d entries, want 0", len(entries))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateHandshakeResponse_Good(t *testing.T) {
|
||||||
|
resp := &HandshakeResponse{
|
||||||
|
NodeData: NodeData{
|
||||||
|
NetworkID: config.NetworkIDTestnet,
|
||||||
|
},
|
||||||
|
PayloadData: CoreSyncData{
|
||||||
|
ClientVersion: "6.0.1.2[go-blockchain]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true); err != nil {
|
||||||
|
t.Fatalf("ValidateHandshakeResponse: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateHandshakeResponse_BadNetwork(t *testing.T) {
|
||||||
|
resp := &HandshakeResponse{
|
||||||
|
NodeData: NodeData{
|
||||||
|
NetworkID: config.NetworkIDMainnet,
|
||||||
|
},
|
||||||
|
PayloadData: CoreSyncData{
|
||||||
|
ClientVersion: "6.0.1.2[go-blockchain]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ValidateHandshakeResponse: expected network mismatch error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "network id") {
|
||||||
|
t.Fatalf("ValidateHandshakeResponse error: got %v, want network id mismatch", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateHandshakeResponse_BadBuildVersion(t *testing.T) {
|
||||||
|
resp := &HandshakeResponse{
|
||||||
|
NodeData: NodeData{
|
||||||
|
NetworkID: config.NetworkIDMainnet,
|
||||||
|
},
|
||||||
|
PayloadData: CoreSyncData{
|
||||||
|
ClientVersion: "0.0.1.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ValidateHandshakeResponse(resp, config.NetworkIDMainnet, false)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ValidateHandshakeResponse: expected build version error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "below minimum") {
|
||||||
|
t.Fatalf("ValidateHandshakeResponse error: got %v, want build minimum failure", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateHandshakeResponse_BadMalformedBuildVersion(t *testing.T) {
|
||||||
|
resp := &HandshakeResponse{
|
||||||
|
NodeData: NodeData{
|
||||||
|
NetworkID: config.NetworkIDMainnet,
|
||||||
|
},
|
||||||
|
PayloadData: CoreSyncData{
|
||||||
|
ClientVersion: "bogus",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ValidateHandshakeResponse(resp, config.NetworkIDMainnet, false)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ValidateHandshakeResponse: expected malformed build version error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "malformed") {
|
||||||
|
t.Fatalf("ValidateHandshakeResponse error: got %v, want malformed build version failure", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/config"
|
"dappco.re/go/core/blockchain/config"
|
||||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
"dappco.re/go/core/p2p/node/levin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
package p2p
|
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.
|
// EncodePingRequest returns an encoded empty ping request payload.
|
||||||
func EncodePingRequest() ([]byte, error) {
|
func EncodePingRequest() ([]byte, error) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ package p2p
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
"dappco.re/go/core/p2p/node/levin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEncodePingRequest_Good_EmptySection(t *testing.T) {
|
func TestEncodePingRequest_Good_EmptySection(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
package p2p
|
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).
|
// NewBlockNotification is NOTIFY_NEW_BLOCK (2001).
|
||||||
type NewBlockNotification struct {
|
type NewBlockNotification struct {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
"dappco.re/go/core/p2p/node/levin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewBlockNotification_Good_Roundtrip(t *testing.T) {
|
func TestNewBlockNotification_Good_Roundtrip(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
package p2p
|
package p2p
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
"dappco.re/go/core/p2p/node/levin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CoreSyncData is the blockchain state exchanged during handshake and timed sync.
|
// 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),
|
"current_height": levin.Uint64Val(d.CurrentHeight),
|
||||||
"top_id": levin.StringVal(d.TopID[:]),
|
"top_id": levin.StringVal(d.TopID[:]),
|
||||||
"last_checkpoint_height": levin.Uint64Val(d.LastCheckpointHeight),
|
"last_checkpoint_height": levin.Uint64Val(d.LastCheckpointHeight),
|
||||||
"core_time": levin.Uint64Val(d.CoreTime),
|
"core_time": levin.Uint64Val(d.CoreTime),
|
||||||
"client_version": levin.StringVal([]byte(d.ClientVersion)),
|
"client_version": levin.StringVal([]byte(d.ClientVersion)),
|
||||||
"non_pruning_mode_enabled": levin.BoolVal(d.NonPruningMode),
|
"non_pruning_mode_enabled": levin.BoolVal(d.NonPruningMode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ package p2p
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.lthn.ai/core/go-blockchain/types"
|
"dappco.re/go/core/blockchain/types"
|
||||||
"forge.lthn.ai/core/go-p2p/node/levin"
|
"dappco.re/go/core/p2p/node/levin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCoreSyncData_Good_Roundtrip(t *testing.T) {
|
func TestCoreSyncData_Good_Roundtrip(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
package p2p
|
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.
|
// TimedSyncRequest is a COMMAND_TIMED_SYNC request.
|
||||||
type TimedSyncRequest struct {
|
type TimedSyncRequest struct {
|
||||||
|
|
|
||||||
86
p2p/version.go
Normal file
86
p2p/version.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package p2p
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MinimumRequiredBuildVersionMainnet matches the C++ daemon's mainnet gate.
|
||||||
|
MinimumRequiredBuildVersionMainnet uint64 = 601
|
||||||
|
|
||||||
|
// MinimumRequiredBuildVersionTestnet matches the C++ daemon's testnet gate.
|
||||||
|
MinimumRequiredBuildVersionTestnet uint64 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// MinimumRequiredBuildVersion returns the minimum accepted peer version gate
|
||||||
|
// for the given network.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// MinimumRequiredBuildVersion(false) // 601 on mainnet
|
||||||
|
func MinimumRequiredBuildVersion(isTestnet bool) uint64 {
|
||||||
|
if isTestnet {
|
||||||
|
return MinimumRequiredBuildVersionTestnet
|
||||||
|
}
|
||||||
|
return MinimumRequiredBuildVersionMainnet
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerBuildVersion extracts the numeric major.minor.revision component from a
|
||||||
|
// daemon client version string.
|
||||||
|
//
|
||||||
|
// The daemon formats its version as "major.minor.revision.build[extra]".
|
||||||
|
// The minimum build gate compares the first three components, so
|
||||||
|
// "6.0.1.2[go-blockchain]" becomes 601.
|
||||||
|
func PeerBuildVersion(clientVersion string) (uint64, bool) {
|
||||||
|
parts := strings.SplitN(clientVersion, ".", 4)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.ParseUint(parts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
minor, err := strconv.ParseUint(parts[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
revPart := parts[2]
|
||||||
|
for i := 0; i < len(revPart); i++ {
|
||||||
|
if revPart[i] < '0' || revPart[i] > '9' {
|
||||||
|
revPart = revPart[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if revPart == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
revision, err := strconv.ParseUint(revPart, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return major*100 + minor*10 + revision, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeetsMinimumBuildVersion reports whether the peer's version is acceptable
|
||||||
|
// for the given network.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", false) // true
|
||||||
|
func MeetsMinimumBuildVersion(clientVersion string, isTestnet bool) bool {
|
||||||
|
buildVersion, ok := PeerBuildVersion(clientVersion)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return buildVersion >= MinimumRequiredBuildVersion(isTestnet)
|
||||||
|
}
|
||||||
71
p2p/version_test.go
Normal file
71
p2p/version_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package p2p
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPeerBuildVersion_Good(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want uint64
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{"release", "6.0.1.2[go-blockchain]", 601, true},
|
||||||
|
{"two_digits", "12.3.4.5", 1234, true},
|
||||||
|
{"suffix", "6.0.1-beta.2", 601, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, ok := PeerBuildVersion(tt.input)
|
||||||
|
if ok != tt.wantOK {
|
||||||
|
t.Fatalf("PeerBuildVersion(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("PeerBuildVersion(%q) = %d, want %d", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeerBuildVersion_Bad(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"",
|
||||||
|
"6",
|
||||||
|
"6.0",
|
||||||
|
"abc.def.ghi",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range tests {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
if got, ok := PeerBuildVersion(input); ok || got != 0 {
|
||||||
|
t.Fatalf("PeerBuildVersion(%q) = (%d, %v), want (0, false)", input, got, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMeetsMinimumBuildVersion_Good(t *testing.T) {
|
||||||
|
if !MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", false) {
|
||||||
|
t.Fatal("expected mainnet build version to satisfy minimum")
|
||||||
|
}
|
||||||
|
if !MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", true) {
|
||||||
|
t.Fatal("expected testnet build version to satisfy minimum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMeetsMinimumBuildVersion_Bad(t *testing.T) {
|
||||||
|
if MeetsMinimumBuildVersion("0.0.1.0", false) {
|
||||||
|
t.Fatal("expected low mainnet build version to fail")
|
||||||
|
}
|
||||||
|
if MeetsMinimumBuildVersion("0.0.1.0", true) {
|
||||||
|
t.Fatal("expected low testnet build version to fail")
|
||||||
|
}
|
||||||
|
if MeetsMinimumBuildVersion("bogus", false) {
|
||||||
|
t.Fatal("expected malformed version to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,19 +5,23 @@
|
||||||
|
|
||||||
package rpc
|
package rpc
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
|
)
|
||||||
|
|
||||||
// GetLastBlockHeader returns the header of the most recent block.
|
// GetLastBlockHeader returns the header of the most recent block.
|
||||||
func (c *Client) GetLastBlockHeader() (*BlockHeader, error) {
|
func (c *Client) GetLastBlockHeader() (*BlockHeader, error) {
|
||||||
var resp struct {
|
var resp struct {
|
||||||
BlockHeader BlockHeader `json:"block_header"`
|
BlockHeader BlockHeader `json:"block_header"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
if err := c.call("getlastblockheader", struct{}{}, &resp); err != nil {
|
if err := c.call("getlastblockheader", struct{}{}, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Status != "OK" {
|
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
|
return &resp.BlockHeader, nil
|
||||||
}
|
}
|
||||||
|
|
@ -29,13 +33,13 @@ func (c *Client) GetBlockHeaderByHeight(height uint64) (*BlockHeader, error) {
|
||||||
}{Height: height}
|
}{Height: height}
|
||||||
var resp struct {
|
var resp struct {
|
||||||
BlockHeader BlockHeader `json:"block_header"`
|
BlockHeader BlockHeader `json:"block_header"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
if err := c.call("getblockheaderbyheight", params, &resp); err != nil {
|
if err := c.call("getblockheaderbyheight", params, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Status != "OK" {
|
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
|
return &resp.BlockHeader, nil
|
||||||
}
|
}
|
||||||
|
|
@ -47,13 +51,13 @@ func (c *Client) GetBlockHeaderByHash(hash string) (*BlockHeader, error) {
|
||||||
}{Hash: hash}
|
}{Hash: hash}
|
||||||
var resp struct {
|
var resp struct {
|
||||||
BlockHeader BlockHeader `json:"block_header"`
|
BlockHeader BlockHeader `json:"block_header"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
if err := c.call("getblockheaderbyhash", params, &resp); err != nil {
|
if err := c.call("getblockheaderbyhash", params, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Status != "OK" {
|
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
|
return &resp.BlockHeader, nil
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +77,7 @@ func (c *Client) GetBlocksDetails(heightStart, count uint64) ([]BlockDetails, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Status != "OK" {
|
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
|
return resp.Blocks, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is a Lethean daemon RPC client.
|
// Client is a Lethean daemon RPC client.
|
||||||
|
|
@ -66,10 +68,10 @@ type jsonRPCRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonRPCResponse struct {
|
type jsonRPCResponse struct {
|
||||||
JSONRPC string `json:"jsonrpc"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
ID json.RawMessage `json:"id"`
|
ID json.RawMessage `json:"id"`
|
||||||
Result json.RawMessage `json:"result"`
|
Result json.RawMessage `json:"result"`
|
||||||
Error *jsonRPCError `json:"error,omitempty"`
|
Error *jsonRPCError `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonRPCError struct {
|
type jsonRPCError struct {
|
||||||
|
|
@ -86,27 +88,27 @@ func (c *Client) call(method string, params any, result any) error {
|
||||||
Params: params,
|
Params: params,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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))
|
resp, err := c.httpClient.Post(c.url, "application/json", bytes.NewReader(reqBody))
|
||||||
if err != nil {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read response: %w", err)
|
return coreerr.E("Client.call", "read response", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rpcResp jsonRPCResponse
|
var rpcResp jsonRPCResponse
|
||||||
if err := json.Unmarshal(body, &rpcResp); err != nil {
|
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 {
|
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 result != nil && len(rpcResp.Result) > 0 {
|
||||||
if err := json.Unmarshal(rpcResp.Result, result); err != nil {
|
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
|
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 {
|
func (c *Client) legacyCall(path string, params any, result any) error {
|
||||||
reqBody, err := json.Marshal(params)
|
reqBody, err := json.Marshal(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal request: %w", err)
|
return coreerr.E("Client.legacyCall", "marshal request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
url := c.baseURL + path
|
url := c.baseURL + path
|
||||||
resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(reqBody))
|
resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(reqBody))
|
||||||
if err != nil {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read response: %w", err)
|
return coreerr.E("Client.legacyCall", "read response", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result != nil {
|
if result != nil {
|
||||||
if err := json.Unmarshal(body, result); err != 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
|
return nil
|
||||||
|
|
|
||||||
12
rpc/info.go
12
rpc/info.go
|
|
@ -5,7 +5,11 @@
|
||||||
|
|
||||||
package rpc
|
package rpc
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
coreerr "dappco.re/go/core/log"
|
||||||
|
)
|
||||||
|
|
||||||
// GetInfo returns the daemon status.
|
// GetInfo returns the daemon status.
|
||||||
// Uses flags=0 for the cheapest query (no expensive calculations).
|
// Uses flags=0 for the cheapest query (no expensive calculations).
|
||||||
|
|
@ -21,7 +25,7 @@ func (c *Client) GetInfo() (*DaemonInfo, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Status != "OK" {
|
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
|
return &resp.DaemonInfo, nil
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +41,7 @@ func (c *Client) GetHeight() (uint64, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if resp.Status != "OK" {
|
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
|
return resp.Height, nil
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +56,7 @@ func (c *Client) GetBlockCount() (uint64, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if resp.Status != "OK" {
|
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
|
return resp.Count, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue