Entropy Solidity SDK & usage example (#1124)

* grr

* revert

* ok

* ok

* ok

* deploy script

* implement interface

* doc comments

* fix comment
This commit is contained in:
Jayant Krishnamurthy 2023-10-31 06:32:21 -07:00 committed by GitHub
parent d4fad5049c
commit ddbbe2af14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 384 additions and 15 deletions

View File

@ -2,3 +2,9 @@ chains:
optimism-goerli:
geth_rpc_addr: https://goerli.optimism.io
contract_addr: 0x28F16Af4D87523910b843a801454AEde5F9B0459
avalanche-fuji:
geth_rpc_addr: https://api.avax-test.network/ext/bc/C/rpc
contract_addr: 0xD42c7a708E74AD19401D907a14146F006c851Ee3
eos-evm-testnet:
geth_rpc_addr: https://api.testnet.evm.eosnetwork.com/
contract_addr: 0xD42c7a708E74AD19401D907a14146F006c851Ee3

View File

@ -16,7 +16,7 @@ pub mod state;
// Server TODO list:
// - Tests
// - Reduce memory requirements for storing hash chains to increase scalability
// - Name things nicely (service name, API resource names)
// - Name things nicely (API resource names)
// - README
// - Choose data formats for binary data
#[tokio::main]

92
package-lock.json generated
View File

@ -17,6 +17,7 @@
"target_chains/cosmwasm/tools",
"target_chains/cosmwasm/deploy-scripts",
"target_chains/ethereum/contracts",
"target_chains/ethereum/entropy_sdk/solidity",
"target_chains/ethereum/sdk/js",
"target_chains/ethereum/sdk/solidity",
"target_chains/ethereum/examples/oracle_swap/app",
@ -11755,6 +11756,10 @@
"resolved": "target_chains/cosmwasm/tools",
"link": true
},
"node_modules/@pythnetwork/entropy-sdk-solidity": {
"resolved": "target_chains/ethereum/entropy_sdk/solidity",
"link": true
},
"node_modules/@pythnetwork/eth-oracle-swap-example-frontend": {
"resolved": "target_chains/ethereum/examples/oracle_swap/app",
"link": true
@ -57381,6 +57386,7 @@
"@openzeppelin/contracts": "^4.5.0",
"@openzeppelin/contracts-upgradeable": "^4.5.2",
"@openzeppelin/hardhat-upgrades": "^1.22.1",
"@pythnetwork/entropy-sdk-solidity": "*",
"@pythnetwork/pyth-multisig-wh-message-builder": "*",
"@pythnetwork/pyth-sdk-solidity": "^2.2.0",
"contract_manager": "*",
@ -57988,6 +57994,54 @@
}
}
},
"target_chains/ethereum/entropy_sdk/solidity": {
"version": "0.1.0",
"license": "Apache-2.0",
"devDependencies": {
"prettier": "^2.7.1",
"prettier-plugin-solidity": "^1.0.0-rc.1",
"solc": "^0.8.15"
}
},
"target_chains/ethereum/entropy_sdk/solidity/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true,
"engines": {
"node": ">= 12"
}
},
"target_chains/ethereum/entropy_sdk/solidity/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"target_chains/ethereum/entropy_sdk/solidity/node_modules/solc": {
"version": "0.8.21",
"resolved": "https://registry.npmjs.org/solc/-/solc-0.8.21.tgz",
"integrity": "sha512-N55ogy2dkTRwiONbj4e6wMZqUNaLZkiRcjGyeafjLYzo/tf/IvhHY5P5wpe+H3Fubh9idu071i8eOGO31s1ylg==",
"dev": true,
"dependencies": {
"command-exists": "^1.2.8",
"commander": "^8.1.0",
"follow-redirects": "^1.12.1",
"js-sha3": "0.8.0",
"memorystream": "^0.3.1",
"semver": "^5.5.0",
"tmp": "0.0.33"
},
"bin": {
"solcjs": "solc.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"target_chains/ethereum/examples/oracle_swap/app": {
"name": "@pythnetwork/eth-oracle-swap-example-frontend",
"version": "0.1.0",
@ -66350,6 +66404,43 @@
}
}
},
"@pythnetwork/entropy-sdk-solidity": {
"version": "file:target_chains/ethereum/entropy_sdk/solidity",
"requires": {
"prettier": "^2.7.1",
"prettier-plugin-solidity": "^1.0.0-rc.1",
"solc": "^0.8.15"
},
"dependencies": {
"commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true
},
"semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true
},
"solc": {
"version": "0.8.21",
"resolved": "https://registry.npmjs.org/solc/-/solc-0.8.21.tgz",
"integrity": "sha512-N55ogy2dkTRwiONbj4e6wMZqUNaLZkiRcjGyeafjLYzo/tf/IvhHY5P5wpe+H3Fubh9idu071i8eOGO31s1ylg==",
"dev": true,
"requires": {
"command-exists": "^1.2.8",
"commander": "^8.1.0",
"follow-redirects": "^1.12.1",
"js-sha3": "0.8.0",
"memorystream": "^0.3.1",
"semver": "^5.5.0",
"tmp": "0.0.33"
}
}
}
},
"@pythnetwork/eth-oracle-swap-example-frontend": {
"version": "file:target_chains/ethereum/examples/oracle_swap/app",
"requires": {
@ -67858,6 +67949,7 @@
"@openzeppelin/hardhat-upgrades": "^1.22.1",
"@openzeppelin/test-helpers": "^0.5.15",
"@openzeppelin/truffle-upgrades": "^1.14.0",
"@pythnetwork/entropy-sdk-solidity": "*",
"@pythnetwork/pyth-multisig-wh-message-builder": "*",
"@pythnetwork/pyth-sdk-solidity": "^2.2.0",
"@truffle/hdwallet-provider": "^2.1.5",

View File

@ -12,6 +12,7 @@
"target_chains/cosmwasm/tools",
"target_chains/cosmwasm/deploy-scripts",
"target_chains/ethereum/contracts",
"target_chains/ethereum/entropy_sdk/solidity",
"target_chains/ethereum/sdk/js",
"target_chains/ethereum/sdk/solidity",
"target_chains/ethereum/examples/oracle_swap/app",

View File

@ -2,9 +2,10 @@
pragma solidity ^0.8.0;
import "./PythRandomState.sol";
import "./PythRandomErrors.sol";
import "./PythRandomEvents.sol";
import "@pythnetwork/entropy-sdk-solidity/PythRandomState.sol";
import "@pythnetwork/entropy-sdk-solidity/PythRandomErrors.sol";
import "@pythnetwork/entropy-sdk-solidity/PythRandomEvents.sol";
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
// PythRandom implements a secure 2-party random number generation procedure. The protocol
// is an extension of a simple commit/reveal protocol. The original version has the following steps:
@ -78,7 +79,7 @@ import "./PythRandomEvents.sol";
// - function to check invariants??
// - need to increment pyth fees if someone transfers funds to the contract via another method
// - off-chain data ERC support?
contract PythRandom is PythRandomState, PythRandomEvents {
contract PythRandom is IEntropy, PythRandomState {
// TODO: Use an upgradeable proxy
constructor(uint pythFeeInWei) {
_state.accruedPythFeesInWei = 0;
@ -95,7 +96,7 @@ contract PythRandom is PythRandomState, PythRandomEvents {
bytes32 commitment,
bytes32 commitmentMetadata,
uint64 chainLength
) public {
) public override {
if (chainLength == 0) revert PythRandomErrors.AssertionFailure();
PythRandomStructs.ProviderInfo storage provider = _state.providers[
@ -124,7 +125,7 @@ contract PythRandom is PythRandomState, PythRandomEvents {
// Withdraw a portion of the accumulated fees for the provider msg.sender.
// Calling this function will transfer `amount` wei to the caller (provided that they have accrued a sufficient
// balance of fees in the contract).
function withdraw(uint256 amount) public {
function withdraw(uint256 amount) public override {
PythRandomStructs.ProviderInfo storage providerInfo = _state.providers[
msg.sender
];
@ -155,7 +156,7 @@ contract PythRandom is PythRandomState, PythRandomEvents {
address provider,
bytes32 userCommitment,
bool useBlockHash
) public payable returns (uint64 assignedSequenceNumber) {
) public payable override returns (uint64 assignedSequenceNumber) {
PythRandomStructs.ProviderInfo storage providerInfo = _state.providers[
provider
];
@ -205,7 +206,7 @@ contract PythRandom is PythRandomState, PythRandomEvents {
uint64 sequenceNumber,
bytes32 userRandomness,
bytes32 providerRevelation
) public returns (bytes32 randomNumber) {
) public override returns (bytes32 randomNumber) {
// TODO: do we need to check that this request exists?
// TODO: this method may need to be authenticated to prevent griefing
bytes32 key = requestKey(provider, sequenceNumber);
@ -257,25 +258,33 @@ contract PythRandom is PythRandomState, PythRandomEvents {
function getProviderInfo(
address provider
) public view returns (PythRandomStructs.ProviderInfo memory info) {
)
public
view
override
returns (PythRandomStructs.ProviderInfo memory info)
{
info = _state.providers[provider];
}
function getRequest(
address provider,
uint64 sequenceNumber
) public view returns (PythRandomStructs.Request memory req) {
) public view override returns (PythRandomStructs.Request memory req) {
bytes32 key = requestKey(provider, sequenceNumber);
req = _state.requests[key];
}
function getFee(address provider) public view returns (uint feeAmount) {
function getFee(
address provider
) public view override returns (uint feeAmount) {
return _state.providers[provider].feeInWei + _state.pythFeeInWei;
}
function getAccruedPythFees()
public
view
override
returns (uint accruedPythFeesInWei)
{
return _state.accruedPythFeesInWei;
@ -283,7 +292,7 @@ contract PythRandom is PythRandomState, PythRandomEvents {
function constructUserCommitment(
bytes32 userRandomness
) public pure returns (bytes32 userCommitment) {
) public pure override returns (bytes32 userCommitment) {
userCommitment = keccak256(bytes.concat(userRandomness));
}
@ -291,7 +300,7 @@ contract PythRandom is PythRandomState, PythRandomEvents {
bytes32 userRandomness,
bytes32 providerRandomness,
bytes32 blockHash
) public pure returns (bytes32 combinedRandomness) {
) public pure override returns (bytes32 combinedRandomness) {
combinedRandomness = keccak256(
abi.encodePacked(userRandomness, providerRandomness, blockHash)
);

View File

@ -36,6 +36,7 @@
"@openzeppelin/hardhat-upgrades": "^1.22.1",
"@pythnetwork/pyth-multisig-wh-message-builder": "*",
"@pythnetwork/pyth-sdk-solidity": "^2.2.0",
"@pythnetwork/entropy-sdk-solidity": "*",
"contract_manager": "*",
"dotenv": "^10.0.0",
"elliptic": "^6.5.2",

View File

@ -0,0 +1,79 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "./PythRandomEvents.sol";
interface IEntropy is PythRandomEvents {
// Register msg.sender as a randomness provider. The arguments are the provider's configuration parameters
// and initial commitment. Re-registering the same provider rotates the provider's commitment (and updates
// the feeInWei).
//
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
function register(
uint feeInWei,
bytes32 commitment,
bytes32 commitmentMetadata,
uint64 chainLength
) external;
// Withdraw a portion of the accumulated fees for the provider msg.sender.
// Calling this function will transfer `amount` wei to the caller (provided that they have accrued a sufficient
// balance of fees in the contract).
function withdraw(uint256 amount) external;
// As a user, request a random number from `provider`. Prior to calling this method, the user should
// generate a random number x and keep it secret. The user should then compute hash(x) and pass that
// as the userCommitment argument. (You may call the constructUserCommitment method to compute the hash.)
//
// This method returns a sequence number. The user should pass this sequence number to
// their chosen provider (the exact method for doing so will depend on the provider) to retrieve the provider's
// number. The user should then call fulfillRequest to construct the final random number.
//
// This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value.
// Note that excess value is *not* refunded to the caller.
function request(
address provider,
bytes32 userCommitment,
bool useBlockHash
) external payable returns (uint64 assignedSequenceNumber);
// Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof
// against the corresponding commitments in the in-flight request. If both values are validated, this function returns
// the corresponding random number.
//
// Note that this function can only be called once per in-flight request. Calling this function deletes the stored
// request information (so that the contract doesn't use a linear amount of storage in the number of requests).
// If you need to use the returned random number more than once, you are responsible for storing it.
function reveal(
address provider,
uint64 sequenceNumber,
bytes32 userRandomness,
bytes32 providerRevelation
) external returns (bytes32 randomNumber);
function getProviderInfo(
address provider
) external view returns (PythRandomStructs.ProviderInfo memory info);
function getRequest(
address provider,
uint64 sequenceNumber
) external view returns (PythRandomStructs.Request memory req);
function getFee(address provider) external view returns (uint feeAmount);
function getAccruedPythFees()
external
view
returns (uint accruedPythFeesInWei);
function constructUserCommitment(
bytes32 userRandomness
) external pure returns (bytes32 userCommitment);
function combineRandomValues(
bytes32 userRandomness,
bytes32 providerRandomness,
bytes32 blockHash
) external pure returns (bytes32 combinedRandomness);
}

View File

@ -0,0 +1,29 @@
{
"name": "@pythnetwork/entropy-sdk-solidity",
"version": "0.1.0",
"description": "Generate secure random numbers with Pyth Entropy",
"repository": {
"type": "git",
"url": "https://github.com/pyth-network/pyth-crosschain",
"directory": "target_chains/ethereum/entropy_sdk/solidity"
},
"scripts": {
"format": "npx prettier --write ."
},
"keywords": [
"pyth",
"solidity",
"random"
],
"author": "Douro Labs",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/pyth-network/pyth-crosschain/issues"
},
"homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/entropy_sdk/solidity",
"devDependencies": {
"prettier": "^2.7.1",
"prettier-plugin-solidity": "^1.0.0-rc.1",
"solc": "^0.8.15"
}
}

View File

@ -0,0 +1,4 @@
lib/*
!lib/README.md
cache
out

View File

@ -0,0 +1,7 @@
[profile.default]
solc = '0.8.4'
src = 'src'
out = 'out'
libs = ['lib', '../../../entropy_sdk/solidity']
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

View File

@ -0,0 +1 @@
Forge installs the dependencies in this folder. They are .gitignored

View File

@ -0,0 +1,3 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
entropy-sdk-solidity/=../../../entropy_sdk/solidity/

View File

@ -0,0 +1,20 @@
#!/bin/bash -e
# URL of the ethereum RPC node to use. Choose this based on your target network
RPC_URL=https://api.avax-test.network/ext/bc/C/rpc
# The address of the Pyth contract on your network. See the list of contract addresses here https://docs.pyth.network/documentation/pythnet-price-feeds/evm
ENTROPY_CONTRACT_ADDRESS="0xD42c7a708E74AD19401D907a14146F006c851Ee3"
PROVIDER="0x368397bDc956b4F23847bE244f350Bde4615F25E"
# Avalanche fuji address:
# 0x544c5ab499C38dff495724451783F63a3eeA40F2
# Note the -l here uses a ledger wallet to deploy your contract. You may need to change this
# option if you are using a different wallet.
forge create src/CoinFlip.sol:CoinFlip \
-l \
--rpc-url $RPC_URL \
--constructor-args \
$ENTROPY_CONTRACT_ADDRESS \
$PROVIDER

View File

@ -0,0 +1,117 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "entropy-sdk-solidity/IEntropy.sol";
library CoinFlipErrors {
error IncorrectSender();
error InsufficientFee();
}
/// Example contract using Pyth Entropy to allow a user to flip a secure fair coin.
/// Users interact with the contract by sending two transactions:
/// 1. Users request a coin flip. This operation commits the flip to use a specific (but currently unknown) random number
/// generated by Pyth Entropy.
/// 2. Users reveal the result of the coin flip. This operation reveals the random number from Pyth Entropy and checks
/// its validity, then converts the random number into the result of a coin flip.
contract CoinFlip {
// Event emitted when a coin flip is requested. The sequence number is required to reveal
// the result of the flip.
event FlipRequest(uint64 sequenceNumber);
// Event emitted when the result of the coin flip is known.
event FlipResult(bool isHeads);
// Contracts using Pyth Entropy should import the solidity SDK and then store both the Entropy contract
// and a specific entropy provider to use for requests. Each provider commits to a sequence of random numbers.
// Providers are then responsible for two things:
// 1. Operating an off-chain service that reveals their random numbers once they've been committed to on-chain
// 2. Maintaining the secrecy of the other random numbers
// Users should choose a reliable provider who they trust to uphold these commitments.
// (For the moment, the only available provider is 0x368397bDc956b4F23847bE244f350Bde4615F25E)
IEntropy private entropy;
address private entropyProvider;
// The contract is required to maintain a collection of in-flight requests. This mapping allows the contract
// to match the revealed random numbers against the original requests. The key of the map can be the
// sequence number provided by the Entropy protocol, and the value can be whatever information the protocol
// needs to resolve in-flight requests.
mapping(uint64 => address) private requestedFlips;
constructor(address _entropy, address _entropyProvider) {
entropy = IEntropy(_entropy);
entropyProvider = _entropyProvider;
}
// Request to flip a coin. The caller should generate a random number prior to calling this method, then
// submit the hash of that number as userCommitment. (You can call `IEntropy.constructUserCommitment` with
// the random number to generate the commitment.)
function requestFlip(bytes32 userCommitment) external payable {
// The entropy protocol requires the caller to pay a fee (in native gas tokens) per requested random number.
// This fee can either be paid by the contract itself or passed on to the end user.
// This implementation of the requestFlip method passes on the fee to the end user.
uint256 fee = entropy.getFee(entropyProvider);
if (msg.value < fee) {
revert CoinFlipErrors.InsufficientFee();
}
// Request the random number from the Entropy protocol. The call returns a sequence number that uniquely
// identifies the generated random number. Callers should save this sequence number so that they can match
// which request is being revealed in the next stage of the protocol.
//
// The final `true` parameter to this method incorporates the blockhash of the request's block into the
// generated random value. The blockhash adds another level of security and manipulation-resistance to the
// random value. Set this to `true` unless your blockchain has poor support for retrieving blockhashes.
uint64 sequenceNumber = entropy.request{value: fee}(
entropyProvider,
userCommitment,
true
);
requestedFlips[sequenceNumber] = msg.sender;
emit FlipRequest(sequenceNumber);
}
// Reveal the result of the coin flip. The caller must have an in-flight request for a coin flip, which is
// identified by `sequenceNumber`. The caller must additionally provide the random number that they previously
// committed to, as well as the entropy provider's random number. The provider's random number can be retrieved
// from them in a provider-dependent manner.
//
// For the moment, the provider 0x368397bDc956b4F23847bE244f350Bde4615F25E hosts a webservice at
// https://fortuna-staging.pyth.network/ that allows anyone to retrieve their random values.
// Fetch the following url:
// https://fortuna-staging.pyth.network/v1/chains/<chain id>/revelations/<sequence number>
//
// The list of supported chain ids is available here https://fortuna-staging.pyth.network/v1/chains
//
// **Warning** users of this protocol can stall the protocol by choosing not to reveal their generated random number.
// Developers using Pyth Entropy should ensure that users are always incentivized (or at least, not disincentivized)
// to finish both stages of the protocol.
function revealFlip(
uint64 sequenceNumber,
bytes32 userRandom,
bytes32 providerRandom
) public {
// Validate that the caller is allowed to reveal the result of this particular in-flight request.
if (requestedFlips[sequenceNumber] != msg.sender) {
revert CoinFlipErrors.IncorrectSender();
}
// Optional: delete the in-flight request to save gas / chain storage.
delete requestedFlips[sequenceNumber];
// Reveal the random number. This call reverts if the provided values fail to match the commitments
// from the request phase. If the call returns, randomNumber is a uniformly distributed bytes32.
bytes32 randomNumber = entropy.reveal(
entropyProvider,
sequenceNumber,
userRandom,
providerRandom
);
// You can then convert the returned bytes32 into the range required by your application.
emit FlipResult(uint256(randomNumber) % 2 == 0);
}
receive() external payable {}
}

View File

@ -17,7 +17,7 @@
"solidity",
"oracle"
],
"author": "Pyth Data Foundation",
"author": "Pyth Data Association",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/pyth-network/pyth-crosschain/issues"