feat: Phase 0 scaffold -- config, types, wire, difficulty

Chain parameters from Lethean C++ source. Base58 address encoding with
Keccak-256 checksum. CryptoNote varint. LWMA difficulty skeleton.

Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
Claude 2026-02-20 15:10:33 +00:00
commit 4c0b7f290e
No known key found for this signature in database
GPG key ID: AF404715446AEB41
16 changed files with 2158 additions and 0 deletions

46
CLAUDE.md Normal file
View file

@ -0,0 +1,46 @@
# go-blockchain
Go implementation of the Lethean blockchain protocol. Pure Go package providing
chain configuration, core data types, wire serialisation, and difficulty
calculation for the Lethean CryptoNote/Zano-fork chain.
This package follows ADR-001: Go Shell + C++ Crypto Library. Protocol logic
lives in Go; only the mathematically complex cryptographic primitives (ring
signatures, bulletproofs, Zarcanum proofs) are delegated to a cleaned C++
library via CGo in later phases.
Lineage: CryptoNote -> IntenseCoin (2017) -> Lethean -> Zano rebase.
Licence: EUPL-1.2
## Build and Test
```bash
go build ./...
go test -v -race ./...
go vet ./...
```
## Package Layout
```
config/ Chain parameters (mainnet/testnet), hardfork schedule
types/ Core data types: Hash, PublicKey, Address, Block, Transaction
wire/ Binary serialisation (CryptoNote varint encoding)
difficulty/ PoW + PoS difficulty adjustment (LWMA variant)
crypto/ (future) CGo bridge to libcryptonote
p2p/ (future) Levin TCP protocol
rpc/ (future) Daemon and wallet JSON-RPC
chain/ (future) Blockchain storage, validation, mempool
wallet/ (future) Key management, output scanning, tx construction
consensus/ (future) Hardfork rules, block reward, fee policy
```
## Coding Standards
- **Language:** UK English in all comments and documentation (colour, organisation, centre)
- **Commits:** Conventional commits (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`)
- **Co-Author:** All commits include `Co-Authored-By: Charon <charon@lethean.io>`
- **Test naming:** `_Good` (happy path), `_Bad` (expected errors), `_Ugly` (panics/edge cases)
- **Imports:** stdlib, then `golang.org/x`, then `forge.lthn.ai`, each separated by a blank line
- **No emojis** in code or comments

465
config/config.go Normal file
View file

@ -0,0 +1,465 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
// Package config defines chain parameters for the Lethean blockchain.
//
// All constants are derived from the canonical C++ source files
// currency_config.h.in and default.cmake. Mainnet and Testnet configurations
// are provided as package-level variables.
package config
// ---------------------------------------------------------------------------
// Tokenomics
// ---------------------------------------------------------------------------
// Coin is the number of smallest indivisible units in one LTHN.
// 1 LTHN = 10^12 atomic units.
const Coin uint64 = 1_000_000_000_000
// DisplayDecimalPoint is the number of decimal places used when displaying
// amounts in human-readable form.
const DisplayDecimalPoint = 12
// BlockReward is the fixed reward per block in atomic units (1.0 LTHN).
const BlockReward uint64 = 1_000_000_000_000
// DefaultFee is the standard transaction fee in atomic units (0.01 LTHN).
const DefaultFee uint64 = 10_000_000_000
// MinimumFee is the lowest acceptable transaction fee in atomic units (0.01 LTHN).
const MinimumFee uint64 = 10_000_000_000
// Premine is the total pre-mined supply in atomic units (10,000,000 LTHN).
// This covers the coinswap allocation (13,827,203 LTHN reserved) and the
// initial premine (3,690,000 LTHN). The raw value from cmake is
// 10,000,000,000,000,000,000 but that exceeds uint64 range. The C++ code
// uses an unsigned literal suffix; we store the value faithfully.
const Premine uint64 = 10_000_000_000_000_000_000
// BaseRewardDustThreshold is the minimum meaningful fraction of a block
// reward in atomic units.
const BaseRewardDustThreshold uint64 = 1_000_000
// DefaultDustThreshold is the dust threshold for normal transactions.
const DefaultDustThreshold uint64 = 0
// ---------------------------------------------------------------------------
// Address prefixes
// ---------------------------------------------------------------------------
// Address prefix constants determine the leading characters of base58-encoded
// addresses. Each prefix is varint-encoded before the public keys.
const (
// AddressPrefix is the standard public address prefix.
// Produces addresses starting with "iTHN".
AddressPrefix uint64 = 0x1eaf7
// IntegratedAddressPrefix is the prefix for integrated addresses
// (address + embedded payment ID). Produces addresses starting with "iTHn".
IntegratedAddressPrefix uint64 = 0xdeaf7
// AuditableAddressPrefix is the prefix for auditable addresses.
// Produces addresses starting with "iThN".
AuditableAddressPrefix uint64 = 0x3ceff7
// AuditableIntegratedAddressPrefix is the prefix for auditable
// integrated addresses. Produces addresses starting with "iThn".
AuditableIntegratedAddressPrefix uint64 = 0x8b077
)
// ---------------------------------------------------------------------------
// P2P and RPC ports
// ---------------------------------------------------------------------------
const (
MainnetP2PPort uint16 = 36942
MainnetRPCPort uint16 = 36941
MainnetStratumPort uint16 = 36940
TestnetP2PPort uint16 = 46942
TestnetRPCPort uint16 = 46941
TestnetStratumPort uint16 = 46940
)
// ---------------------------------------------------------------------------
// Timing and difficulty
// ---------------------------------------------------------------------------
const (
// DifficultyPowTarget is the target block interval for PoW blocks in seconds.
DifficultyPowTarget uint64 = 120
// DifficultyPosTarget is the target block interval for PoS blocks in seconds.
DifficultyPosTarget uint64 = 120
// DifficultyTotalTarget is the effective combined target:
// (PoW + PoS) / 4 = 60 seconds.
DifficultyTotalTarget uint64 = (DifficultyPowTarget + DifficultyPosTarget) / 4
// DifficultyWindow is the number of blocks used for difficulty calculation.
DifficultyWindow uint64 = 720
// DifficultyLag is the additional lookback beyond the window.
DifficultyLag uint64 = 15
// DifficultyCut is the number of timestamps cut from each end after sorting.
DifficultyCut uint64 = 60
// DifficultyBlocksCount is the total number of blocks considered
// (Window + Lag).
DifficultyBlocksCount uint64 = DifficultyWindow + DifficultyLag
// DifficultyPowStarter is the initial PoW difficulty.
DifficultyPowStarter uint64 = 1
// DifficultyPosStarter is the initial PoS difficulty.
DifficultyPosStarter uint64 = 1
// DifficultyPowTargetHF6 is the PoW target after hardfork 6 (240s).
DifficultyPowTargetHF6 uint64 = 240
// DifficultyPosTargetHF6 is the PoS target after hardfork 6 (240s).
DifficultyPosTargetHF6 uint64 = 240
// DifficultyTotalTargetHF6 is the combined target after HF6.
DifficultyTotalTargetHF6 uint64 = (DifficultyPowTargetHF6 + DifficultyPosTargetHF6) / 4
)
// ---------------------------------------------------------------------------
// Block and transaction limits
// ---------------------------------------------------------------------------
const (
// MaxBlockNumber is the absolute maximum block height.
MaxBlockNumber uint64 = 500_000_000
// MaxBlockSize is the maximum block header blob size in bytes.
MaxBlockSize uint64 = 500_000_000
// TxMaxAllowedInputs is the maximum number of inputs per transaction.
// Limited primarily by the asset surjection proof.
TxMaxAllowedInputs uint64 = 256
// TxMaxAllowedOutputs is the maximum number of outputs per transaction.
TxMaxAllowedOutputs uint64 = 2000
// TxMinAllowedOutputs is the minimum number of outputs (effective from HF4 Zarcanum).
TxMinAllowedOutputs uint64 = 2
// DefaultDecoySetSize is the ring size for pre-HF4 transactions.
DefaultDecoySetSize uint64 = 10
// HF4MandatoryDecoySetSize is the ring size required from HF4 onwards.
HF4MandatoryDecoySetSize uint64 = 15
// HF4MandatoryMinCoinage is the minimum coinage in blocks required for HF4.
HF4MandatoryMinCoinage uint64 = 10
// MinedMoneyUnlockWindow is the number of blocks before mined coins
// can be spent.
MinedMoneyUnlockWindow uint64 = 10
// BlockGrantedFullRewardZone is the block size threshold in bytes after
// which block reward is calculated using the actual block size.
BlockGrantedFullRewardZone uint64 = 125_000
// CoinbaseBlobReservedSize is the reserved space for the coinbase
// transaction blob in bytes.
CoinbaseBlobReservedSize uint64 = 1100
// BlockFutureTimeLimit is the maximum acceptable future timestamp for
// PoW blocks in seconds (2 hours).
BlockFutureTimeLimit uint64 = 60 * 60 * 2
// PosBlockFutureTimeLimit is the maximum acceptable future timestamp
// for PoS blocks in seconds (20 minutes).
PosBlockFutureTimeLimit uint64 = 60 * 20
// TimestampCheckWindow is the number of blocks used when checking
// whether a block timestamp is valid.
TimestampCheckWindow uint64 = 60
// PosStartHeight is the block height from which PoS is enabled.
PosStartHeight uint64 = 0
// RewardBlocksWindow is the number of recent blocks used to calculate
// the reward median.
RewardBlocksWindow uint64 = 400
// FreeTxMaxBlobSize is the soft txpool-based limit for free transactions
// in bytes.
FreeTxMaxBlobSize uint64 = 1024
// PreHardforkTxFreezePeriod is the number of blocks before hardfork
// activation when no new transactions are accepted (effective from HF5).
PreHardforkTxFreezePeriod uint64 = 60
)
// ---------------------------------------------------------------------------
// Block version constants
// ---------------------------------------------------------------------------
const (
BlockMajorVersionGenesis uint8 = 1
BlockMinorVersionGenesis uint8 = 0
BlockMajorVersionInitial uint8 = 0
HF1BlockMajorVersion uint8 = 1
HF3BlockMajorVersion uint8 = 2
HF3BlockMinorVersion uint8 = 0
CurrentBlockMajorVersion uint8 = 3
CurrentBlockMinorVersion uint8 = 0
)
// ---------------------------------------------------------------------------
// Transaction version constants
// ---------------------------------------------------------------------------
const (
TransactionVersionInitial uint8 = 0
TransactionVersionPreHF4 uint8 = 1
TransactionVersionPostHF4 uint8 = 2
TransactionVersionPostHF5 uint8 = 3
CurrentTransactionVersion uint8 = 3
)
// ---------------------------------------------------------------------------
// PoS constants
// ---------------------------------------------------------------------------
const (
PosScanWindow uint64 = 60 * 10 // 10 minutes in seconds
PosScanStep uint64 = 15 // seconds
PosModifierInterval uint64 = 10
PosMinimumCoinstakeAge uint64 = 10 // blocks
PosStrictSequenceLimit uint64 = 20
PosStarterKernelHash = "00000000000000000006382a8d8f94588ce93a1351924f6ccb9e07dd287c6e4b"
)
// ---------------------------------------------------------------------------
// P2P constants
// ---------------------------------------------------------------------------
const (
P2PLocalWhitePeerlistLimit uint64 = 1000
P2PLocalGrayPeerlistLimit uint64 = 5000
P2PDefaultConnectionsCount uint64 = 8
P2PDefaultHandshakeInterval uint64 = 60 // seconds
P2PDefaultPacketMaxSize uint64 = 50_000_000
P2PIPBlockTime uint64 = 60 * 60 * 24 // 24 hours
P2PIPFailsBeforeBlock uint64 = 10
)
// ---------------------------------------------------------------------------
// Network identity
// ---------------------------------------------------------------------------
const (
// CurrencyFormationVersion identifies the mainnet network.
CurrencyFormationVersion uint64 = 84
// CurrencyFormationVersionTestnet identifies the testnet network.
CurrencyFormationVersionTestnet uint64 = 100
// P2PNetworkIDVer is derived from CurrencyFormationVersion + 0.
P2PNetworkIDVer uint64 = CurrencyFormationVersion + 0
)
// ---------------------------------------------------------------------------
// Currency identity
// ---------------------------------------------------------------------------
const (
CurrencyNameAbbreviation = "LTHN"
CurrencyNameBase = "Lethean"
CurrencyNameShort = "Lethean"
)
// ---------------------------------------------------------------------------
// Alias constants
// ---------------------------------------------------------------------------
const (
AliasMinimumPublicShortNameAllowed uint64 = 6
AliasNameMaxLen uint64 = 255
AliasValidChars = "0123456789abcdefghijklmnopqrstuvwxyz-."
AliasCommentMaxSizeBytes uint64 = 400
MaxAliasPerBlock uint64 = 1000
)
// ---------------------------------------------------------------------------
// ChainConfig aggregates all chain parameters into a single struct.
// ---------------------------------------------------------------------------
// ChainConfig holds the complete set of parameters for a particular chain
// (mainnet or testnet).
type ChainConfig struct {
// Name is the human-readable chain name.
Name string
// Abbreviation is the ticker symbol.
Abbreviation string
// IsTestnet indicates whether this is a test network.
IsTestnet bool
// CurrencyFormationVersion identifies the network.
CurrencyFormationVersion uint64
// Coin is the number of atomic units per coin.
Coin uint64
// DisplayDecimalPoint is the number of decimal places.
DisplayDecimalPoint uint8
// BlockReward is the fixed block reward in atomic units.
BlockReward uint64
// DefaultFee is the default transaction fee in atomic units.
DefaultFee uint64
// MinimumFee is the minimum acceptable fee in atomic units.
MinimumFee uint64
// Premine is the pre-mined amount in atomic units.
Premine uint64
// AddressPrefix is the base58 prefix for standard addresses.
AddressPrefix uint64
// IntegratedAddressPrefix is the base58 prefix for integrated addresses.
IntegratedAddressPrefix uint64
// AuditableAddressPrefix is the base58 prefix for auditable addresses.
AuditableAddressPrefix uint64
// AuditableIntegratedAddressPrefix is the base58 prefix for auditable
// integrated addresses.
AuditableIntegratedAddressPrefix uint64
// P2PPort is the default peer-to-peer port.
P2PPort uint16
// RPCPort is the default RPC port.
RPCPort uint16
// StratumPort is the default stratum mining port.
StratumPort uint16
// DifficultyPowTarget is the target PoW block interval in seconds.
DifficultyPowTarget uint64
// DifficultyPosTarget is the target PoS block interval in seconds.
DifficultyPosTarget uint64
// DifficultyWindow is the number of blocks in the difficulty window.
DifficultyWindow uint64
// DifficultyLag is the additional lookback beyond the window.
DifficultyLag uint64
// DifficultyCut is the number of timestamps cut after sorting.
DifficultyCut uint64
// DifficultyPowStarter is the initial PoW difficulty.
DifficultyPowStarter uint64
// DifficultyPosStarter is the initial PoS difficulty.
DifficultyPosStarter uint64
// MaxBlockNumber is the absolute maximum block height.
MaxBlockNumber uint64
// TxMaxAllowedInputs is the maximum inputs per transaction.
TxMaxAllowedInputs uint64
// TxMaxAllowedOutputs is the maximum outputs per transaction.
TxMaxAllowedOutputs uint64
// DefaultDecoySetSize is the default ring size.
DefaultDecoySetSize uint64
// HF4MandatoryDecoySetSize is the mandatory ring size from HF4.
HF4MandatoryDecoySetSize uint64
// MinedMoneyUnlockWindow is the maturity period for mined coins.
MinedMoneyUnlockWindow uint64
// P2PMaintainersPubKey is the hex-encoded maintainers public key.
P2PMaintainersPubKey string
}
// Mainnet holds the chain configuration for the Lethean mainnet.
var Mainnet = ChainConfig{
Name: CurrencyNameBase,
Abbreviation: CurrencyNameAbbreviation,
IsTestnet: false,
CurrencyFormationVersion: CurrencyFormationVersion,
Coin: Coin,
DisplayDecimalPoint: DisplayDecimalPoint,
BlockReward: BlockReward,
DefaultFee: DefaultFee,
MinimumFee: MinimumFee,
Premine: Premine,
AddressPrefix: AddressPrefix,
IntegratedAddressPrefix: IntegratedAddressPrefix,
AuditableAddressPrefix: AuditableAddressPrefix,
AuditableIntegratedAddressPrefix: AuditableIntegratedAddressPrefix,
P2PPort: MainnetP2PPort,
RPCPort: MainnetRPCPort,
StratumPort: MainnetStratumPort,
DifficultyPowTarget: DifficultyPowTarget,
DifficultyPosTarget: DifficultyPosTarget,
DifficultyWindow: DifficultyWindow,
DifficultyLag: DifficultyLag,
DifficultyCut: DifficultyCut,
DifficultyPowStarter: DifficultyPowStarter,
DifficultyPosStarter: DifficultyPosStarter,
MaxBlockNumber: MaxBlockNumber,
TxMaxAllowedInputs: TxMaxAllowedInputs,
TxMaxAllowedOutputs: TxMaxAllowedOutputs,
DefaultDecoySetSize: DefaultDecoySetSize,
HF4MandatoryDecoySetSize: HF4MandatoryDecoySetSize,
MinedMoneyUnlockWindow: MinedMoneyUnlockWindow,
P2PMaintainersPubKey: "8f138bb73f6d663a3746a542770781a09579a7b84cb4125249e95530824ee607",
}
// Testnet holds the chain configuration for the Lethean testnet.
var Testnet = ChainConfig{
Name: CurrencyNameBase + "_testnet",
Abbreviation: CurrencyNameAbbreviation,
IsTestnet: true,
CurrencyFormationVersion: CurrencyFormationVersionTestnet,
Coin: Coin,
DisplayDecimalPoint: DisplayDecimalPoint,
BlockReward: BlockReward,
DefaultFee: DefaultFee,
MinimumFee: MinimumFee,
Premine: Premine,
AddressPrefix: AddressPrefix,
IntegratedAddressPrefix: IntegratedAddressPrefix,
AuditableAddressPrefix: AuditableAddressPrefix,
AuditableIntegratedAddressPrefix: AuditableIntegratedAddressPrefix,
P2PPort: TestnetP2PPort,
RPCPort: TestnetRPCPort,
StratumPort: TestnetStratumPort,
DifficultyPowTarget: DifficultyPowTarget,
DifficultyPosTarget: DifficultyPosTarget,
DifficultyWindow: DifficultyWindow,
DifficultyLag: DifficultyLag,
DifficultyCut: DifficultyCut,
DifficultyPowStarter: DifficultyPowStarter,
DifficultyPosStarter: DifficultyPosStarter,
MaxBlockNumber: MaxBlockNumber,
TxMaxAllowedInputs: TxMaxAllowedInputs,
TxMaxAllowedOutputs: TxMaxAllowedOutputs,
DefaultDecoySetSize: DefaultDecoySetSize,
HF4MandatoryDecoySetSize: HF4MandatoryDecoySetSize,
MinedMoneyUnlockWindow: MinedMoneyUnlockWindow,
P2PMaintainersPubKey: "8f138bb73f6d663a3746a542770781a09579a7b84cb4125249e95530824ee607",
}

168
config/config_test.go Normal file
View file

@ -0,0 +1,168 @@
// 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 TestMainnetConstants_Good(t *testing.T) {
// Verify tokenomics match C++ source (default.cmake).
if Coin != 1_000_000_000_000 {
t.Errorf("Coin: got %d, want 1000000000000", Coin)
}
if DisplayDecimalPoint != 12 {
t.Errorf("DisplayDecimalPoint: got %d, want 12", DisplayDecimalPoint)
}
if BlockReward != 1_000_000_000_000 {
t.Errorf("BlockReward: got %d, want 1000000000000", BlockReward)
}
if DefaultFee != 10_000_000_000 {
t.Errorf("DefaultFee: got %d, want 10000000000", DefaultFee)
}
if MinimumFee != 10_000_000_000 {
t.Errorf("MinimumFee: got %d, want 10000000000", MinimumFee)
}
if BaseRewardDustThreshold != 1_000_000 {
t.Errorf("BaseRewardDustThreshold: got %d, want 1000000", BaseRewardDustThreshold)
}
if DefaultDustThreshold != 0 {
t.Errorf("DefaultDustThreshold: got %d, want 0", DefaultDustThreshold)
}
}
func TestMainnetAddressPrefixes_Good(t *testing.T) {
tests := []struct {
name string
got uint64
want uint64
}{
{"AddressPrefix", AddressPrefix, 0x1eaf7},
{"IntegratedAddressPrefix", IntegratedAddressPrefix, 0xdeaf7},
{"AuditableAddressPrefix", AuditableAddressPrefix, 0x3ceff7},
{"AuditableIntegratedAddressPrefix", AuditableIntegratedAddressPrefix, 0x8b077},
}
for _, tt := range tests {
if tt.got != tt.want {
t.Errorf("%s: got 0x%x, want 0x%x", tt.name, tt.got, tt.want)
}
}
}
func TestMainnetPorts_Good(t *testing.T) {
if Mainnet.P2PPort != 36942 {
t.Errorf("Mainnet P2P port: got %d, want 36942", Mainnet.P2PPort)
}
if Mainnet.RPCPort != 36941 {
t.Errorf("Mainnet RPC port: got %d, want 36941", Mainnet.RPCPort)
}
if Mainnet.StratumPort != 36940 {
t.Errorf("Mainnet Stratum port: got %d, want 36940", Mainnet.StratumPort)
}
}
func TestTestnetPortDifferences_Good(t *testing.T) {
if Testnet.P2PPort != 46942 {
t.Errorf("Testnet P2P port: got %d, want 46942", Testnet.P2PPort)
}
if Testnet.RPCPort != 46941 {
t.Errorf("Testnet RPC port: got %d, want 46941", Testnet.RPCPort)
}
if Testnet.StratumPort != 46940 {
t.Errorf("Testnet Stratum port: got %d, want 46940", Testnet.StratumPort)
}
if Testnet.P2PPort == Mainnet.P2PPort {
t.Error("Testnet and Mainnet P2P ports must differ")
}
}
func TestDifficultyConstants_Good(t *testing.T) {
if DifficultyPowTarget != 120 {
t.Errorf("DifficultyPowTarget: got %d, want 120", DifficultyPowTarget)
}
if DifficultyPosTarget != 120 {
t.Errorf("DifficultyPosTarget: got %d, want 120", DifficultyPosTarget)
}
if DifficultyTotalTarget != 60 {
t.Errorf("DifficultyTotalTarget: got %d, want 60 ((120+120)/4)", DifficultyTotalTarget)
}
if DifficultyWindow != 720 {
t.Errorf("DifficultyWindow: got %d, want 720", DifficultyWindow)
}
if DifficultyLag != 15 {
t.Errorf("DifficultyLag: got %d, want 15", DifficultyLag)
}
if DifficultyCut != 60 {
t.Errorf("DifficultyCut: got %d, want 60", DifficultyCut)
}
if DifficultyBlocksCount != 735 {
t.Errorf("DifficultyBlocksCount: got %d, want 735 (720+15)", DifficultyBlocksCount)
}
}
func TestNetworkIdentity_Good(t *testing.T) {
if CurrencyFormationVersion != 84 {
t.Errorf("CurrencyFormationVersion: got %d, want 84", CurrencyFormationVersion)
}
if CurrencyFormationVersionTestnet != 100 {
t.Errorf("CurrencyFormationVersionTestnet: got %d, want 100", CurrencyFormationVersionTestnet)
}
if P2PNetworkIDVer != 84 {
t.Errorf("P2PNetworkIDVer: got %d, want 84 (84+0)", P2PNetworkIDVer)
}
}
func TestChainConfigStruct_Good(t *testing.T) {
// Verify Mainnet struct fields are populated correctly.
if Mainnet.Name != "Lethean" {
t.Errorf("Mainnet.Name: got %q, want %q", Mainnet.Name, "Lethean")
}
if Mainnet.Abbreviation != "LTHN" {
t.Errorf("Mainnet.Abbreviation: got %q, want %q", Mainnet.Abbreviation, "LTHN")
}
if Mainnet.IsTestnet {
t.Error("Mainnet.IsTestnet should be false")
}
if !Testnet.IsTestnet {
t.Error("Testnet.IsTestnet should be true")
}
if Testnet.Name != "Lethean_testnet" {
t.Errorf("Testnet.Name: got %q, want %q", Testnet.Name, "Lethean_testnet")
}
}
func TestTransactionLimits_Good(t *testing.T) {
if TxMaxAllowedInputs != 256 {
t.Errorf("TxMaxAllowedInputs: got %d, want 256", TxMaxAllowedInputs)
}
if TxMaxAllowedOutputs != 2000 {
t.Errorf("TxMaxAllowedOutputs: got %d, want 2000", TxMaxAllowedOutputs)
}
if DefaultDecoySetSize != 10 {
t.Errorf("DefaultDecoySetSize: got %d, want 10", DefaultDecoySetSize)
}
if HF4MandatoryDecoySetSize != 15 {
t.Errorf("HF4MandatoryDecoySetSize: got %d, want 15", HF4MandatoryDecoySetSize)
}
}
func TestTransactionVersionConstants_Good(t *testing.T) {
if TransactionVersionInitial != 0 {
t.Errorf("TransactionVersionInitial: got %d, want 0", TransactionVersionInitial)
}
if TransactionVersionPreHF4 != 1 {
t.Errorf("TransactionVersionPreHF4: got %d, want 1", TransactionVersionPreHF4)
}
if TransactionVersionPostHF4 != 2 {
t.Errorf("TransactionVersionPostHF4: got %d, want 2", TransactionVersionPostHF4)
}
if TransactionVersionPostHF5 != 3 {
t.Errorf("TransactionVersionPostHF5: got %d, want 3", TransactionVersionPostHF5)
}
if CurrentTransactionVersion != 3 {
t.Errorf("CurrentTransactionVersion: got %d, want 3", CurrentTransactionVersion)
}
}

95
config/hardfork.go Normal file
View file

@ -0,0 +1,95 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
package config
// HardFork describes a single hard fork activation point on the chain.
type HardFork struct {
// Version is the hardfork version number (0-6).
Version uint8
// Height is the block height AFTER which this fork activates.
// The fork is active at heights strictly greater than this value.
// A value of 0 means the fork is active from genesis.
Height uint64
// Mandatory indicates whether nodes must support this fork version
// to remain on the network.
Mandatory bool
// Description is a short human-readable summary of what this fork changes.
Description string
}
// Hardfork version constants, matching the C++ ZANO_HARDFORK_* identifiers.
const (
HF0Initial uint8 = 0
HF1 uint8 = 1
HF2 uint8 = 2
HF3 uint8 = 3
HF4Zarcanum uint8 = 4
HF5 uint8 = 5
HF6 uint8 = 6
HFTotal uint8 = 7
)
// MainnetForks lists all hardfork activations for the Lethean mainnet.
// Heights correspond to ZANO_HARDFORK_*_AFTER_HEIGHT in the C++ source.
// The fork activates at heights strictly greater than the listed height,
// so Height=0 means active from genesis, and Height=10080 means active
// from block 10081 onwards.
var MainnetForks = []HardFork{
{Version: HF0Initial, Height: 0, Mandatory: true, Description: "CryptoNote base, PoW+PoS hybrid"},
{Version: HF1, Height: 10080, Mandatory: true, Description: "New transaction types"},
{Version: HF2, Height: 10080, Mandatory: true, Description: "Block time adjustment (720 blocks/day)"},
{Version: HF3, Height: 999999999, Mandatory: false, Description: "Block version 2"},
{Version: HF4Zarcanum, Height: 999999999, Mandatory: false, Description: "Zarcanum privacy (confidential txs, CLSAG)"},
{Version: HF5, Height: 999999999, Mandatory: false, Description: "Confidential assets, surjection proofs"},
{Version: HF6, Height: 999999999, Mandatory: false, Description: "Block time halving (120s to 240s)"},
}
// TestnetForks lists all hardfork activations for the Lethean testnet.
var TestnetForks = []HardFork{
{Version: HF0Initial, Height: 0, Mandatory: true, Description: "CryptoNote base, PoW+PoS hybrid"},
{Version: HF1, Height: 0, Mandatory: true, Description: "New transaction types"},
{Version: HF2, Height: 10, Mandatory: true, Description: "Block time adjustment"},
{Version: HF3, Height: 0, Mandatory: true, Description: "Block version 2"},
{Version: HF4Zarcanum, Height: 100, Mandatory: true, Description: "Zarcanum privacy"},
{Version: HF5, Height: 200, Mandatory: true, Description: "Confidential assets"},
{Version: HF6, Height: 999999999, Mandatory: false, Description: "Block time halving"},
}
// VersionAtHeight returns the highest hardfork version that is active at the
// given block height. It performs a reverse scan of the fork list to find the
// latest applicable version.
//
// A fork with Height=0 is active from genesis (height 0).
// A fork with Height=N is active at heights > N.
func VersionAtHeight(forks []HardFork, height uint64) uint8 {
var version uint8
for _, hf := range forks {
if hf.Height == 0 || height > hf.Height {
if hf.Version > version {
version = hf.Version
}
}
}
return version
}
// IsHardForkActive reports whether the specified hardfork version is active
// at the given block height.
func IsHardForkActive(forks []HardFork, version uint8, height uint64) bool {
for _, hf := range forks {
if hf.Version == version {
return hf.Height == 0 || height > hf.Height
}
}
return false
}

197
config/hardfork_test.go Normal file
View file

@ -0,0 +1,197 @@
// 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 TestVersionAtHeight_Good(t *testing.T) {
tests := []struct {
name string
height uint64
want uint8
}{
// Genesis block should return HF0 (the initial version).
{"genesis", 0, HF0Initial},
// Just before HF1/HF2 activation on mainnet (height 10080).
// HF1 activates at heights > 10080, so height 10080 is still HF0.
{"before_hf1", 10080, HF0Initial},
// At height 10081, both HF1 and HF2 activate (both have height 10080).
// The highest version wins.
{"at_hf1_hf2", 10081, HF2},
// Well past HF2 but before any future forks.
{"mid_chain", 50000, HF2},
// Far future but still below 999999999 — should still be HF2.
{"high_but_before_future", 999999999, HF2},
// At height 1000000000 (> 999999999), all future forks activate.
{"all_forks_active", 1000000000, HF6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := VersionAtHeight(MainnetForks, tt.height)
if got != tt.want {
t.Errorf("VersionAtHeight(MainnetForks, %d) = %d, want %d", tt.height, got, tt.want)
}
})
}
}
func TestVersionAtHeightTestnet_Good(t *testing.T) {
tests := []struct {
name string
height uint64
want uint8
}{
// On testnet, HF0, HF1, and HF3 all have Height=0 so they are
// active from genesis. The highest version at genesis is HF3.
{"genesis", 0, HF3},
// At height 10, HF2 is still not active (activates at > 10).
{"before_hf2", 10, HF3},
// At height 11, HF2 activates — but HF3 is already active so
// the version remains 3.
{"after_hf2", 11, HF3},
// At height 101, HF4 Zarcanum activates.
{"after_hf4", 101, HF4Zarcanum},
// At height 201, HF5 activates.
{"after_hf5", 201, HF5},
// Far future activates HF6.
{"far_future", 1000000000, HF6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := VersionAtHeight(TestnetForks, tt.height)
if got != tt.want {
t.Errorf("VersionAtHeight(TestnetForks, %d) = %d, want %d", tt.height, got, tt.want)
}
})
}
}
func TestIsHardForkActive_Good(t *testing.T) {
tests := []struct {
name string
version uint8
height uint64
want bool
}{
// HF0 is always active (Height=0).
{"hf0_at_genesis", HF0Initial, 0, true},
{"hf0_at_10000", HF0Initial, 10000, true},
// HF1 activates at heights > 10080.
{"hf1_before", HF1, 10080, false},
{"hf1_at", HF1, 10081, true},
// HF4 is far future on mainnet.
{"hf4_now", HF4Zarcanum, 50000, false},
{"hf4_far_future", HF4Zarcanum, 1000000000, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsHardForkActive(MainnetForks, tt.version, tt.height)
if got != tt.want {
t.Errorf("IsHardForkActive(MainnetForks, %d, %d) = %v, want %v",
tt.version, tt.height, got, tt.want)
}
})
}
}
func TestIsHardForkActive_Bad(t *testing.T) {
// Querying a version that does not exist should return false.
got := IsHardForkActive(MainnetForks, 99, 1000000000)
if got {
t.Error("IsHardForkActive with unknown version should return false")
}
}
func TestVersionAtHeight_Ugly(t *testing.T) {
// Empty fork list should return version 0.
got := VersionAtHeight(nil, 100)
if got != 0 {
t.Errorf("VersionAtHeight(nil, 100) = %d, want 0", got)
}
// Single-element fork list.
single := []HardFork{{Version: 1, Height: 0, Mandatory: true}}
got = VersionAtHeight(single, 0)
if got != 1 {
t.Errorf("VersionAtHeight(single, 0) = %d, want 1", got)
}
}
func TestMainnetForkSchedule_Good(t *testing.T) {
// Verify the fork schedule matches the C++ ZANO_HARDFORK_* constants.
expectedMainnet := []struct {
version uint8
height uint64
}{
{0, 0},
{1, 10080},
{2, 10080},
{3, 999999999},
{4, 999999999},
{5, 999999999},
{6, 999999999},
}
if len(MainnetForks) != len(expectedMainnet) {
t.Fatalf("MainnetForks length: got %d, want %d", len(MainnetForks), len(expectedMainnet))
}
for i, exp := range expectedMainnet {
hf := MainnetForks[i]
if hf.Version != exp.version {
t.Errorf("MainnetForks[%d].Version = %d, want %d", i, hf.Version, exp.version)
}
if hf.Height != exp.height {
t.Errorf("MainnetForks[%d].Height = %d, want %d", i, hf.Height, exp.height)
}
}
}
func TestTestnetForkSchedule_Good(t *testing.T) {
expectedTestnet := []struct {
version uint8
height uint64
}{
{0, 0},
{1, 0},
{2, 10},
{3, 0},
{4, 100},
{5, 200},
{6, 999999999},
}
if len(TestnetForks) != len(expectedTestnet) {
t.Fatalf("TestnetForks length: got %d, want %d", len(TestnetForks), len(expectedTestnet))
}
for i, exp := range expectedTestnet {
hf := TestnetForks[i]
if hf.Version != exp.version {
t.Errorf("TestnetForks[%d].Version = %d, want %d", i, hf.Version, exp.version)
}
if hf.Height != exp.height {
t.Errorf("TestnetForks[%d].Height = %d, want %d", i, hf.Height, exp.height)
}
}
}

100
difficulty/difficulty.go Normal file
View file

@ -0,0 +1,100 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
// Package difficulty implements the LWMA (Linear Weighted Moving Average)
// difficulty adjustment algorithm used by the Lethean blockchain for both
// PoW and PoS blocks.
//
// The algorithm examines a window of recent block timestamps and cumulative
// difficulties to calculate the next target difficulty, ensuring blocks
// arrive at the desired interval on average.
package difficulty
import (
"math/big"
)
// Algorithm constants matching the C++ source.
const (
// Window is the number of blocks in the difficulty calculation window.
Window uint64 = 720
// Lag is the additional lookback beyond the window.
Lag uint64 = 15
// Cut is the number of extreme timestamps trimmed from each end after
// sorting. This dampens the effect of outlier timestamps.
Cut uint64 = 60
// BlocksCount is the total number of blocks considered (Window + Lag).
BlocksCount uint64 = Window + Lag
)
// StarterDifficulty is the minimum difficulty returned when there is
// insufficient data to calculate a proper value.
var StarterDifficulty = big.NewInt(1)
// NextDifficulty calculates the next block difficulty using the LWMA algorithm.
//
// Parameters:
// - timestamps: block timestamps for the last BlocksCount blocks, ordered
// from oldest to newest.
// - cumulativeDiffs: cumulative difficulties corresponding to each block.
// - target: the desired block interval in seconds (e.g. 120 for PoW/PoS).
//
// Returns the calculated difficulty for the next block.
//
// If the input slices are too short to perform a meaningful calculation, the
// function returns StarterDifficulty.
func NextDifficulty(timestamps []uint64, cumulativeDiffs []*big.Int, target uint64) *big.Int {
// Need at least 2 entries to compute a time span and difficulty delta.
if len(timestamps) < 2 || len(cumulativeDiffs) < 2 {
return new(big.Int).Set(StarterDifficulty)
}
length := uint64(len(timestamps))
if length > BlocksCount {
length = BlocksCount
}
// Use the available window, but ensure we have at least 2 points.
windowSize := length
if windowSize < 2 {
return new(big.Int).Set(StarterDifficulty)
}
// Calculate the time span across the window.
// Use only the last windowSize entries.
startIdx := uint64(len(timestamps)) - windowSize
endIdx := uint64(len(timestamps)) - 1
timeSpan := timestamps[endIdx] - timestamps[startIdx]
if timeSpan == 0 {
timeSpan = 1 // prevent division by zero
}
// Calculate the difficulty delta across the same window.
diffDelta := new(big.Int).Sub(cumulativeDiffs[endIdx], cumulativeDiffs[startIdx])
if diffDelta.Sign() <= 0 {
return new(big.Int).Set(StarterDifficulty)
}
// LWMA core: nextDiff = diffDelta * target / timeSpan
// This keeps the difficulty proportional to the hash rate needed to
// maintain the target block interval.
nextDiff := new(big.Int).Mul(diffDelta, new(big.Int).SetUint64(target))
nextDiff.Div(nextDiff, new(big.Int).SetUint64(timeSpan))
// Ensure we never return zero difficulty.
if nextDiff.Sign() <= 0 {
return new(big.Int).Set(StarterDifficulty)
}
return nextDiff
}

View file

@ -0,0 +1,129 @@
// 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 difficulty
import (
"math/big"
"testing"
)
func TestNextDifficulty_Good(t *testing.T) {
// Synthetic test: constant block times at exactly the target interval.
// With perfectly timed blocks, the difficulty should remain stable.
const target uint64 = 120
const numBlocks = 100
timestamps := make([]uint64, numBlocks)
cumulativeDiffs := make([]*big.Int, numBlocks)
baseDifficulty := big.NewInt(1000)
for i := 0; i < numBlocks; i++ {
timestamps[i] = uint64(i) * target
cumulativeDiffs[i] = new(big.Int).Mul(baseDifficulty, big.NewInt(int64(i)))
}
result := NextDifficulty(timestamps, cumulativeDiffs, target)
if result.Sign() <= 0 {
t.Fatalf("NextDifficulty returned non-positive value: %s", result)
}
// With constant intervals, the result should be approximately equal to
// the base difficulty. Allow some tolerance due to integer arithmetic.
expected := baseDifficulty
tolerance := new(big.Int).Div(expected, big.NewInt(10)) // 10% tolerance
diff := new(big.Int).Sub(result, expected)
diff.Abs(diff)
if diff.Cmp(tolerance) > 0 {
t.Errorf("NextDifficulty with constant intervals: got %s, expected ~%s (tolerance %s)",
result, expected, tolerance)
}
}
func TestNextDifficultyEmpty_Good(t *testing.T) {
// Empty input should return starter difficulty.
result := NextDifficulty(nil, nil, 120)
if result.Cmp(StarterDifficulty) != 0 {
t.Errorf("NextDifficulty(nil, nil, 120) = %s, want %s", result, StarterDifficulty)
}
}
func TestNextDifficultySingleEntry_Good(t *testing.T) {
// A single entry is insufficient for calculation.
timestamps := []uint64{1000}
diffs := []*big.Int{big.NewInt(100)}
result := NextDifficulty(timestamps, diffs, 120)
if result.Cmp(StarterDifficulty) != 0 {
t.Errorf("NextDifficulty with single entry = %s, want %s", result, StarterDifficulty)
}
}
func TestNextDifficultyFastBlocks_Good(t *testing.T) {
// When blocks come faster than the target, difficulty should increase.
const target uint64 = 120
const numBlocks = 50
const actualInterval uint64 = 60 // half the target — blocks are too fast
timestamps := make([]uint64, numBlocks)
cumulativeDiffs := make([]*big.Int, numBlocks)
baseDifficulty := big.NewInt(1000)
for i := 0; i < numBlocks; i++ {
timestamps[i] = uint64(i) * actualInterval
cumulativeDiffs[i] = new(big.Int).Mul(baseDifficulty, big.NewInt(int64(i)))
}
result := NextDifficulty(timestamps, cumulativeDiffs, target)
if result.Cmp(baseDifficulty) <= 0 {
t.Errorf("expected difficulty > %s for fast blocks, got %s", baseDifficulty, result)
}
}
func TestNextDifficultySlowBlocks_Good(t *testing.T) {
// When blocks come slower than the target, difficulty should decrease.
const target uint64 = 120
const numBlocks = 50
const actualInterval uint64 = 240 // double the target — blocks are too slow
timestamps := make([]uint64, numBlocks)
cumulativeDiffs := make([]*big.Int, numBlocks)
baseDifficulty := big.NewInt(1000)
for i := 0; i < numBlocks; i++ {
timestamps[i] = uint64(i) * actualInterval
cumulativeDiffs[i] = new(big.Int).Mul(baseDifficulty, big.NewInt(int64(i)))
}
result := NextDifficulty(timestamps, cumulativeDiffs, target)
if result.Cmp(baseDifficulty) >= 0 {
t.Errorf("expected difficulty < %s for slow blocks, got %s", baseDifficulty, result)
}
}
func TestNextDifficulty_Ugly(t *testing.T) {
// Two entries with zero time span — should handle gracefully.
timestamps := []uint64{1000, 1000}
diffs := []*big.Int{big.NewInt(0), big.NewInt(100)}
result := NextDifficulty(timestamps, diffs, 120)
if result.Sign() <= 0 {
t.Errorf("NextDifficulty with zero time span should still return positive, got %s", result)
}
}
func TestConstants_Good(t *testing.T) {
if Window != 720 {
t.Errorf("Window: got %d, want 720", Window)
}
if Lag != 15 {
t.Errorf("Lag: got %d, want 15", Lag)
}
if Cut != 60 {
t.Errorf("Cut: got %d, want 60", Cut)
}
if BlocksCount != 735 {
t.Errorf("BlocksCount: got %d, want 735", BlocksCount)
}
}

7
go.mod Normal file
View file

@ -0,0 +1,7 @@
module forge.lthn.ai/core/go-blockchain
go 1.25
require golang.org/x/crypto v0.48.0
require golang.org/x/sys v0.41.0 // indirect

4
go.sum Normal file
View file

@ -0,0 +1,4 @@
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

289
types/address.go Normal file
View file

@ -0,0 +1,289 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
package types
import (
"errors"
"fmt"
"math/big"
"golang.org/x/crypto/sha3"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/wire"
)
// FlagAuditable marks an address as auditable. When set, the address was
// generated with a deterministic view key derivation, allowing a third party
// with the view key to audit all incoming transactions.
const FlagAuditable uint8 = 0x01
// Address represents a Lethean account public address consisting of a spend
// public key, a view public key, and optional flags (e.g. auditable).
type Address struct {
SpendPublicKey PublicKey
ViewPublicKey PublicKey
Flags uint8
}
// IsAuditable reports whether the address has the auditable flag set.
func (a *Address) IsAuditable() bool {
return a.Flags&FlagAuditable != 0
}
// IsIntegrated reports whether the given prefix corresponds to an integrated
// address type (standard integrated or auditable integrated).
func (a *Address) IsIntegrated() bool {
// This method checks whether the address was decoded with an integrated
// prefix. Since we do not store the prefix in the Address struct, callers
// should use the prefix returned by DecodeAddress to determine this.
// This helper exists for convenience when the prefix is not available.
return false
}
// IsIntegratedPrefix reports whether the given prefix corresponds to an
// integrated address type.
func IsIntegratedPrefix(prefix uint64) bool {
return prefix == config.IntegratedAddressPrefix ||
prefix == config.AuditableIntegratedAddressPrefix
}
// Encode serialises the address into a CryptoNote base58 string with the
// given prefix. The encoding format is:
//
// varint(prefix) || spend_pubkey (32 bytes) || view_pubkey (32 bytes) || flags (1 byte) || checksum (4 bytes)
//
// The checksum is the first 4 bytes of Keccak-256 over the preceding data.
func (a *Address) Encode(prefix uint64) string {
// Build the raw data: prefix (varint) + keys + flags.
prefixBytes := wire.EncodeVarint(prefix)
raw := make([]byte, 0, len(prefixBytes)+32+32+1+4)
raw = append(raw, prefixBytes...)
raw = append(raw, a.SpendPublicKey[:]...)
raw = append(raw, a.ViewPublicKey[:]...)
raw = append(raw, a.Flags)
// Compute Keccak-256 checksum over the raw data.
checksum := keccak256Checksum(raw)
raw = append(raw, checksum[:]...)
return base58Encode(raw)
}
// DecodeAddress parses a CryptoNote base58-encoded address string. It returns
// the decoded address, the prefix that was used, and any error.
func DecodeAddress(s string) (*Address, uint64, error) {
raw, err := base58Decode(s)
if err != nil {
return nil, 0, fmt.Errorf("types: base58 decode failed: %w", err)
}
// The minimum size is: 1 byte prefix varint + 32 + 32 + 1 flags + 4 checksum = 70.
if len(raw) < 70 {
return nil, 0, errors.New("types: address data too short")
}
// Decode the prefix varint.
prefix, prefixLen, err := wire.DecodeVarint(raw)
if err != nil {
return nil, 0, fmt.Errorf("types: invalid address prefix varint: %w", err)
}
// After the prefix we need exactly 32+32+1+4 = 69 bytes.
remaining := raw[prefixLen:]
if len(remaining) != 69 {
return nil, 0, fmt.Errorf("types: unexpected address data length: want 69 bytes after prefix, got %d", len(remaining))
}
// Validate checksum: Keccak-256 of everything except the last 4 bytes.
payloadEnd := len(raw) - 4
expectedChecksum := keccak256Checksum(raw[:payloadEnd])
actualChecksum := raw[payloadEnd:]
if expectedChecksum[0] != actualChecksum[0] ||
expectedChecksum[1] != actualChecksum[1] ||
expectedChecksum[2] != actualChecksum[2] ||
expectedChecksum[3] != actualChecksum[3] {
return nil, 0, errors.New("types: address checksum mismatch")
}
addr := &Address{}
copy(addr.SpendPublicKey[:], remaining[0:32])
copy(addr.ViewPublicKey[:], remaining[32:64])
addr.Flags = remaining[64]
return addr, prefix, nil
}
// keccak256Checksum returns the first 4 bytes of the Keccak-256 hash of data.
// This uses the legacy Keccak-256 (pre-NIST), NOT SHA3-256.
func keccak256Checksum(data []byte) [4]byte {
h := sha3.NewLegacyKeccak256()
h.Write(data)
sum := h.Sum(nil)
var checksum [4]byte
copy(checksum[:], sum[:4])
return checksum
}
// ---------------------------------------------------------------------------
// CryptoNote Base58 encoding
// ---------------------------------------------------------------------------
// base58Alphabet is the CryptoNote base58 character set. Note: this omits
// 0, O, I, l to avoid visual ambiguity.
const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
// base58BlockSizes maps input byte counts (0-8) to the number of base58
// characters needed to encode that many bytes. CryptoNote encodes data in
// 8-byte blocks, each producing 11 base58 characters, with the final
// partial block producing fewer characters.
var base58BlockSizes = [9]int{0, 2, 3, 5, 6, 7, 9, 10, 11}
// base58ReverseBlockSizes maps encoded character counts back to byte counts.
var base58ReverseBlockSizes [12]int
func init() {
for i := range base58ReverseBlockSizes {
base58ReverseBlockSizes[i] = -1
}
for byteCount, charCount := range base58BlockSizes {
if charCount < len(base58ReverseBlockSizes) {
base58ReverseBlockSizes[charCount] = byteCount
}
}
}
// base58Encode encodes raw bytes using the CryptoNote base58 scheme.
// Data is split into 8-byte blocks; each block is encoded independently.
func base58Encode(data []byte) string {
if len(data) == 0 {
return ""
}
result := make([]byte, 0, len(data)*2)
fullBlocks := len(data) / 8
lastBlockSize := len(data) % 8
for i := 0; i < fullBlocks; i++ {
block := data[i*8 : (i+1)*8]
encoded := encodeBlock(block, 11)
result = append(result, encoded...)
}
if lastBlockSize > 0 {
block := data[fullBlocks*8:]
encodedSize := base58BlockSizes[lastBlockSize]
encoded := encodeBlock(block, encodedSize)
result = append(result, encoded...)
}
return string(result)
}
// encodeBlock encodes a single block (up to 8 bytes) into the specified
// number of base58 characters.
func encodeBlock(block []byte, encodedSize int) []byte {
// Convert the block to a big integer.
num := new(big.Int).SetBytes(block)
base := big.NewInt(58)
result := make([]byte, encodedSize)
for i := range result {
result[i] = base58Alphabet[0] // fill with '1' (zero digit)
}
// Encode from least significant digit to most significant.
idx := encodedSize - 1
zero := new(big.Int)
mod := new(big.Int)
for num.Cmp(zero) > 0 && idx >= 0 {
num.DivMod(num, base, mod)
result[idx] = base58Alphabet[mod.Int64()]
idx--
}
return result
}
// base58Decode decodes a CryptoNote base58 string back into raw bytes.
func base58Decode(s string) ([]byte, error) {
if len(s) == 0 {
return nil, errors.New("types: empty base58 string")
}
fullBlocks := len(s) / 11
lastBlockChars := len(s) % 11
// Validate that the last block size maps to a valid byte count.
if lastBlockChars > 0 && base58ReverseBlockSizes[lastBlockChars] < 0 {
return nil, fmt.Errorf("types: invalid base58 string length %d", len(s))
}
var result []byte
for i := 0; i < fullBlocks; i++ {
blockStr := s[i*11 : (i+1)*11]
decoded, err := decodeBlock(blockStr, 8)
if err != nil {
return nil, err
}
result = append(result, decoded...)
}
if lastBlockChars > 0 {
blockStr := s[fullBlocks*11:]
byteCount := base58ReverseBlockSizes[lastBlockChars]
decoded, err := decodeBlock(blockStr, byteCount)
if err != nil {
return nil, err
}
result = append(result, decoded...)
}
return result, nil
}
// decodeBlock decodes a base58 string block into the specified number of bytes.
func decodeBlock(s string, byteCount int) ([]byte, error) {
num := new(big.Int)
base := big.NewInt(58)
for _, c := range []byte(s) {
idx := base58CharIndex(c)
if idx < 0 {
return nil, fmt.Errorf("types: invalid base58 character %q", c)
}
num.Mul(num, base)
num.Add(num, big.NewInt(int64(idx)))
}
// Convert to fixed-size byte array, big-endian.
raw := num.Bytes()
if len(raw) > byteCount {
return nil, fmt.Errorf("types: base58 block overflow: decoded %d bytes, expected %d", len(raw), byteCount)
}
// Pad with leading zeroes if necessary.
result := make([]byte, byteCount)
copy(result[byteCount-len(raw):], raw)
return result, nil
}
// base58CharIndex returns the index of character c in the base58 alphabet,
// or -1 if the character is not in the alphabet.
func base58CharIndex(c byte) int {
for i, ch := range []byte(base58Alphabet) {
if ch == c {
return i
}
}
return -1
}

175
types/address_test.go Normal file
View file

@ -0,0 +1,175 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package types
import (
"testing"
"forge.lthn.ai/core/go-blockchain/config"
)
// makeTestAddress creates an address with deterministic test data.
func makeTestAddress(flags uint8) *Address {
addr := &Address{Flags: flags}
// Fill keys with recognisable patterns.
for i := 0; i < 32; i++ {
addr.SpendPublicKey[i] = byte(i)
addr.ViewPublicKey[i] = byte(32 + i)
}
return addr
}
func TestAddressEncodeDecodeRoundTrip_Good(t *testing.T) {
tests := []struct {
name string
prefix uint64
flags uint8
}{
{"standard_address", config.AddressPrefix, 0x00},
{"integrated_address", config.IntegratedAddressPrefix, 0x00},
{"auditable_address", config.AuditableAddressPrefix, FlagAuditable},
{"auditable_integrated", config.AuditableIntegratedAddressPrefix, FlagAuditable},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := makeTestAddress(tt.flags)
encoded := original.Encode(tt.prefix)
if len(encoded) == 0 {
t.Fatal("Encode returned empty string")
}
decoded, decodedPrefix, err := DecodeAddress(encoded)
if err != nil {
t.Fatalf("DecodeAddress failed: %v", err)
}
if decodedPrefix != tt.prefix {
t.Errorf("prefix mismatch: got 0x%x, want 0x%x", decodedPrefix, tt.prefix)
}
if decoded.SpendPublicKey != original.SpendPublicKey {
t.Errorf("SpendPublicKey mismatch: got %s, want %s",
decoded.SpendPublicKey, original.SpendPublicKey)
}
if decoded.ViewPublicKey != original.ViewPublicKey {
t.Errorf("ViewPublicKey mismatch: got %s, want %s",
decoded.ViewPublicKey, original.ViewPublicKey)
}
if decoded.Flags != original.Flags {
t.Errorf("Flags mismatch: got 0x%02x, want 0x%02x", decoded.Flags, original.Flags)
}
})
}
}
func TestAddressEncodeDeterministic_Good(t *testing.T) {
// Encoding the same address twice must produce the same string.
addr := makeTestAddress(0x00)
first := addr.Encode(config.AddressPrefix)
second := addr.Encode(config.AddressPrefix)
if first != second {
t.Errorf("non-deterministic encoding:\n first: %s\n second: %s", first, second)
}
}
func TestAddressIsAuditable_Good(t *testing.T) {
addr := makeTestAddress(FlagAuditable)
if !addr.IsAuditable() {
t.Error("address with FlagAuditable should report IsAuditable() == true")
}
nonAuditable := makeTestAddress(0x00)
if nonAuditable.IsAuditable() {
t.Error("address without FlagAuditable should report IsAuditable() == false")
}
}
func TestIsIntegratedPrefix_Good(t *testing.T) {
if !IsIntegratedPrefix(config.IntegratedAddressPrefix) {
t.Error("IntegratedAddressPrefix should be recognised as integrated")
}
if !IsIntegratedPrefix(config.AuditableIntegratedAddressPrefix) {
t.Error("AuditableIntegratedAddressPrefix should be recognised as integrated")
}
if IsIntegratedPrefix(config.AddressPrefix) {
t.Error("AddressPrefix should not be recognised as integrated")
}
}
func TestDecodeAddress_Bad(t *testing.T) {
tests := []struct {
name string
input string
}{
{"empty_string", ""},
{"invalid_base58_char", "0OIl"},
{"too_short", "1111"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, err := DecodeAddress(tt.input)
if err == nil {
t.Error("expected error for invalid input, got nil")
}
})
}
}
func TestDecodeAddressChecksumCorruption_Bad(t *testing.T) {
addr := makeTestAddress(0x00)
encoded := addr.Encode(config.AddressPrefix)
// Corrupt the last character of the encoded string to break the checksum.
corrupted := []byte(encoded)
lastChar := corrupted[len(corrupted)-1]
if lastChar == '1' {
corrupted[len(corrupted)-1] = '2'
} else {
corrupted[len(corrupted)-1] = '1'
}
_, _, err := DecodeAddress(string(corrupted))
if err == nil {
t.Error("expected checksum error for corrupted address, got nil")
}
}
func TestBase58RoundTrip_Good(t *testing.T) {
// Test the underlying base58 encode/decode with known data.
testData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
encoded := base58Encode(testData)
decoded, err := base58Decode(encoded)
if err != nil {
t.Fatalf("base58 round-trip failed: %v", err)
}
if len(decoded) != len(testData) {
t.Fatalf("base58 round-trip length mismatch: got %d, want %d", len(decoded), len(testData))
}
for i := range testData {
if decoded[i] != testData[i] {
t.Errorf("base58 round-trip byte %d: got 0x%02x, want 0x%02x", i, decoded[i], testData[i])
}
}
}
func TestBase58Empty_Ugly(t *testing.T) {
// Encoding empty data should produce an empty string.
result := base58Encode(nil)
if result != "" {
t.Errorf("base58Encode(nil) = %q, want empty string", result)
}
// Decoding empty string should return an error.
_, err := base58Decode("")
if err == nil {
t.Error("base58Decode(\"\") should return an error")
}
}

47
types/block.go Normal file
View file

@ -0,0 +1,47 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
package types
// BlockHeader contains the fields present in every block header. These fields
// are consensus-critical and must be serialised in the exact order defined by
// the CryptoNote wire format.
type BlockHeader struct {
// MajorVersion determines which consensus rules apply to this block.
// The version increases at hardfork boundaries.
MajorVersion uint8
// MinorVersion is used for soft-fork signalling within a major version.
MinorVersion uint8
// Timestamp is the Unix epoch time (seconds) when the block was created.
// For PoS blocks this is the kernel timestamp; for PoW blocks it is the
// miner's claimed time.
Timestamp uint64
// PrevID is the hash of the previous block in the chain.
PrevID Hash
// Nonce is the value iterated by the miner to find a valid PoW solution.
// For PoS blocks this field carries the stake modifier.
Nonce uint64
}
// Block is a complete block including the header, miner (coinbase) transaction,
// and the hashes of all other transactions included in the block.
type Block struct {
BlockHeader
// MinerTx is the coinbase transaction that pays the block reward.
MinerTx Transaction
// TxHashes contains the hashes of all non-coinbase transactions included
// in this block, in the order they appear.
TxHashes []Hash
}

143
types/transaction.go Normal file
View file

@ -0,0 +1,143 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
package types
// Transaction version constants matching the C++ TRANSACTION_VERSION_* defines.
const (
// VersionInitial is the genesis/coinbase transaction version.
VersionInitial uint8 = 0
// VersionPreHF4 is the standard transaction version before hardfork 4.
VersionPreHF4 uint8 = 1
// VersionPostHF4 is the Zarcanum transaction version introduced at HF4.
VersionPostHF4 uint8 = 2
// VersionPostHF5 is the confidential assets transaction version from HF5.
VersionPostHF5 uint8 = 3
)
// Transaction represents a Lethean blockchain transaction. The structure
// covers all transaction versions (0 through 3) with version-dependent
// interpretation of inputs and outputs.
type Transaction struct {
// Version determines the transaction format and which consensus rules
// apply to validation.
Version uint8
// UnlockTime is the block height or Unix timestamp after which the
// outputs of this transaction become spendable. A value of 0 means
// immediately spendable (after the standard unlock window).
UnlockTime uint64
// Vin contains all transaction inputs.
Vin []TxInput
// Vout contains all transaction outputs.
Vout []TxOutput
// Extra holds auxiliary data such as the transaction public key,
// payment IDs, and other per-transaction metadata. The format is a
// sequence of tagged TLV fields.
Extra []byte
}
// TxInput is the interface implemented by all transaction input types.
// Each concrete type corresponds to a different input variant in the
// CryptoNote protocol.
type TxInput interface {
// InputType returns the wire type tag for this input variant.
InputType() uint8
}
// TxOutput is the interface implemented by all transaction output types.
type TxOutput interface {
// OutputType returns the wire type tag for this output variant.
OutputType() uint8
}
// Input type tags matching the C++ serialisation tags.
const (
InputTypeGenesis uint8 = 0xFF // txin_gen (coinbase)
InputTypeToKey uint8 = 0x02 // txin_to_key (standard spend)
)
// Output type tags.
const (
OutputTypeBare uint8 = 0x02 // tx_out_bare (transparent output)
OutputTypeZarcanum uint8 = 0x03 // tx_out_zarcanum (confidential output)
)
// TxInputGenesis is the coinbase input that appears in miner transactions.
// It has no real input data; only the block height is recorded.
type TxInputGenesis struct {
// Height is the block height this coinbase transaction belongs to.
Height uint64
}
// InputType returns the wire type tag for genesis (coinbase) inputs.
func (t TxInputGenesis) InputType() uint8 { return InputTypeGenesis }
// TxInputToKey is a standard input that spends a previously received output
// by proving knowledge of the corresponding secret key via a ring signature.
type TxInputToKey struct {
// Amount is the input amount in atomic units. For pre-HF4 transparent
// transactions this is the real amount; for HF4+ Zarcanum transactions
// this is zero (amounts are hidden in Pedersen commitments).
Amount uint64
// KeyOffsets contains the relative offsets into the global output index
// that form the decoy ring. The first offset is absolute; subsequent
// offsets are relative to the previous one.
KeyOffsets []uint64
// KeyImage is the key image that prevents double-spending of this input.
KeyImage KeyImage
}
// InputType returns the wire type tag for key inputs.
func (t TxInputToKey) InputType() uint8 { return InputTypeToKey }
// TxOutputBare is a transparent (pre-Zarcanum) transaction output.
type TxOutputBare struct {
// Amount is the output amount in atomic units.
Amount uint64
// TargetKey is the one-time public key derived from the recipient's
// address and the transaction secret key.
TargetKey PublicKey
}
// OutputType returns the wire type tag for bare outputs.
func (t TxOutputBare) OutputType() uint8 { return OutputTypeBare }
// TxOutputZarcanum is a confidential (HF4+) transaction output where the
// amount is hidden inside a Pedersen commitment.
type TxOutputZarcanum struct {
// StealthAddress is the one-time stealth address for this output.
StealthAddress PublicKey
// AmountCommitment is the Pedersen commitment to the output amount.
AmountCommitment PublicKey
// ConcealingPoint is an additional point used in the Zarcanum protocol
// for blinding.
ConcealingPoint PublicKey
// EncryptedAmount is the amount encrypted with a key derived from the
// shared secret between sender and recipient.
EncryptedAmount [32]byte
// MixAttr encodes the minimum ring size and other mixing attributes.
MixAttr uint8
}
// OutputType returns the wire type tag for Zarcanum outputs.
func (t TxOutputZarcanum) OutputType() uint8 { return OutputTypeZarcanum }

92
types/types.go Normal file
View file

@ -0,0 +1,92 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
// Package types defines the core cryptographic and blockchain data types for
// the Lethean protocol. All types are fixed-size byte arrays matching the
// CryptoNote specification.
package types
import (
"encoding/hex"
"fmt"
)
// Hash is a 256-bit (32-byte) hash value, typically produced by Keccak-256.
type Hash [32]byte
// String returns the hexadecimal representation of the hash.
func (h Hash) String() string {
return hex.EncodeToString(h[:])
}
// IsZero reports whether the hash is all zeroes.
func (h Hash) IsZero() bool {
for _, b := range h {
if b != 0 {
return false
}
}
return true
}
// HashFromHex parses a 64-character hexadecimal string into a Hash.
func HashFromHex(s string) (Hash, error) {
var h Hash
b, err := hex.DecodeString(s)
if err != nil {
return h, fmt.Errorf("types: invalid hex for hash: %w", err)
}
if len(b) != 32 {
return h, fmt.Errorf("types: hash hex must be 64 characters, got %d", len(s))
}
copy(h[:], b)
return h, nil
}
// PublicKey is a 256-bit Ed25519 public key.
type PublicKey [32]byte
// String returns the hexadecimal representation of the public key.
func (pk PublicKey) String() string {
return hex.EncodeToString(pk[:])
}
// PublicKeyFromHex parses a 64-character hexadecimal string into a PublicKey.
func PublicKeyFromHex(s string) (PublicKey, error) {
var pk PublicKey
b, err := hex.DecodeString(s)
if err != nil {
return pk, fmt.Errorf("types: invalid hex for public key: %w", err)
}
if len(b) != 32 {
return pk, fmt.Errorf("types: public key hex must be 64 characters, got %d", len(s))
}
copy(pk[:], b)
return pk, nil
}
// SecretKey is a 256-bit Ed25519 secret (private) key.
type SecretKey [32]byte
// String returns the hexadecimal representation of the secret key.
// Note: take care when logging or displaying secret keys.
func (sk SecretKey) String() string {
return hex.EncodeToString(sk[:])
}
// KeyImage is a 256-bit key image used for double-spend detection.
type KeyImage [32]byte
// String returns the hexadecimal representation of the key image.
func (ki KeyImage) String() string {
return hex.EncodeToString(ki[:])
}
// Signature is a 512-bit (64-byte) cryptographic signature.
type Signature [64]byte

75
wire/varint.go Normal file
View file

@ -0,0 +1,75 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
// Package wire provides binary serialisation primitives for the CryptoNote
// wire protocol. All encoding is consensus-critical and must be bit-identical
// to the C++ reference implementation.
package wire
import (
"errors"
)
// MaxVarintLen is the maximum number of bytes a CryptoNote varint can occupy.
// A uint64 requires at most 10 bytes of 7-bit encoding (64 bits / 7 = ~9.14,
// so values above 2^63-1 need a 10th byte).
const MaxVarintLen = 10
// ErrVarintOverflow is returned when a varint exceeds the maximum allowed
// length of 10 bytes.
var ErrVarintOverflow = errors.New("wire: varint overflow (exceeds 10 bytes)")
// ErrVarintEmpty is returned when attempting to decode a varint from an
// empty byte slice.
var ErrVarintEmpty = errors.New("wire: cannot decode varint from empty data")
// EncodeVarint encodes a uint64 value as a CryptoNote variable-length integer.
//
// The encoding uses 7 bits per byte, with the most significant bit (MSB) set
// to 1 to indicate that more bytes follow. This is the same scheme as protobuf
// varints but limited to 9 bytes maximum for uint64 values.
func EncodeVarint(v uint64) []byte {
if v == 0 {
return []byte{0x00}
}
var buf [MaxVarintLen]byte
n := 0
for v > 0 {
buf[n] = byte(v & 0x7f)
v >>= 7
if v > 0 {
buf[n] |= 0x80
}
n++
}
return append([]byte(nil), buf[:n]...)
}
// DecodeVarint decodes a CryptoNote variable-length integer from the given
// byte slice. It returns the decoded value, the number of bytes consumed,
// and any error encountered.
func DecodeVarint(data []byte) (uint64, int, error) {
if len(data) == 0 {
return 0, 0, ErrVarintEmpty
}
var v uint64
for i := 0; i < len(data) && i < MaxVarintLen; i++ {
v |= uint64(data[i]&0x7f) << (7 * uint(i))
if data[i]&0x80 == 0 {
return v, i + 1, nil
}
}
// If we read MaxVarintLen bytes and the last one still has the
// continuation bit set, or if we ran out of data with the continuation
// bit still set, that is an overflow.
if len(data) >= MaxVarintLen {
return 0, 0, ErrVarintOverflow
}
return 0, 0, ErrVarintOverflow
}

126
wire/varint_test.go Normal file
View file

@ -0,0 +1,126 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package wire
import (
"math"
"testing"
)
func TestEncodeVarint_Good(t *testing.T) {
tests := []struct {
name string
value uint64
want []byte
}{
{"zero", 0, []byte{0x00}},
{"one", 1, []byte{0x01}},
{"max_single_byte", 127, []byte{0x7f}},
{"128", 128, []byte{0x80, 0x01}},
{"255", 255, []byte{0xff, 0x01}},
{"256", 256, []byte{0x80, 0x02}},
{"16384", 16384, []byte{0x80, 0x80, 0x01}},
{"65535", 65535, []byte{0xff, 0xff, 0x03}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := EncodeVarint(tt.value)
if len(got) != len(tt.want) {
t.Fatalf("EncodeVarint(%d) = %x (len %d), want %x (len %d)",
tt.value, got, len(got), tt.want, len(tt.want))
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("EncodeVarint(%d)[%d] = 0x%02x, want 0x%02x",
tt.value, i, got[i], tt.want[i])
}
}
})
}
}
func TestDecodeVarint_Good(t *testing.T) {
tests := []struct {
name string
data []byte
wantVal uint64
wantLen int
}{
{"zero", []byte{0x00}, 0, 1},
{"one", []byte{0x01}, 1, 1},
{"127", []byte{0x7f}, 127, 1},
{"128", []byte{0x80, 0x01}, 128, 2},
{"16384", []byte{0x80, 0x80, 0x01}, 16384, 3},
// With trailing data — should only consume the varint bytes.
{"with_trailing", []byte{0x01, 0xff, 0xff}, 1, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val, n, err := DecodeVarint(tt.data)
if err != nil {
t.Fatalf("DecodeVarint(%x) returned error: %v", tt.data, err)
}
if val != tt.wantVal {
t.Errorf("DecodeVarint(%x) value = %d, want %d", tt.data, val, tt.wantVal)
}
if n != tt.wantLen {
t.Errorf("DecodeVarint(%x) length = %d, want %d", tt.data, n, tt.wantLen)
}
})
}
}
func TestVarintRoundTrip_Good(t *testing.T) {
values := []uint64{
0, 1, 127, 128, 255, 256, 1000, 65535, 65536,
1<<14 - 1, 1 << 14, 1<<21 - 1, 1 << 21,
1<<28 - 1, 1 << 28, 1<<35 - 1, 1 << 35,
1<<42 - 1, 1 << 42, 1<<49 - 1, 1 << 49,
1<<56 - 1, 1 << 56, math.MaxUint64,
}
for _, v := range values {
encoded := EncodeVarint(v)
decoded, n, err := DecodeVarint(encoded)
if err != nil {
t.Errorf("round-trip failed for %d: encode→%x, decode error: %v", v, encoded, err)
continue
}
if decoded != v {
t.Errorf("round-trip failed for %d: encode→%x, decode→%d", v, encoded, decoded)
}
if n != len(encoded) {
t.Errorf("round-trip for %d: consumed %d bytes, encoded %d bytes", v, n, len(encoded))
}
}
}
func TestDecodeVarint_Bad(t *testing.T) {
// Empty input.
_, _, err := DecodeVarint(nil)
if err != ErrVarintEmpty {
t.Errorf("DecodeVarint(nil) error = %v, want ErrVarintEmpty", err)
}
_, _, err = DecodeVarint([]byte{})
if err != ErrVarintEmpty {
t.Errorf("DecodeVarint([]) error = %v, want ErrVarintEmpty", err)
}
}
func TestDecodeVarint_Ugly(t *testing.T) {
// A varint with all continuation bits set for 11 bytes (overflow).
overflow := make([]byte, 11)
for i := range overflow {
overflow[i] = 0x80
}
_, _, err := DecodeVarint(overflow)
if err != ErrVarintOverflow {
t.Errorf("DecodeVarint(overflow) error = %v, want ErrVarintOverflow", err)
}
}