Gas Network V2 Oracle
Overview
Gasnet currently stores estimates in the form of values representing facts about chain’s gas market or estimates for future prices. The values for a specific chain and timestamp has the following structure:
interface OraclePayload {
height: bigint
timestamp: bigint
systemid: number
chainid: bigint
payloads: Array<OracleRecord>
}
Each OraclePayload
contains a specific typ
and its associated value
:
interface OracleRecord {
typ: number
value: string
}
The typ
identifies the meaning of the value
. It can be a fact about the chain’s gas market (e.g. base fee) or an estimate for future price to settle on that chain (e.g. 90% confidence for inclusion in next 10 seconds).
Additional estimate values will be added to accommodate chain specific mechanics, such as blob pricing. Gas estimates are available for all the chains currently support by the Blocknative Gas API. The complete list of available chains is listed in the API docs available here: https://docs.blocknative.com/gas-prediction/gas-platform
Fetch Gasnet Estimation Data
The following code will fetch a gas estimate from Gasnet using Ethers in Typescript:
const GASNET_URL=https://test.devnet.gas.network
const GASNET_CONTRACT_ADDRESS = '0x4245Cb7c690B650a38E680C9BcfB7814D42BfD32'
const gasnetProvider = new ethers.JsonRpcProvider(GASNET_URL);
const gasnetContract = new ethers.Contract(
GASNET_CONTRACT_ADDRESS,
abi,
gasnetProvider
);
const payload = await gasnetContract.getValues('2', '1') // get estimates for Ethereum (chain id 1)
This estimate containing the supported values for the specified chain includes a signature that the Gasnet Pull Oracle on the Consumer Chain uses to verify the integrity of the estimate.
Update Consumer Chain Oracle
Once a gas estimation is retrieved from Gasnet above, the following sample code puts the data on the Consumer Chain with the Pull Oracle using Ethers in Typescript:
Currently the Pull Oracle contract is deployed to the following networks. More will be added soon.
Mainnet
// Arbitrum One
export const CONSUMER_CHAIN_URL=https://arbitrum.llamarpc.com
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x1c51B22954af03FE11183aaDF43F6415907a9287'
// Base
export const CONSUMER_CHAIN_URL=https://base.llamarpc.com
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x35FD54AcB3219C90EFC9823877f3Cb2Df2b30e3f'
// Ethereum
export const CONSUMER_CHAIN_URL=https://eth.llamarpc.com
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x0248BC1998C36087B0B0d2D17B9461827067ae04'
// Linea
export const CONSUMER_CHAIN_URL=https://rpc.linea.build
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x34816ce96F8451d21f4D61fe9543Bd78F3326048'
// Optimism
export const CONSUMER_CHAIN_URL=https://optimism.llamarpc.com
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x152bB09cE15f1149dBE46EB9E4D4e1B572513453'
// Unichain
export const CONSUMER_CHAIN_URL=https://mainnet.unichain.org
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x9d1F623d8708f950C266b422D5Bffa5E244A08e1'
Testnet
// Base Sepolia
export const CONSUMER_CHAIN_URL=https://sepolia.base.org
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0xD87f5Ea40C592DfFAe5B87922E1cdA2bb44CB67F'
// Ethereum Sepolia
export const CONSUMER_CHAIN_URL=https://endpoints.omniatech.io/v1/eth/sepolia/public
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0xCc936bE977BeDb5140C5584d8B6043C9068622A6'
// Linea Sepolia
export const CONSUMER_CHAIN_URL=https://linea-sepolia-rpc.publicnode.com
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x2c84370DaddBcD67d729689671A9Fe63DF39Cf13'
// Optimism Sepolia
export const CONSUMER_CHAIN_URL=https://sepolia.optimism.io
export const CONSUMER_CHAIN_CONTRACT_ADDRESS = '0x20A5DCE3646BD975edEE3082319bd0dB64A0e0B9'
async function publishGasEstimation(provider: any) {
try {
const { ethers } = await loadEthers()
const ethersProvider = new ethers.BrowserProvider(provider, 'any')
const signer = await ethersProvider.getSigner()
const consumerContract = new ethers.Contract(contractAddress, abi, signer)
let transaction = await consumerContract.storeValues(payload)
const receipt = await transaction.wait()
console.info('Publication success:', receipt.hash)
} catch (error) {
const revertErrorFromGasNetContract = (error as any)?.info?.error?.message
console.error('Publication error:', error)
}
}
At this point the estimates are available on the Consumer Chain for use by other contracts on that chain. You can find example code to update the estimate on the Consumer Chain using an injected provider (i.e. wallet) in the demo dapp .
Parse GasNet Estimation Data
In order to minimize gas usage and network transport when updating the oracle, the estimation data is binary encoded. The payload contains a signed header and a set of values.
The payload starts with a 32 byte header, a set of 32 byte type-value pairs or records, followed by a 65 byte signature. All the values are written in BigEndian byte order.
payload = header + (record * n) + sig( keccak256(header + (values * n)) )
This payload format is visualized using Mermaid below, where the first row indicates each byte. You can copy/paste the code into the Mermaid playground at https://www.mermaidchart.com/play to zoom into details.
block-beta
columns 32
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
23 24 25 26 27 28 29 30 31 32
a[" Empty "]:6
length["Length: uint16"]:2
timestamp["Timestamp: uint48"]:6
sid["System ID: uint8"]:1
chid["Chain ID: uint64"]:8
Height["Height: uint64"]:8
Version["Version: uint8"]:1
rec1typ["Record 1 Type"]:2
rec1val["Record 1 Value"]:30
rec2typ["Record 2 Type"]:2
rec2val["Record 2 Value"]:30
rec3typ["Record 3 Type"]:2
rec3val["Record 3 Value"]:30
sigr["Signature - part R"]:32
sigs["Signature - part S"]:32
sigv["Signature - part V"]:1
Header
Each payload begins with a 32 byte header with the following structure specified right to left byte order (See “starting position).
Name | Type | Size | Starting Position | Description | |
---|---|---|---|---|---|
1 | Version | uint8 | 1 | 32 | The version of the entire payload. This document describes the second version of payload |
2 | Height | uint64 | 8 | 31 | The height, or blocknumber, of the estimated chain at time of estimate. |
3 | ChainID | uint64 | 8 | 23 | The chain id of the estimated chain. For EVM chains, this is the EIP-155 id. |
4 | SystemID | uint8 | 1 | 15 | The system id of the estiimate chain. For EVM chains, this value is 2 in decimal. Non-EVM chains will have other system ids (e.g. BTC is 1 ). |
5 | Timestamp | uint48 | 6 | 14 | The unix millisecond timestamp when the estimate was determined. |
6 | Length | uint16 | 2 | 8 | The count of records, or type/value pairs, in the payload |
7 | Empty | 6 | Not used but reserved for future expansion. |
Record(s)
Each payload contains 1 or more 32 bytes records containing the values of the estimate
Name | Type | Size | Starting Position | Description | |
---|---|---|---|---|---|
1 | Value | uint240 | 30 | 0 | The value is the value of the record indicated by type number |
2 | Type | uint16 | 2 | 31 | The type describing what the value represents. It is specific to chain + system mixture. Some currently deployed types are listed in the next table. |
Currently Used Types
The following types are currently supported. This list will be updated as more types are defined and supported on GasNet.
TypeID | Name | Type | Systems | Chains |
---|---|---|---|---|
107 | base_fee_per_gas | Number | 2 | 1 |
112 | blob_base_fee_per_gas | Number | 2 | 1 |
322 | pred_max_priority_fee_per_gas_p90 | Number | 2 | 1 |
The following sample code will unpack and parse the above format into its constituent parts and return as an OraclePayload
with one or more OracleRecord
items defined above.
function parsePayload(payload: string): OraclePayload {
let pV = {} as OraclePayload
const buf = Buffer.from(payload.replace('0x', ''), 'hex')
pV.height = buf.readBigUInt64BE(0x17)
pV.chainid = buf.readBigUInt64BE(0xf)
pV.systemid = buf.readUint8(0xe)
let timeBuff = Buffer.alloc(8)
buf.copy(timeBuff, 2, 0x8, 0xe)
pV.timestamp = timeBuff.readBigUInt64BE(0)
let pLen = buf.readUint16BE(0x6)
pV.payloads = new Array()
let value = Buffer.alloc(0x20)
let pos = 0
for (let i = 0; i < pLen; i++) {
pos += 0x20 // also, skip header
buf.copy(value, 2, pos + 0x2)
pV.payloads.push({
typ: buf.readUint16BE(pos),
value: '0x' + value.toString('hex')
} as OracleRecord)
}
// ... and signature
return pV
Read Consumer Chain Oracle
Sample code calling the Oracle to read a gas estimate using ethers in Typescript:
async function handleOracleValues(gasNetContract: Contract) {
try {
// get base fee (107), blob base fee (112), and p90 max priority fee (322)
const valuesObject = await ([107, 112, 322]
).reduce(async (accPromise, typ) => {
const acc = await accPromise
const contractRespPerType = await gasNetContract.getInTime(
'2', // evm architecture
'1', // Ethereum mainnet
typ,
3600000 // recency in ms
)
const [value, blockNumber, estTimestamp] = contractRespPerType
// A value of 0 inidicates no data available
if (value === 0n && height === 0n && timestamp === 0n) {
throw (`Estimate not available at selected recency`
}
return {
chainId,
estTimestamp,
blockNumber,
value: (Number(value) / 1e9).toPrecision(4)
}
}, Promise.resolve({}))
return (valuesObject)
} catch (error) {
console.error('Error Reading V2 Published Data:', error)
const revertErrorFromContract = (error as any)?.info?.error?.message || (error as any)?.reason
readFromTargetNetErrorMessage = revertErrorFromContract || (error as string)
return null
}
}
async function readFromOracle(provider: any) {
try {
const { ethers } = await loadEthers()
const ethersProvider = new ethers.BrowserProvider(provider, 'any')
const signer = await ethersProvider.getSigner()
const gasNetContract = new ethers.Contract(
CONSUMER_CHAIN_CONTRACT_ADDRESS, abi, signer
)
publishedGasData = handleOracleValues(gasNetContract)
} catch (error) {
console.error('Gas data read published data error:', error)
const revertErrorFromContract = (error as any)?.info?.error?.message || (error as any)?.reason
}
}