Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ca1bdfd
Rename miner address to payload root
lploom Mar 17, 2026
4130e90
BFT snapshots
lploom Mar 17, 2026
b39f6a4
fix import
lploom Mar 26, 2026
4529b97
fix genesis block hashes
lploom Mar 26, 2026
8336ef4
Update fuzzer for new block structure
lploom Mar 30, 2026
8623efd
Update getblocktemplate and submitblock for new block structure
lploom Mar 31, 2026
e6cbe81
Track last UTB in block index
lploom Apr 1, 2026
66c8423
Add CBlockHeader::MAX_PAYLOAD_SIZE
lploom Apr 1, 2026
0c2cd0f
Improve trust base verification
lploom Apr 1, 2026
c6501af
Cross-platform random bytes
lploom Apr 1, 2026
ec992bf
Add trust base total stake overflow checks
lploom Apr 1, 2026
ba42a92
Truncate external error message before logging
lploom Apr 1, 2026
3ca823c
Add MAX_BFT_RESPONSE_SIZE
lploom Apr 1, 2026
1808636
Refactor bft client
lploom Apr 1, 2026
8459b36
Optimize CBlockHeader serialization
lploom Apr 1, 2026
eb1d706
Refactor HttpBFTClient to use Pimpl for httplib::Client
lploom Apr 2, 2026
c11d6b0
Use secp256k1_context_static for signature verification
lploom Apr 2, 2026
d4502a0
Restrict permissions of miner_state.json to owner-only
lploom Apr 2, 2026
7fc7036
More reliable initialization in HttpBFTClient
lploom Apr 2, 2026
991e10c
Fix CBlockIndex::ToString
lploom Apr 2, 2026
0d1e803
Remove unused imports
lploom Apr 2, 2026
582deec
Remove a redundant deep copy
lploom Apr 2, 2026
ebd5485
Remove redundant field
lploom Apr 2, 2026
446f267
Consolidate error handling
lploom Apr 2, 2026
cd3a256
Add trust base validation to CheckBlockHeader
lploom Apr 6, 2026
075c64a
Wrap miner worker thread in try-catch
lploom Apr 7, 2026
101df88
Reorder RPCServer params to match the declaration
lploom Apr 7, 2026
4776ad9
Use same version in HandleGetBlockTemplate and miner
lploom Apr 7, 2026
b502252
Extract CBOR tag prepending into a helper function
lploom Apr 7, 2026
8cbd497
Use correct epoch if trust base parsing fails
lploom Apr 7, 2026
c971e00
Update submitheader for new block structure
lploom Apr 7, 2026
a42d9f9
Merge branch 'main' into bft-snapshots
lploom Apr 8, 2026
ee14107
Fix submitheader payload handling and testnet functional tests
lploom Apr 8, 2026
f7d9fdb
Mirror request id in rpc response
lploom Apr 14, 2026
fdc8e3a
Update trust base cbor tags
lploom Apr 14, 2026
9371d1c
Update trust base cbor tags
lploom Apr 14, 2026
fc15973
Minor: wording improvements in the block payload format spec
ahtotruu Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Track prebuilt test chains via Git LFS

test/functional/test_chains/** filter=lfs diff=lfs merge=lfs -text

# keep bash scripts in LF
*.sh text eol=lf
39 changes: 39 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Enable PIC (Position Independent Code) for all targets
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Default to Release build
if(NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "")
set(CMAKE_BUILD_TYPE "Release")
Expand Down Expand Up @@ -206,6 +209,31 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(json)

# cpp-httplib - Single header HTTP library
FetchContent_Declare(
httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.38.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The specified git tag v0.38.0 for cpp-httplib does not appear to exist in the official yhirose/cpp-httplib repository. The latest version seems to follow a v0.major.minor scheme (e.g., v0.15.3). This could be a typo and might cause build failures for users who do not have this specific commit cached. Please verify the tag and use a valid, existing tag to ensure build reproducibility.

GIT_SHALLOW TRUE
)
set(HTTPLIB_COMPILE OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(httplib)

# libsecp256k1 - Optimized C library for EC operations on curve secp256k1
FetchContent_Declare(
libsecp256k1
GIT_REPOSITORY https://github.com/bitcoin-core/secp256k1.git
GIT_TAG v0.7.1
GIT_SHALLOW TRUE
)
set(SECP256K1_BUILD_BENCHMARK OFF CACHE BOOL "" FORCE)
set(SECP256K1_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(SECP256K1_BUILD_EXHAUSTIVE_TESTS OFF CACHE BOOL "" FORCE)
set(SECP256K1_ECMULT_WINDOW_SIZE 15 CACHE STRING "" FORCE)
set(SECP256K1_ECMULT_GEN_WINDOW_SIZE 4 CACHE STRING "" FORCE)
set(SECP256K1_ENABLE_MODULE_RECOVERY ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(libsecp256k1)

# miniupnpc - UPnP NAT traversal
FetchContent_Declare(
miniupnpc
Expand Down Expand Up @@ -266,6 +294,11 @@ add_library(chain STATIC
src/chain/chainstate_manager.cpp
src/chain/timedata.cpp
src/chain/miner.cpp
src/chain/token_generator.cpp
src/chain/token_manager.cpp
src/chain/trust_base.cpp
src/chain/bft_client.cpp
src/chain/trust_base_manager.cpp
)

target_include_directories(chain PUBLIC
Expand All @@ -278,10 +311,16 @@ if(randomx_POPULATED)
target_include_directories(chain PUBLIC ${randomx_SOURCE_DIR}/src)
endif()

FetchContent_GetProperties(httplib)
if(httplib_POPULATED)
target_include_directories(chain PUBLIC ${httplib_SOURCE_DIR})
endif()

target_link_libraries(chain PUBLIC
util
randomx
nlohmann_json::nlohmann_json
secp256k1
)

# Network Library - P2P networking and RPC
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Unicity adopts proven architectural patterns from Bitcoin Core for P2P networkin
**Unicity-Specific Design:**
- **Proof-of-work**: RandomX (ASIC-resistant) instead of SHA256d
- **Difficulty adjustment**: ASERT per-block instead of 2016-block retargeting
- **Block headers**: 100 bytes (includes miner address + RandomX hash)
- **Block headers**: 112 bytes fixed header + variable-length payload
- **Block payload**: Contains mandatory 32-byte Miner Reward Token ID hash and optional BFT Trust Base snapshot
- **Target spacing**: 2.4 hours instead of 10 minutes
- **No transaction layer**: No mempool, UTXO set, or transaction processing

Expand Down Expand Up @@ -123,6 +124,15 @@ See [test/README.md](test/README.md) for testing documentation.
./build/bin/unicity-cli startmining
```

### BFT Integration

Running on **testnet** and **mainnet** requires BFT integration to verify the genesis block and sync trust bases.
- **Default address**: `--bftaddr=http://127.0.0.1:25866`
- **Verification**: If the BFT node is not accessible at the specified address, genesis block verification will fail and the node will not start.
- **Disabling**: BFT integration can be explicitly disabled by setting the address to an empty string: `--bftaddr=""`.
- **Regtest**: In `regtest` mode, BFT integration is **disabled by default**. To enable it, you must explicitly provide an address, e.g., `--bftaddr=http://127.0.0.1:25866`.


### Using Docker

For production deployments, Docker is recommended:
Expand Down
9 changes: 9 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ PORT="${UNICITY_PORT:-9590}"
LISTEN="${UNICITY_LISTEN:-1}"
SERVER="${UNICITY_SERVER:-0}"
VERBOSE="${UNICITY_VERBOSE:-0}"
BFTADDR="${UNICITY_BFTADDR:-}"
NETWORK="${UNICITY_NETWORK:-mainnet}"
LOGLEVEL="${UNICITY_LOGLEVEL:-}"
DEBUG="${UNICITY_DEBUG:-}"
Expand Down Expand Up @@ -66,6 +67,11 @@ fi

# Note: RPC server is always enabled, no --server flag needed

# BFT RPC address
if [ -n "$BFTADDR" ]; then
ARGS+=("--bftaddr=$BFTADDR")
fi

# Verbose logging
if [ "$VERBOSE" = "1" ]; then
ARGS+=("--verbose")
Expand Down Expand Up @@ -97,6 +103,9 @@ echo "Port: $PORT"
echo "Listen: $LISTEN"
echo "Server: $SERVER"
echo "Verbose: $VERBOSE"
if [ -n "$BFTADDR" ]; then
echo "BFT Addr: $BFTADDR"
fi
if [ -n "$LOGLEVEL" ]; then
echo "Log Level: $LOGLEVEL"
fi
Expand Down
29 changes: 18 additions & 11 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,19 @@ The chain layer validates block headers and maintains the blockchain state:

### Data Structures

#### Block Header (100 bytes)
#### Block Header (112 bytes)

The fundamental unit of the blockchain:

| Field | Size | Description |
|-------|------|-------------|
| nVersion | 4 bytes | Block version |
| hashPrevBlock | 32 bytes | Previous block hash |
| minerAddress | 20 bytes | Miner reward address (replaces merkleRoot) |
| nTime | 4 bytes | Unix timestamp |
| nBits | 4 bytes | Difficulty target (compact format) |
| nNonce | 4 bytes | PoW nonce |
| hashRandomX | 32 bytes | RandomX hash (PoW commitment) |

| Field | Size | Description |
|---------------|----------|----------------------------------------------------------|
| nVersion | 4 bytes | Block version |
| hashPrevBlock | 32 bytes | Previous block hash |
| payloadRoot | 32 bytes | Payload hash (Hash of reward token id hash and UTB hash) |
| nTime | 4 bytes | Unix timestamp |
| nBits | 4 bytes | Difficulty target (compact format) |
| nNonce | 4 bytes | PoW nonce |
| hashRandomX | 32 bytes | RandomX hash (PoW commitment) |

#### Block Index

Expand Down Expand Up @@ -188,6 +187,14 @@ After Reorg:
- **Epoch-based**: Dataset changes periodically
- **Two-phase verification**: Fast commitment check, then full RandomX

#### BFT Integration

Unicity integrates with the BFT layer to establish periodic checkpoints.
- **Verification**: The node verifies the hardcoded genesis UTB against the epoch 1 trust base fetched from the BFT network.
- **Requirement**: BFT integration is required for **testnet** and **mainnet**. The node will fail to start if it cannot connect to the BFT node at the configured `bftaddr`.
- **Disabling**: Integration can be disabled by setting `bftaddr` to an empty string. This is the **default for regtest**.
- **Epoch 1**: When disabled, the node assumes the hardcoded genesis UTB is valid without network verification.

#### ASERT Difficulty Adjustment

Formula: `target_new = target_ref * 2^((time_diff - ideal_time) / half_life)`
Expand Down
71 changes: 55 additions & 16 deletions docs/SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

## Overview

Unicity is a headers-only blockchain protocol. Nodes exchange block headers (100 bytes each) without transaction data. The protocol uses a Bitcoin like wire format with custom network identifiers.
Unicity is a headers-only blockchain protocol. Nodes exchange block headers (112 bytes each) without transaction data. The protocol uses a Bitcoin like wire format with custom network identifiers.

## Network Parameters

Expand Down Expand Up @@ -69,19 +69,51 @@ All integers use little-endian byte order except where noted.
| timestamp | 4 | uint32_t | Unix timestamp (LE) |
| address | 26 | NetworkAddress | Network address |

### Block Header (112 bytes)

| Field | Size | Type | Description |
|---------------|------|----------|---------------------|
| nVersion | 4 | int32_t | Block version |
| hashPrevBlock | 32 | uint256 | Previous block hash |
| payloadRoot | 32 | uint256 | Hash of payload |
| nTime | 4 | uint32_t | Timestamp |
| nBits | 4 | uint32_t | Difficulty target |
| nNonce | 4 | uint32_t | Nonce |
| hashRandomX | 32 | uint256 | RandomX hash |

### Block Header (100 bytes)
### Block Payload

| Field | Size | Type | Description |
|-------|------|------|-------------|
| nVersion | 4 | int32_t | Block version |
| hashPrevBlock | 32 | uint256 | Previous block hash |
| minerAddress | 20 | uint160 | Miner address |
| nTime | 4 | uint32_t | Timestamp |
| nBits | 4 | uint32_t | Difficulty target |
| nNonce | 4 | uint32_t | Nonce |
| hashRandomX | 32 | uint256 | RandomX hash |
The block payload is committed to by the `payloadRoot` in the block header. It is transmitted alongside the header in
the `HEADERS` message.

| Field | Size | Description |
|------------------------|------|------------------------------------------------|
| minerRewardTokenIdHash | 32 | SHA256 hash of the Miner Reward Token ID |
| trustBaseRecord | Var | Optional CBOR-encoded Unicity Trust Base (UTB) |

The preimage of `minerRewardTokenIdHash` (the actual token ID) is generated by the miner and stored locally (e.g., in `reward_tokens.csv`).

The `trustBaseRecord` field is only present if the BFT Trust Base has changed since the previous block. If no update is available, this field is omitted.

The `payloadRoot` is calculated as `SHA256(leaf_0 || leaf_1)`, where `leaf_0` is the `minerRewardTokenIdHash` and `leaf_1` is the `SHA256` hash of the `trustBaseRecord` (or a zero-filled `uint256` if the record is absent).

### Miner Reward Tokens

For every block mined, the miner generates a unique **Reward Token ID**.

- **TokenID Generation**: `TokenID = SHA256(Seed || Counter)`
- `Seed`: A persistent 32-byte cryptographically secure random value generated once per miner instance.
- `Counter`: An 8-byte (uint64_t) little-endian incrementing counter.
- **Commitment**: The block header commits to the **Hash of the Token ID** (`SHA256(TokenID)`). This hash is stored as the first 32 bytes of the block payload (`minerRewardTokenIdHash`).
- **Privacy**: Only the hash of the Token ID is public on the blockchain. The actual Token ID (preimage) is kept private by the miner.
- **Local Logging**: Successful miners must store the mapping between the block and the generated Token ID locally. By default, this is saved in `reward_tokens.csv` in the node's data directory.

**Example `reward_tokens.csv` format:**
```csv
Height,BlockHash,TokenID
3,2caef7f1d57eeeef6b34d1abdadbf8b0a9937dd95ec38287c1d4e928d932bb3e,9621e355e0dc5fa287bafc36face5b7a2486a8860588f09156384a7489b59b7c
4,f250f586b509aba4f1790b0a0e25d0f4767ecccc0c1aaab590af6805da687d2f,112029fb4744e7fec8deb93f67d383b2aab51126815ce905a26194f442af7b35
```

## Messages

Expand Down Expand Up @@ -148,12 +180,19 @@ Request block headers.

### HEADERS

Send block headers.
Send block headers. Headers are variable-length.

| Field | Type | Description |
|-------|------|-------------|
| count | VarInt | Number of headers (max 80000) |
| headers | BlockHeader[] | Block headers |
| Field | Type | Description |
|---------|---------------|-------------------------------------|
| count | VarInt | Number of block headers (max 80000) |
| headers | BlockHeader[] | List of block headers |

**BlockHeader Structure:**

| Field | Type | Description |
|-------|--------|----------------------------------------------|
| size | VarInt | Total size of the following block data |
| block | Bytes | Serialized block (112-byte header + payload) |

## Protocol Flows

Expand Down
10 changes: 5 additions & 5 deletions fuzz/fuzz_block_header.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (success && !header.IsNull()) {
auto serialized = header.Serialize();

// CRITICAL: Validate serialized size is exactly BLOCK_HEADER_SIZE (100 bytes)
// Unicity headers: 4 + 32 + 20 + 4 + 4 + 4 + 32 = 100 bytes
// (version + prevHash + minerAddress + time + bits + nonce + hashRandomX)
// CRITICAL: Validate serialized size is exactly BLOCK_HEADER_SIZE (112 bytes)
// Unicity headers: 4 + 32 + 32 + 4 + 4 + 4 + 32 = 112 bytes
// (version + prevHash + payloadRoot + time + bits + nonce + hashRandomX)
if (serialized.size() != CBlockHeader::HEADER_SIZE) {
// Serialize() produced wrong size - BUG!
__builtin_trap();
Expand All @@ -45,8 +45,8 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
__builtin_trap();
}

if (header.minerAddress != header2.minerAddress) {
// Miner address changed during round-trip - BUG!
if (header.payloadRoot != header2.payloadRoot) {
// Payload root changed during round-trip - BUG!
__builtin_trap();
}

Expand Down
25 changes: 19 additions & 6 deletions fuzz/fuzz_chain_reorg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#include <memory>
#include <cstring>

#include "../test/common/mock_trust_base_manager.hpp"
#include "../test/common/test_chainstate_manager.hpp"

using namespace unicity;
using namespace unicity::chain;
using namespace unicity::validation;
Expand All @@ -31,7 +34,8 @@ using namespace unicity::consensus;
class FuzzChainstateManager : public ChainstateManager {
public:
FuzzChainstateManager(const ChainParams& params)
: ChainstateManager(params) {}
: FuzzChainstateManager(params, std::make_unique<test::MockTrustBaseManager>())
{}

// Override PoW check to always pass (we're fuzzing chain logic, not RandomX)
bool CheckProofOfWork(const CBlockHeader& header, crypto::POWVerifyMode mode) const override {
Expand All @@ -56,6 +60,14 @@ class FuzzChainstateManager : public ChainstateManager {
}
return true;
}

private:
FuzzChainstateManager(const chain::ChainParams& params, std::unique_ptr<test::MockTrustBaseManager> tbm)
: ChainstateManager(params, *tbm),
mock_tbm_(std::move(tbm))
{}

std::unique_ptr<test::MockTrustBaseManager> mock_tbm_;
};

// Fuzz input parser
Expand Down Expand Up @@ -115,7 +127,7 @@ CBlockHeader BuildFuzzHeader(FuzzInput& input, const uint256& prevHash, uint32_t

// Fuzz miner address (just use random bytes)
for (size_t i = 0; i < 20; i++) {
header.minerAddress.data()[i] = input.ReadByte();
header.payloadRoot.data()[i] = input.ReadByte();
}

// Time: base + small offset to keep roughly increasing
Expand All @@ -124,6 +136,7 @@ CBlockHeader BuildFuzzHeader(FuzzInput& input, const uint256& prevHash, uint32_t

// Difficulty: use easy target for fuzzing
header.nBits = 0x207fffff; // Very easy target
header.payloadRoot.SetHex("00");

// Nonce and RandomX hash (not validated in fuzz mode)
header.nNonce = input.ReadUInt32();
Expand Down Expand Up @@ -176,7 +189,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
baseTime = header.nTime + 120;

ValidationState state;
auto* pindex = chainstate.AcceptBlockHeader(header, state, true);
auto* pindex = chainstate.AcceptBlockHeader(header, state);
if (pindex) {
chain_tips[0] = header.GetHash();
}
Expand All @@ -198,7 +211,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
CBlockHeader header = BuildFuzzHeader(input, prevHash, baseTime);

ValidationState state;
auto* pindex = chainstate.AcceptBlockHeader(header, state, true);
auto* pindex = chainstate.AcceptBlockHeader(header, state);
if (pindex && chain_tips.size() < num_chains) {
chain_tips.push_back(header.GetHash());
}
Expand All @@ -216,7 +229,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
baseTime = header.nTime + 120;

ValidationState state;
auto* pindex = chainstate.AcceptBlockHeader(header, state, true);
auto* pindex = chainstate.AcceptBlockHeader(header, state);
if (pindex) {
chain_tips[tip_idx] = header.GetHash();
}
Expand All @@ -231,7 +244,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
CBlockHeader header = BuildFuzzHeader(input, fakeParent, baseTime);

ValidationState state;
chainstate.AcceptBlockHeader(header, state, input.ReadByte());
chainstate.AcceptBlockHeader(header, state);
// Might be orphaned or rejected
break;
}
Expand Down
Loading
Loading