Skip to content

Commit

Permalink
Implement the next PoRA mine specification (#48)
Browse files Browse the repository at this point in the history
* init next_mine_spec branch

* feat: Implement EVM-friendly scratch pad (#46)

* feat: Introduce sub-task for PoRA mine. (#47)

* feat: Introduce sub-task for PoRA mine.

* Implement helper function for PoRA miner
  • Loading branch information
bruno-valante authored Dec 28, 2024
1 parent 30f0f92 commit 24031c0
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 73 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
branches:
- main
- next_mine_spec

jobs:
build:
Expand Down
103 changes: 74 additions & 29 deletions contracts/miner/Mine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import "../interfaces/IDigestHistory.sol";

import "./RecallRange.sol";
import "./MineLib.sol";
import "./WorkerContext.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
Expand All @@ -37,7 +38,7 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
uint private constant NO_DATA_PROOF = 0x2;
uint private constant FIXED_DIFFICULTY = 0x4;

uint64 private constant PORA_VERSION = 0;
uint64 private constant PORA_VERSION = 1;

// Deferred initializd fields
address public flow;
Expand All @@ -61,6 +62,7 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {

// Updated configurable parameters
uint public minDifficulty;
uint public nSubtasks;

event NewMinerId(bytes32 indexed minerId, address indexed beneficiary);
event UpdateMinerId(bytes32 indexed minerId, address indexed from, address indexed to);
Expand All @@ -87,6 +89,7 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
targetSubmissionsNextEpoch = 10;
difficultyAdjustRatio = 20;
maxShards = 32;
nSubtasks = 1;
}

function poraVersion() external pure returns (uint64) {
Expand All @@ -101,15 +104,8 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {

// Step 2: maintain context
MineContext memory context = IFlow(flow).makeContextWithResult();
require(context.epoch >= lastMinedEpoch, "Internal error: epoch number decrease");
if (context.epoch > lastMinedEpoch && lastMinedEpoch > 0) {
if (currentSubmissions < targetSubmissions) {
// Not enough submissions in the last epoch
_adjustDifficultyOnIncompleteEpoch();
}
currentSubmissions = 0;
targetSubmissions = targetSubmissionsNextEpoch;
}
_updateMineEpochWhenNeeded(context);
bytes32 subtaskDigest = getSubtaskDigest(context, answer.minerId);

// Step 3: basic check for submission
basicCheck(answer, context);
Expand All @@ -129,7 +125,7 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
delete unsealedData;

// Step 5: compute PoRA hash
bytes32 poraOutput = pora(answer);
bytes32 poraOutput = pora(answer, subtaskDigest);
uint scaleX64 = answer.range.targetScaleX64(context.flowLength);
// scaleX64 >= 2^64, so there is no overflow
require(uint(poraOutput) <= (poraTarget / scaleX64) << 64, "Do not reach target quality");
Expand All @@ -147,21 +143,13 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
emit NewSubmission(context.epoch, answer.minerId, currentSubmissions, answer.recallPosition);
lastMinedEpoch = context.epoch;
currentSubmissions += 1;
if (currentSubmissions < targetSubmissions) {
return;
}

// Step 8: adjust quality
if (!fixedDifficulty) {
_adjustDifficulty(context);
}
}

function basicCheck(MineLib.PoraAnswer memory answer, MineContext memory context) public view {
// Check basic field
require(context.digest == answer.contextDigest, "Inconsistent mining digest");
require(context.digest != EMPTY_HASH, "Empty digest can not mine");
require(currentSubmissions < targetSubmissions, "Epoch has enough submissions");
require(currentSubmissions < 2 * targetSubmissions, "Epoch has enough submissions");

// Check validity of recall range
uint maxLength = (context.flowLength / SECTORS_PER_LOAD) * SECTORS_PER_LOAD;
Expand All @@ -176,10 +164,10 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
);
}

function pora(MineLib.PoraAnswer memory answer) public view returns (bytes32) {
function pora(MineLib.PoraAnswer memory answer, bytes32 subtaskDigest) public view returns (bytes32) {
require(answer.minerId != bytes32(0x0), "Miner ID cannot be empty");

bytes32[4] memory seedInput = [answer.minerId, answer.nonce, answer.contextDigest, answer.range.digest()];
bytes32[4] memory seedInput = [answer.minerId, answer.nonce, subtaskDigest, answer.range.digest()];

bytes32[2] memory padSeed = Blake2b.blake2b(seedInput);

Expand All @@ -197,6 +185,25 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
return MineLib.computePoraHash(answer.sealOffset, padSeed, mixedData);
}

function _updateMineEpochWhenNeeded(MineContext memory context) internal {
require(context.epoch >= lastMinedEpoch, "Internal error: epoch number decrease");

if (context.epoch > lastMinedEpoch && lastMinedEpoch > 0) {
_adjustDifficultyOnNewEpoch();
currentSubmissions = 0;
targetSubmissions = targetSubmissionsNextEpoch;
}
}

function getSubtaskDigest(MineContext memory context, bytes32 minerId) public view returns (bytes32) {
uint subtaskIdx = uint(keccak256(abi.encode(context.digest, minerId))) % nSubtasks;
uint subtaskMineStart = context.mineStart + subtaskIdx;
require(block.number > subtaskMineStart, "Earlier than expected subtask start block.");
require(block.number - subtaskMineStart <= targetMineBlocks, "Mine deadline exceed");

return keccak256(abi.encode(context.digest, blockhash(subtaskMineStart)));
}

function requestMinerId(address beneficiary, uint64 seed) public {
bytes32 minerId = keccak256(abi.encodePacked(blockhash(block.number - 1), msg.sender, seed));
require(beneficiaries[minerId] == address(0), "MinerId has registered");
Expand All @@ -210,21 +217,27 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
emit UpdateMinerId(minerId, msg.sender, to);
}

function _adjustDifficulty(MineContext memory context) internal {
uint miningBlocks = block.number - context.mineStart;

function _adjustDifficultyOnNewEpoch() internal {
// Remove least significant 16 bits to avoid overflow
uint scaledTarget = poraTarget >> 16;
uint scaledExpected = Math.mulDiv(scaledTarget, miningBlocks, targetMineBlocks);
uint scaledExpected;
if (currentSubmissions > 0) {
uint scaledTarget = poraTarget >> 16;
scaledExpected = Math.mulDiv(scaledTarget, targetMineBlocks, currentSubmissions);
} else {
scaledExpected = type(uint).max >> 16;

Check failure on line 227 in contracts/miner/Mine.sol

View workflow job for this annotation

GitHub Actions / build

Delete ····
}

_adjustDifficultyInner(scaledExpected);
}

function _adjustDifficultyOnIncompleteEpoch() internal {
function _adjustDifficultyOnSkippedEpoch() internal {
_adjustDifficultyInner(type(uint).max >> 16);
}

function _adjustDifficultyInner(uint scaledExpected) internal {
if(fixedDifficulty) {

Check failure on line 238 in contracts/miner/Mine.sol

View workflow job for this annotation

GitHub Actions / build

Insert ·
return;
}
uint scaledTarget = poraTarget >> 16;

uint n = difficultyAdjustRatio;
Expand Down Expand Up @@ -260,6 +273,7 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
}

function setTargetMineBlocks(uint targetMineBlocks_) external onlyRole(PARAMS_ADMIN_ROLE) {
require(targetMineBlocks_ <= 256, "target mine block must <= 256");
targetMineBlocks = targetMineBlocks_;
}

Expand Down Expand Up @@ -289,8 +303,39 @@ contract PoraMine is ZgInitializable, AccessControlEnumerable {
}
}

function setNumSubtasks(uint nSubtasks_) external onlyRole(PARAMS_ADMIN_ROLE) {
require(nSubtasks_ > 0, "Number of subtasks cannot be zero");
require(nSubtasks_ < IFlow(flow).blocksPerEpoch(), "Number of subtasks must be less than blocks per epoch");
nSubtasks = nSubtasks_;
}

function canSubmit() external returns (bool) {
MineContext memory context = IFlow(flow).makeContextWithResult();
return context.epoch > lastMinedEpoch || currentSubmissions < targetSubmissions;
return context.epoch > lastMinedEpoch || currentSubmissions < targetSubmissions * 2;
}

function computeWorkerContext(bytes32 minerId) external returns (WorkerContext memory answer) {
require(minerId != bytes32(0), "MinerId cannot be zero");
address beneficiary = beneficiaries[minerId];
require(beneficiary != address(0), "MinerId does not registered");

answer.maxShards = maxShards;
answer.context = IFlow(flow).makeContextWithResult();

uint subtaskIdx = uint(keccak256(abi.encode(answer.context.digest, minerId))) % nSubtasks;
uint subtaskMineStart = answer.context.mineStart + subtaskIdx;
if (block.number <= subtaskMineStart || block.number - subtaskMineStart > targetMineBlocks) {
return answer;
}

Check failure on line 330 in contracts/miner/Mine.sol

View workflow job for this annotation

GitHub Actions / build

Delete ········
answer.subtaskDigest = keccak256(abi.encode(answer.context.digest, blockhash(subtaskMineStart)));

if (answer.context.epoch > lastMinedEpoch) {
_updateMineEpochWhenNeeded(answer.context);
}

Check failure on line 336 in contracts/miner/Mine.sol

View workflow job for this annotation

GitHub Actions / build

Delete ········
if (currentSubmissions < targetSubmissions * 2) {
answer.poraTarget = poraTarget;
}
}
}
30 changes: 12 additions & 18 deletions contracts/miner/MineLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,24 @@ library MineLib {
bytes32[] merkleProof;
}

function scratchPadHash(bytes32[2] memory padDigest, uint rounds) internal view {
bytes32[8] memory slots;
slots[1] = Blake2b.BLAKE2B_INIT_STATE0;
slots[2] = Blake2b.BLAKE2B_INIT_STATE1;
slots[3] = padDigest[0];
slots[4] = padDigest[1];
function scratchPadHash(bytes32[2] memory padDigest, uint rounds) internal pure {
assembly {
let argPtr := add(slots, 0x1c)
let mPtr := add(slots, 0x60)
mstore8(add(slots, 0x1f), 12) // round = 12
mstore8(add(slots, 0xe0), 64) // offset = 64
mstore8(add(slots, 0xf0), 1) // finalized = true

for {
let i := 0
} lt(i, rounds) {
i := add(i, 1)
} {
if iszero(staticcall(not(0), 0x09, argPtr, 0xd5, mPtr, 0x40)) {
revert(0, 0)
}
mstore(padDigest, keccak256(padDigest, 0x40))
mstore(add(padDigest, 0x20), keccak256(padDigest, 0x40))
}
}
padDigest[0] = slots[3];
padDigest[1] = slots[4];
}

function scratchPadHashOnce(bytes32[2] memory padDigest) internal pure {
assembly {
mstore(padDigest, keccak256(padDigest, 0x40))
mstore(add(padDigest, 0x20), keccak256(padDigest, 0x40))
}
}

function computeScratchPadAndMix(
Expand All @@ -58,7 +51,8 @@ library MineLib {
scratchPadHash(currentDigest, skipSeals * BHASHES_PER_SEAL);
unchecked {
for (uint i = 0; i < UNITS_PER_SEAL; i += 2) {
scratchPadHash(currentDigest, 1);
scratchPadHashOnce(currentDigest);

mixedData[i] = currentDigest[0] ^ sealedData[i];
mixedData[i + 1] = currentDigest[1] ^ sealedData[i + 1];
}
Expand Down
11 changes: 11 additions & 0 deletions contracts/miner/WorkerContext.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.0 <0.9.0;

import "../interfaces/IFlow.sol";

struct WorkerContext {
MineContext context;
uint poraTarget;
bytes32 subtaskDigest;
uint64 maxShards;
}

Check failure on line 11 in contracts/miner/WorkerContext.sol

View workflow job for this annotation

GitHub Actions / build

Insert ⏎
4 changes: 2 additions & 2 deletions contracts/test/PoraMineTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ contract PoraMineTest is PoraMine {
return MineLib.recoverMerkleRoot(answer, unsealedData);
}

function testAll(MineLib.PoraAnswer memory answer) external view {
function testAll(MineLib.PoraAnswer memory answer, bytes32 subtaskDigest) external view {
bytes32[UNITS_PER_SEAL] memory unsealedData = MineLib.unseal(answer);

MineLib.recoverMerkleRoot(answer, unsealedData);
delete unsealedData;

pora(answer);
pora(answer, subtaskDigest);
}
}
2 changes: 1 addition & 1 deletion contracts/utils/ZgsSpec.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ uint constant MAX_MINING_LENGTH = (8 * TB) / BYTES_PER_SECTOR;

uint constant BYTES_PER_SECTOR = 256;
uint constant BYTES_PER_SEAL = 4 * KB;
uint constant BYTES_PER_PAD = 64 * KB;
uint constant BYTES_PER_PAD = 16 * KB;
uint constant BYTES_PER_LOAD = 256 * KB;
uint constant BYTES_PER_PRICE = 8 * GB;
uint constant BYTES_PER_SEGMENT = 256 * KB;
Expand Down
Loading

0 comments on commit 24031c0

Please sign in to comment.