Skip to content

Commit

Permalink
Merge pull request #1 from catalogfi/feat/spvlib
Browse files Browse the repository at this point in the history
Add libraries and contracts for Bitcoin SPV
  • Loading branch information
susruth authored Aug 8, 2024
2 parents 9667797 + 8387231 commit e6c178c
Show file tree
Hide file tree
Showing 25 changed files with 17,504 additions and 1 deletion.
19 changes: 19 additions & 0 deletions .github/workflows/slither.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: slither
on: [push, pull_request, workflow_dispatch]

jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Code Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Run Analysis
uses: crytic/[email protected]
with:
fail-on: high
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: test

on: [push, pull_request, workflow_dispatch]

env:
FOUNDRY_PROFILE: ci

jobs:
check:
strategy:
fail-fast: true

name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build

- name: Run Forge tests
run: |
forge test -vvv
id: test

- name: Run Forge coverage
run: |
forge coverage -vvv
id: coverage
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
.gas-snapshot

# Coverage
lcov.info
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"solidity.remappings": [
"forge-std/=lib/forge-std/src/",
"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"
],
"solidity.compileUsingRemoteVersion": "v0.8.20+commit.a1b79de6"
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# btc-utils
# btc-utils
52 changes: 52 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Technical Specification Document

# Objective

Brief description of what the feature aims to achieve.

# Functional Requirement

1. Requirement
2. Requirement
3. Requirement
1. Sub Requirement
2. Sub Requirement
4. Requirement

# Non-Functional Requirement

1. Performance Criteria
2. Security Measures
3. Scalability Approach
4. Maintenance Requirement

# Dependencies

- [ ] Feature Dependency
- [ ] Knowledge Transfer
- [ ] Specific Tasks

# Visual Representation

## System Architecture

## Flow Diagram

# Exposed API/Interface

# Test Cases

Outline primary test scenarios to validate the functionality.

Generally all repos. should have 80%+ unit test cases. Also, there should be details how we can test something that is not present in the test cases.

# Discussions/Issues

Planning to setup all GitHub repos. to support it there.

```markdown
# Action Items for Team Members

- [ ] All team members are required to create spec sheets for all active projects they're involved in.
- [ ] Each spec sheet must be reviewed by Reetik before implementation begins and any changes way forward/
```
10 changes: 10 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
'forge-std/=lib/forge-std/src/',
]
fs_permissions = [{ access = "read", path = "./test/fixtures"}]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 978ac6
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at dbb610
3 changes: 3 additions & 0 deletions slither.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"filter_paths": "lib"
}
11 changes: 11 additions & 0 deletions src/Types.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;

struct BlockHeader {
bytes32 merkleRootHash;
bytes4 nBits;
bytes4 nonce;
bytes32 previousBlockHash;
bytes4 timestamp;
bytes4 version;
}
53 changes: 53 additions & 0 deletions src/Utils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {BlockHeader} from "./Types.sol";

library Utils {
function convertBytesToUint(bytes memory b) internal pure returns (uint256) {
uint256 number;
uint256 length = b.length;
require(length <= 32, "SPVLib: length cannot be greater than 32 bytes");
for (uint256 i = 0; i < length; i++) {
number = number + uint256(uint8(b[i])) * (2 ** (8 * (length - (i + 1))));
}
return number;
}

function convertToBigEndian(bytes memory bytesLE) internal pure returns (bytes memory) {
uint256 length = bytesLE.length;
bytes memory bytesBE = new bytes(length);
for (uint256 i = 0; i < length; i++) {
bytesBE[length - i - 1] = bytesLE[i];
}
return bytesBE;
}

function convertnBitsToTarget(bytes memory nBitsBytes) internal pure returns (uint256) {
uint256 nBits = convertBytesToUint(convertToBigEndian(nBitsBytes));
uint256 exp = uint256(nBits) >> 24;
uint256 c = nBits & 0xffffff;
uint256 target = uint256((c * 2 ** (8 * (exp - 3))));
return target;
}

function doubleHash(bytes memory data) internal pure returns (bytes32) {
return sha256(abi.encodePacked(sha256(abi.encodePacked(data))));
}

function convertToBytes32(bytes memory data) internal pure returns (bytes32 result) {
assembly {
// Copy 32 bytes from data into result
result := mload(add(data, 32))
}
}

function parseBlockHeader(bytes calldata blockHeader) internal pure returns (BlockHeader memory parsedHeader) {
parsedHeader.version = bytes4(blockHeader[:4]);
parsedHeader.previousBlockHash = bytes32(blockHeader[4:36]);
parsedHeader.merkleRootHash = bytes32(blockHeader[36:68]);
parsedHeader.timestamp = bytes4(blockHeader[68:72]);
parsedHeader.nBits = bytes4(blockHeader[72:76]);
parsedHeader.nonce = bytes4(blockHeader[76:]);
}
}
109 changes: 109 additions & 0 deletions src/VerifySPV.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;

import {IVerifySPV} from "./interfaces/IVerifySPV.sol";
import {BlockHeader, SPVLib} from "./libraries/SPVLib.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Utils} from "./Utils.sol";

contract VerifySPV is IVerifySPV {
using SPVLib for BlockHeader;
using Utils for bytes;

mapping(bytes32 => BlockHeader) public blockHeaders;
// every difficulty epoch's block hash
// updates for every 2016th block = every 28 epochs
bytes32 public LDEBlockHash;
// epoch is incremented for every block register ,1 epoch = 72 blocks
uint256 public epoch;

event BlockRegistered(bytes32 blockHash);

constructor(BlockHeader memory genesisHeader) {
LDEBlockHash = genesisHeader.calculateBlockHash();
blockHeaders[genesisHeader.calculateBlockHash()] = genesisHeader;
epoch = 0;
}

function registerBlock(BlockHeader[] calldata newEpoch) public {
require(newEpoch.length == 76, "VerifySPV: invalid epoch, should contain previous 72 blocks and next 3 blocks");

require(
blockHeaders[newEpoch[0].calculateBlockHash()].previousBlockHash != bytes32(0x0),
"VerifySPV: invalid epoch, starting block not on chain"
);

epoch++;

verifySequence(newEpoch);

bytes32 newEpochBlockHash = newEpoch[72].calculateBlockHash();
blockHeaders[newEpochBlockHash] = newEpoch[72];

emit BlockRegistered(newEpochBlockHash);
}

function verifyTxInclusion(
BlockHeader[] calldata blockSequence,
uint256 blockIndex,
uint256 txIndex,
bytes32 txHash,
bytes32[] memory proof
) public view returns (bool) {
require(blockSequence.length == 73, "VerifySPV: inclusion verification needs all 72 blocks in the epoch");

require(
blockHeaders[blockSequence[0].calculateBlockHash()].previousBlockHash != bytes32(0x0),
"VerifySPV: invalid epoch, starting block not on chain"
);

require(
blockHeaders[blockSequence[72].calculateBlockHash()].previousBlockHash != bytes32(0x0),
"VerifySPV: invalid epoch, ending block not on chain"
);
uint256 target = (abi.encodePacked((blockSequence[0].nBits))).convertnBitsToTarget();
verifySubSequence(blockSequence[0:72], target);
return blockSequence[blockIndex].verifyProof(txHash, txIndex, proof);
}

function verifySequence(BlockHeader[] calldata blockSequence) internal {
uint256 target = (abi.encodePacked((blockHeaders[LDEBlockHash].nBits))).convertnBitsToTarget();
if (epoch % 28 == 0) {
require(
verifySubSequence(blockSequence[:72], target), "VerifySPV: pre subsequence in difficulty epoch failed"
);
uint256 adjustedTarget = blockSequence[72].calculateNewTarget(target, blockHeaders[LDEBlockHash].timestamp);
uint256 newTarget = (abi.encodePacked((blockSequence[72].nBits))).convertnBitsToTarget();
require(
SPVLib.verifyDifficultyEpochTarget(adjustedTarget, newTarget),
"VerifySPV: adjusted difficulty is not in allowed range"
);
require(
blockSequence[71].calculateBlockHash() == blockSequence[72].previousBlockHash
&& blockSequence[72].verifyWork(),
"VerifySPV: difficulty epoch validation failed"
);
require(
verifySubSequence(blockSequence[73:], newTarget),
"VerifySPV: post subsequence in difficulty epoch failed"
);
LDEBlockHash = blockSequence[72].calculateBlockHash();
} else {
require(verifySubSequence(blockSequence, target), "VerifySPV: sequence verification failed");
}
}

function verifySubSequence(BlockHeader[] calldata blockSequence, uint256 target) internal pure returns (bool) {
for (uint256 i = 1; i < blockSequence.length; i++) {
if (
!(
blockSequence[i - 1].calculateBlockHash() == blockSequence[i].previousBlockHash
&& blockSequence[i].verifyTarget(target) && blockSequence[i].verifyWork()
)
) {
return false;
}
}
return true;
}
}
16 changes: 16 additions & 0 deletions src/interfaces/IVerifySPV.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.20;

import {BlockHeader} from "../libraries/SPVLib.sol";

interface IVerifySPV {
function registerBlock(BlockHeader[] calldata newEpoch) external;

function verifyTxInclusion(
BlockHeader[] calldata blockSequence,
uint256 blockIndex,
uint256 txIndex,
bytes32 txHash,
bytes32[] memory proof
) external view returns (bool);
}
Loading

0 comments on commit e6c178c

Please sign in to comment.