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:
commit
4c0b7f290e
16 changed files with 2158 additions and 0 deletions
46
CLAUDE.md
Normal file
46
CLAUDE.md
Normal 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
465
config/config.go
Normal 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
168
config/config_test.go
Normal 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
95
config/hardfork.go
Normal 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
197
config/hardfork_test.go
Normal 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
100
difficulty/difficulty.go
Normal 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
|
||||
}
|
||||
129
difficulty/difficulty_test.go
Normal file
129
difficulty/difficulty_test.go
Normal 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
7
go.mod
Normal 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
4
go.sum
Normal 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
289
types/address.go
Normal 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
175
types/address_test.go
Normal 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
47
types/block.go
Normal 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
143
types/transaction.go
Normal 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
92
types/types.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.
|
||||
// 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
75
wire/varint.go
Normal 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
126
wire/varint_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue