Skip to content

Commit 6e4e564

Browse files
authored
Merge pull request #1 from distributed-lab/dev
SPVContract implementation
2 parents e4f8a9e + 585b94c commit 6e4e564

34 files changed

+544389
-879
lines changed

.solcover.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
module.exports = {
2-
skipFiles: [],
2+
skipFiles: ["mock/"],
33
configureYulOptimizer: true,
44
};

README.md

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
1-
# Hardhat template
1+
# SPV Contracts
22

3-
Template hardhat repository for ad-hoc smart contracts development.
3+
Smart contract for verifying Bitcoin block headers on EVM-compatible chains using the **Simple Payment Verification (SPV)** method.
44

5-
### How to use
5+
This contract behaves like an SPV node: it builds a valid chain of Bitcoin block headers, verifies them according to Bitcoin consensus rules, and enables Merkle Proof-based verification of Bitcoin transactions.
66

7-
The template works out of the box. To clean up the repo, you may need to delete the mock contracts, tests and migration files.
7+
## Features
88

9-
#### Compilation
9+
- Stores and verifies Bitcoin block headers
10+
- Validates headers using:
11+
- Proof of Work (`bits``target`)
12+
- Median time rule
13+
- Chain continuity
14+
- Handles difficulty adjustment every 2016 blocks
15+
- Supports pending difficulty epochs before finalization
16+
- Stores historical targets and supports reorg handling
17+
18+
## Contract: `SPVContract.sol`
19+
20+
### Key Functions
21+
22+
#### `addBlockHeader(bytes calldata blockHeaderRaw)`
23+
Adds and validates a new block header, updates internal state, and emits an event.
24+
25+
### Validation Rules
26+
- `prevBlockHash` must point to a known block
27+
- New `blockHash` must not exist
28+
- Header `bits` must match the expected network target
29+
- Header `time` must be > median of last 11 blocks
30+
- `blockHash` must be less than or equal to the target (valid PoW)
31+
32+
## Storage Structure
33+
34+
- `BlocksData` – stores block headers, timestamps, and chain height
35+
- `TargetsData` – handles target values and difficulty epochs
36+
- `pendingTargetHeightCount` – controls target finalization after N blocks
37+
38+
## Dev Info
39+
### Compilation
1040

1141
To compile the contracts, use the next script:
1242

1343
```bash
1444
npm run compile
1545
```
1646

17-
#### Test
47+
### Test
1848

1949
To run the tests, execute the following command:
2050

@@ -28,7 +58,7 @@ Or to see the coverage, run:
2858
npm run coverage
2959
```
3060

31-
#### Local deployment
61+
### Local deployment
3262

3363
To deploy the contracts locally, run the following commands (in the different terminals):
3464

@@ -37,28 +67,10 @@ npm run private-network
3767
npm run deploy-localhost
3868
```
3969

40-
#### Bindings
70+
### Bindings
4171

4272
The command to generate the bindings is as follows:
4373

4474
```bash
4575
npm run generate-types
46-
```
47-
48-
> See the full list of available commands in the `package.json` file.
49-
50-
### Integrated plugins
51-
52-
- Hardhat official `ethers` + `ethers-v6`
53-
- [`Typechain`](https://www.npmjs.com/package/@typechain/hardhat)
54-
- [`hardhat-migrate`](https://www.npmjs.com/package/@solarity/hardhat-migrate), [`hardhat-markup`](https://www.npmjs.com/package/@solarity/hardhat-markup), [`hardhat-gobind`](https://www.npmjs.com/package/@solarity/hardhat-gobind)
55-
- [`hardhat-zkit`](https://www.npmjs.com/package/@solarity/hardhat-zkit), [`chai-zkit`](https://www.npmjs.com/package/@solarity/chai-zkit)
56-
- [`hardhat-contract-sizer`](https://www.npmjs.com/package/hardhat-contract-sizer)
57-
- [`hardhat-gas-reporter`](https://www.npmjs.com/package/hardhat-gas-reporter)
58-
- [`solidity-coverage`](https://www.npmjs.com/package/solidity-coverage)
59-
60-
### Other niceties
61-
62-
- The template comes with presetup `prettier` and `solhint` that lint the project via `husky` before compilation hook.
63-
- The `.env.example` file is provided to check what is required as ENVs
64-
- Preinstalled `@openzeppelin/contracts` and `@solarity/solidity-lib`
76+
```

contracts/SPVContract.sol

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5+
6+
import {BlockHeader, BlockHeaderData} from "./libs/BlockHeader.sol";
7+
import {BlocksStorage} from "./libs/BlocksStorage.sol";
8+
import {TargetsStorage} from "./libs/targets/TargetsStorage.sol";
9+
import {TargetsHelper} from "./libs/targets/TargetsHelper.sol";
10+
11+
contract SPVContract is Initializable {
12+
using BlockHeader for bytes;
13+
using BlocksStorage for BlocksStorage.BlocksData;
14+
using TargetsStorage for TargetsStorage.TargetsData;
15+
16+
bytes32 public constant SPV_CONTRACT_STORAGE_SLOT =
17+
keccak256("spv.contract.spv.contract.storage");
18+
19+
error PrevBlockDoesNotExist(bytes32 prevBlockHash);
20+
error BlockAlreadyExists(bytes32 blockHash);
21+
22+
error InvalidTarget(bytes32 blockTarget, bytes32 networkTarget);
23+
error InvalidBlockHash(bytes32 actualBlockHash, bytes32 blockTarget);
24+
error InvalidBlockTime(uint32 blockTime, uint32 medianTime);
25+
26+
event BlockHeaderAdded(uint256 indexed blockHeight, bytes32 indexed blockHash);
27+
28+
struct SPVContractStorage {
29+
BlocksStorage.BlocksData blocksData;
30+
TargetsStorage.TargetsData targets;
31+
uint256 pendingTargetHeightCount;
32+
}
33+
34+
function __SPVContract_init(
35+
uint256 pendingBlockCount_,
36+
uint256 pendingTargetHeightCount_
37+
) external initializer {
38+
SPVContractStorage storage $ = _getSPVContractStorage();
39+
40+
$.blocksData.initialize(pendingBlockCount_);
41+
$.targets.initialize();
42+
43+
$.pendingTargetHeightCount = pendingTargetHeightCount_;
44+
}
45+
46+
function _getSPVContractStorage() internal pure returns (SPVContractStorage storage _spvs) {
47+
bytes32 slot_ = SPV_CONTRACT_STORAGE_SLOT;
48+
49+
assembly {
50+
_spvs.slot := slot_
51+
}
52+
}
53+
54+
function addBlockHeader(bytes calldata blockHeaderRaw_) external {
55+
SPVContractStorage storage $ = _getSPVContractStorage();
56+
57+
(BlockHeaderData memory blockHeader_, bytes32 blockHash_) = blockHeaderRaw_
58+
.parseBlockHeaderData();
59+
60+
require(!$.blocksData.blockExists(blockHash_), BlockAlreadyExists(blockHash_));
61+
require(
62+
$.blocksData.blockExists(blockHeader_.prevBlockHash),
63+
PrevBlockDoesNotExist(blockHeader_.prevBlockHash)
64+
);
65+
66+
uint256 blockHeight_ = $.blocksData.getBlockHeight(blockHeader_.prevBlockHash) + 1;
67+
68+
_tryConfirmPendingTarget(blockHeight_);
69+
70+
bytes32 target_ = _getRequiredTarget(blockHeight_, blockHeader_.prevBlockHash);
71+
72+
_validateBlockRules(blockHeader_, blockHash_, target_);
73+
74+
$.blocksData.addBlock(
75+
blockHeader_,
76+
blockHash_,
77+
blockHeight_,
78+
TargetsStorage.getBlockWork(target_)
79+
);
80+
81+
if (TargetsStorage.isTargetAdjustmentBlock(blockHeight_)) {
82+
$.targets.updatePendingTarget(
83+
blockHeight_,
84+
blockHash_,
85+
$.blocksData.getBlockTimeByBlockHeight(
86+
blockHeight_ - TargetsStorage.DIFFICULTY_ADJSTMENT_INTERVAL
87+
),
88+
blockHeader_.time
89+
);
90+
}
91+
92+
emit BlockHeaderAdded(blockHeight_, blockHash_);
93+
}
94+
95+
function _getRequiredTarget(
96+
uint256 blockHeight_,
97+
bytes32 prevBlockHash_
98+
) internal view returns (bytes32 target_) {
99+
SPVContractStorage storage $ = _getSPVContractStorage();
100+
101+
uint256 blockTargetEpoch_ = TargetsStorage.countTargetEpoch(blockHeight_);
102+
bool isPendingTargetRequired_ = blockTargetEpoch_ == $.targets.getPendingEpoch();
103+
104+
if (isPendingTargetRequired_) {
105+
uint256 epochBlockNumber_ = TargetsStorage.getEpochBlockNumber(blockHeight_);
106+
107+
for (uint256 i = 0; i < epochBlockNumber_ - 1; ++i) {
108+
prevBlockHash_ = $.blocksData.getPrevBlockHash(prevBlockHash_);
109+
}
110+
111+
target_ = $.targets.getPendingTarget(prevBlockHash_);
112+
} else {
113+
target_ = $.targets.getTarget(blockTargetEpoch_);
114+
}
115+
116+
assert(target_ > 0);
117+
}
118+
119+
function _tryConfirmPendingTarget(uint256 blockHeight_) internal {
120+
SPVContractStorage storage $ = _getSPVContractStorage();
121+
122+
if (!$.targets.hasPendingTarget() || blockHeight_ < $.pendingTargetHeightCount) {
123+
return;
124+
}
125+
126+
uint256 lastActiveBlockHeight_ = blockHeight_ - $.pendingTargetHeightCount;
127+
128+
if (TargetsStorage.isTargetAdjustmentBlock(lastActiveBlockHeight_)) {
129+
$.targets.confirmPendingTarget(
130+
$.blocksData.getBlockHashByBlockHeight(lastActiveBlockHeight_)
131+
);
132+
}
133+
}
134+
135+
function _validateBlockRules(
136+
BlockHeaderData memory blockHeader_,
137+
bytes32 blockHash_,
138+
bytes32 target_
139+
) internal view {
140+
SPVContractStorage storage $ = _getSPVContractStorage();
141+
142+
bytes32 blockTarget_ = TargetsHelper.bitsToTarget(blockHeader_.bits);
143+
144+
require(target_ == blockTarget_, InvalidTarget(blockTarget_, target_));
145+
require(blockHash_ <= blockTarget_, InvalidBlockHash(blockHash_, blockTarget_));
146+
147+
uint32 medianTime_ = $.blocksData.getMedianTime(blockHeader_.prevBlockHash);
148+
149+
require(blockHeader_.time > medianTime_, InvalidBlockTime(blockHeader_.time, medianTime_));
150+
}
151+
}

contracts/libs/BlockHeader.sol

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {LibBit} from "solady/src/utils/LibBit.sol";
5+
6+
struct BlockHeaderData {
7+
bytes32 prevBlockHash;
8+
bytes32 merkleRoot;
9+
uint32 version;
10+
uint32 time;
11+
uint32 nonce;
12+
bytes4 bits;
13+
}
14+
15+
library BlockHeader {
16+
using LibBit for uint256;
17+
18+
uint256 public constant BLOCK_HEADER_DATA_LENGTH = 80;
19+
20+
error InvalidBlockHeaderDataLength();
21+
22+
function getBlockHeaderHash(bytes calldata blockHeaderRaw_) internal pure returns (bytes32) {
23+
bytes32 rawBlockHash_ = sha256(abi.encode(sha256(blockHeaderRaw_)));
24+
25+
return reverseHash(rawBlockHash_);
26+
}
27+
28+
function parseBlockHeaderData(
29+
bytes calldata blockHeaderRaw_
30+
) internal pure returns (BlockHeaderData memory headerData_, bytes32 blockHash_) {
31+
require(
32+
blockHeaderRaw_.length == BLOCK_HEADER_DATA_LENGTH,
33+
InvalidBlockHeaderDataLength()
34+
);
35+
36+
headerData_ = BlockHeaderData({
37+
version: uint32(_reverseBytes(blockHeaderRaw_[0:4])),
38+
prevBlockHash: reverseHash(bytes32(blockHeaderRaw_[4:36])),
39+
merkleRoot: reverseHash(bytes32(blockHeaderRaw_[36:68])),
40+
time: uint32(_reverseBytes(blockHeaderRaw_[68:72])),
41+
bits: bytes4(uint32(_reverseBytes(blockHeaderRaw_[72:76]))),
42+
nonce: uint32(_reverseBytes(blockHeaderRaw_[76:80]))
43+
});
44+
blockHash_ = getBlockHeaderHash(blockHeaderRaw_);
45+
}
46+
47+
function toRawBytes(BlockHeaderData memory headerData_) internal pure returns (bytes memory) {
48+
return
49+
abi.encodePacked(
50+
_reverseUint32(headerData_.version),
51+
reverseHash(headerData_.prevBlockHash),
52+
reverseHash(headerData_.merkleRoot),
53+
_reverseUint32(headerData_.time),
54+
_reverseUint32(uint32(headerData_.bits)),
55+
_reverseUint32(headerData_.nonce)
56+
);
57+
}
58+
59+
function reverseHash(bytes32 blockHash_) internal pure returns (bytes32) {
60+
return bytes32(uint256(blockHash_).reverseBytes());
61+
}
62+
63+
function _reverseBytes(bytes calldata bytesToConvert_) private pure returns (uint256) {
64+
return uint256(bytes32(bytesToConvert_)).reverseBytes();
65+
}
66+
67+
function _reverseUint32(uint32 input_) private pure returns (uint32) {
68+
return
69+
((input_ & 0x000000FF) << 24) |
70+
((input_ & 0x0000FF00) << 8) |
71+
((input_ & 0x00FF0000) >> 8) |
72+
((input_ & 0xFF000000) >> 24);
73+
}
74+
}

0 commit comments

Comments
 (0)