Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 59 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ endif
# -----------------------------------------------------------------------------
# PHONY targets (force these to run even if a file with the same name exists)
# -----------------------------------------------------------------------------
.PHONY: all install clean build test format docs snapshot diff deploy deploy-anvil deploy-hpp-sepolia
.PHONY: all install clean build test format docs snapshot diff deploy deploy-anvil deploy-hpp-sepolia deploy-vrf deploy-vrf-hpp-sepolia register-epoch

# -----------------------------------------------------------------------------
# Default target: run dependency install -> clean -> format -> build -> test
Expand Down Expand Up @@ -124,6 +124,64 @@ deploy-hpp-sepolia:
--verifier-url https://sepolia-explorer.hpp.io/api/ \
--sig "run(address,address)" $(PRODUCTION_OWNER_ADDR) $(INITIAL_FEE_RECIPIENT_ADDR)

# -----------------------------------------------------------------------------
# Deploy NoosphereVRF singleton
# - Requires: RPC_URL, PRIVATE_KEY
# - Optional: VRF_OWNER (defaults to deployer address)
# -----------------------------------------------------------------------------
deploy-vrf:
@if [ -z "$(RPC_URL)" ] || [ -z "$(PRIVATE_KEY)" ]; then \
echo "ERROR: RPC_URL and PRIVATE_KEY environment variables are required."; \
exit 1; \
fi
@echo "=> Deploying NoosphereVRF singleton to $(RPC_URL)..."
@forge script scripts/DeployVRF.sol:DeployVRF \
--broadcast \
--skip-simulation \
--gas-estimate-multiplier 130 \
--optimize \
--optimizer-runs 1000000 \
--extra-output-files abi \
--rpc-url $(RPC_URL) \
--chain-id $(CHAIN_ID) \
--private-key $(PRIVATE_KEY)

deploy-vrf-hpp-sepolia:
@if [ -z "$(RPC_URL)" ] || [ -z "$(PRIVATE_KEY)" ]; then \
echo "ERROR: RPC_URL and PRIVATE_KEY environment variables are required."; \
exit 1; \
fi
@echo "=> Deploying NoosphereVRF to HPP Sepolia with verification..."
@forge script scripts/DeployVRF.sol:DeployVRF \
--broadcast \
--skip-simulation \
--gas-estimate-multiplier 130 \
--optimize \
--optimizer-runs 1000000 \
--extra-output-files abi \
--rpc-url $(RPC_URL) \
--chain-id $(CHAIN_ID) \
--private-key $(PRIVATE_KEY) \
--verify \
--verifier blockscout \
--verifier-url https://sepolia-explorer.hpp.io/api/

# -----------------------------------------------------------------------------
# Register epoch on NoosphereVRF
# - Requires: RPC_URL, PRIVATE_KEY, VRF_ADDRESS, EPOCH, MERKLE_ROOT
# -----------------------------------------------------------------------------
register-epoch:
@if [ -z "$(RPC_URL)" ] || [ -z "$(PRIVATE_KEY)" ] || [ -z "$(VRF_ADDRESS)" ] || [ -z "$(EPOCH)" ] || [ -z "$(MERKLE_ROOT)" ]; then \
echo "ERROR: RPC_URL, PRIVATE_KEY, VRF_ADDRESS, EPOCH, MERKLE_ROOT are required."; \
exit 1; \
fi
@echo "=> Registering epoch $(EPOCH) on NoosphereVRF $(VRF_ADDRESS)..."
@NOOSPHERE_VRF_ADDRESS=$(VRF_ADDRESS) forge script scripts/RegisterEpoch.sol:RegisterEpoch \
--broadcast \
--rpc-url $(RPC_URL) \
--chain-id $(CHAIN_ID) \
--private-key $(PRIVATE_KEY)

# -----------------------------------------------------------------------------
# Save gas snapshot (using forge snapshot)
# -----------------------------------------------------------------------------
Expand Down
46 changes: 46 additions & 0 deletions scripts/DeployVRF.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity 0.8.24;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {NoosphereVRF} from "../src/v1_0_0/vrf/NoosphereVRF.sol";

/// @title DeployVRF
/// @notice Deploys the NoosphereVRF singleton contract.
/// The singleton is shared across all consumer dApps on a given chain.
///
/// Required environment variables:
/// PRIVATE_KEY – Deployer private key (pays gas, becomes initial owner unless overridden)
/// VRF_OWNER – (Optional) Owner address for the VRF contract. Defaults to deployer.
///
/// Usage:
/// forge script scripts/DeployVRF.sol:DeployVRF --broadcast --rpc-url $RPC_URL
contract DeployVRF is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
require(deployerPrivateKey != 0, "DeployVRF: PRIVATE_KEY env var required");

address deployerAddress = vm.addr(deployerPrivateKey);

// Owner defaults to deployer if VRF_OWNER is not set
address vrfOwner = vm.envOr("VRF_OWNER", deployerAddress);

console.log("=== DeployVRF: environment ===");
console.log("Deployer address:", deployerAddress);
console.log("VRF Owner: ", vrfOwner);
console.log("Chain ID: ", block.chainid);
console.log("==============================");

vm.startBroadcast(deployerPrivateKey);

NoosphereVRF vrf = new NoosphereVRF(vrfOwner);

vm.stopBroadcast();

console.log("=== DeployVRF: summary ===");
console.log("NoosphereVRF:", address(vrf));
console.log("Owner: ", vrfOwner);
console.log("Epoch size: ", vrf.EPOCH_SIZE());
console.log("==========================");
}
}
20 changes: 20 additions & 0 deletions scripts/RegisterEpoch.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Script.sol";
import {NoosphereVRF} from "@noosphere-evm/v1_0_0/vrf/NoosphereVRF.sol";

contract RegisterEpoch is Script {
function run() external {
address vrfAddr = vm.envAddress("NOOSPHERE_VRF_ADDRESS");
uint256 epoch = vm.envUint("EPOCH");
bytes32 merkleRoot = vm.envBytes32("MERKLE_ROOT");

vm.startBroadcast();
NoosphereVRF(vrfAddr).registerEpoch(epoch, merkleRoot);
vm.stopBroadcast();

console.log("Registered epoch", epoch);
console.logBytes32(merkleRoot);
}
}
99 changes: 99 additions & 0 deletions src/v1_0_0/vrf/INoosphereVRF.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity 0.8.24;

/// @title INoosphereVRF
/// @notice Public interface for Noosphere VRF singleton
/// @dev Manages epoch-based Merkle tree random values shared across all consumer dApps.
/// Provides global request ID assignment, Merkle proof verification, and replay prevention.
interface INoosphereVRF {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/

event EpochRegistered(uint256 indexed epoch, bytes32 merkleRoot, uint256 size);
event EpochRunningLow(uint256 indexed epoch, uint256 remaining);
event RandomValueVerified(uint256 indexed requestId, bytes32 randomValue, bytes32 blockHash);
event RandomValueExpired(uint256 indexed requestId);
event ConsumerAdded(address indexed consumer);
event ConsumerRemoved(address indexed consumer);

/*//////////////////////////////////////////////////////////////
EPOCH MANAGEMENT
//////////////////////////////////////////////////////////////*/

/// @notice Register a Merkle root for an epoch (owner only, one-time per epoch)
function registerEpoch(uint256 epoch, bytes32 merkleRoot) external;

/// @notice Get the Merkle root for an epoch
function getEpochRoot(uint256 epoch) external view returns (bytes32);

/// @notice Get remaining slots in the current epoch
function getEpochRemaining() external view returns (uint256);

/// @notice Get the current epoch number
function getCurrentEpoch() external view returns (uint256);

/*//////////////////////////////////////////////////////////////
REQUEST LIFECYCLE
//////////////////////////////////////////////////////////////*/

/// @notice Atomically reserve a global request ID (called by VRFConsumer before _requestCompute)
/// @dev Increments the global counter and records block number for 2-party entropy.
/// Must be called before building container input to avoid race conditions.
/// @return requestId The globally unique request ID
function reserveRequestId() external returns (uint256 requestId);

/// @notice Bind a reserved request ID to a (subscriptionId, interval) pair for fulfillment routing
/// @param subscriptionId The Noosphere subscription ID
/// @param interval The interval assigned by _requestCompute()
/// @param requestId The previously reserved request ID
function bindRequest(uint64 subscriptionId, uint32 interval, uint256 requestId) external;

/// @notice Verify Merkle proof and return random value (called by VRFConsumer callback)
/// @param subscriptionId The Noosphere subscription ID
/// @param interval The interval from the callback
/// @param outputUri The raw output URI from the VRNG container
/// @return requestId The resolved request ID
/// @return randomValue The verified random value from the Merkle tree
/// @return blockHash The L2 block hash for 2-party entropy
/// @return expired Whether the request expired (blockHash unavailable)
function fulfillRandomValue(uint64 subscriptionId, uint32 interval, bytes calldata outputUri)
external
returns (uint256 requestId, bytes32 randomValue, bytes32 blockHash, bool expired);

/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/

/// @notice Get the next request ID that will be assigned
function nextRequestId() external view returns (uint256);

/// @notice Check if a request has expired (block hash no longer available)
function isRequestExpired(uint256 requestId) external view returns (bool);

/// @notice Get the block number when a request was made
function getRequestBlock(uint256 requestId) external view returns (uint256);

/*//////////////////////////////////////////////////////////////
CONFIG
//////////////////////////////////////////////////////////////*/

/// @notice Number of random values per epoch
function EPOCH_SIZE() external view returns (uint256);

/// @notice Number of blocks before block hash becomes unavailable
function BLOCKHASH_TIMEOUT() external view returns (uint256);

/*//////////////////////////////////////////////////////////////
CONSUMER MANAGEMENT
//////////////////////////////////////////////////////////////*/

/// @notice Add an authorized consumer contract (owner only)
function addConsumer(address consumer) external;

/// @notice Remove an authorized consumer contract (owner only)
function removeConsumer(address consumer) external;

/// @notice Check if an address is an authorized consumer
function isAuthorizedConsumer(address consumer) external view returns (bool);
}
Loading