diff --git a/genesis-tool/config/genesis_config_single.json b/genesis-tool/config/genesis_config_single.json index e280488..4854106 100644 --- a/genesis-tool/config/genesis_config_single.json +++ b/genesis-tool/config/genesis_config_single.json @@ -45,9 +45,21 @@ }, "oracleConfig": { - "_comment": "NativeOracle.initialize - sourceType 1 = JWK", + "_comment": "NativeOracle.initialize - sourceType 1 = JWK, 0 = Blockchain", "sourceTypes": [1], - "callbacks": ["0x00000000000000000000000000000001625F2018"] + "callbacks": ["0x00000000000000000000000000000001625F4001"], + "bridgeConfig": { + "deploy": true, + "trustedBridge": "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc" + }, + "tasks": [ + { + "sourceType": 0, + "sourceId": 31337, + "taskName": "events", + "config": "gravity://0/31337/events?contract=0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc&eventSignature=0xd53bfb630c04654c6d1da5020f14674f190f92c257c92d9b15d8ecb405057c14&fromBlock=0" + } + ] }, "jwkConfig": { diff --git a/genesis-tool/src/genesis.rs b/genesis-tool/src/genesis.rs index 5bca9fd..68b9e12 100644 --- a/genesis-tool/src/genesis.rs +++ b/genesis-tool/src/genesis.rs @@ -1,19 +1,21 @@ use alloy_sol_macro::sol; use alloy_sol_types::SolCall; -use revm_primitives::{Address, Bytes, ExecutionResult, TxEnv, U256, hex}; +use revm_primitives::{hex, Address, Bytes, ExecutionResult, TxEnv, U256}; use serde::{Deserialize, Serialize}; use tracing::{error, info}; use crate::{ post_genesis::handle_execution_result, - utils::{GENESIS_ADDR, VALIDATOR_MANAGER_ADDR, new_system_call_txn, new_system_call_txn_with_value}, + utils::{ + new_system_call_txn, new_system_call_txn_with_value, GENESIS_ADDR, VALIDATOR_MANAGER_ADDR, + }, }; /// Derive 32-byte AccountAddress from BLS consensus public key using SHA3-256 /// This matches the derivation used in gravity-reth for validator identity fn derive_account_address_from_consensus_pubkey(consensus_pubkey: &[u8]) -> [u8; 32] { use tiny_keccak::{Hasher, Sha3}; - + let mut hasher = Sha3::v256(); hasher.update(consensus_pubkey); let mut output = [0u8; 32]; @@ -29,34 +31,34 @@ fn derive_account_address_from_consensus_pubkey(consensus_pubkey: &[u8]) -> [u8; pub struct GenesisConfig { #[serde(rename = "validatorConfig")] pub validator_config: ValidatorConfigParams, - + #[serde(rename = "stakingConfig")] pub staking_config: StakingConfigParams, - + #[serde(rename = "governanceConfig")] pub governance_config: GovernanceConfigParams, - + #[serde(rename = "epochIntervalMicros")] pub epoch_interval_micros: u64, - + #[serde(rename = "majorVersion")] pub major_version: u64, - + #[serde(rename = "consensusConfig")] - pub consensus_config: String, // hex bytes - + pub consensus_config: String, // hex bytes + #[serde(rename = "executionConfig")] - pub execution_config: String, // hex bytes - + pub execution_config: String, // hex bytes + #[serde(rename = "randomnessConfig")] pub randomness_config: RandomnessConfigData, - + #[serde(rename = "oracleConfig")] pub oracle_config: OracleInitParams, - + #[serde(rename = "jwkConfig")] pub jwk_config: JWKInitParams, - + pub validators: Vec, } @@ -64,19 +66,19 @@ pub struct GenesisConfig { pub struct ValidatorConfigParams { #[serde(rename = "minimumBond")] pub minimum_bond: String, - + #[serde(rename = "maximumBond")] pub maximum_bond: String, - + #[serde(rename = "unbondingDelayMicros")] pub unbonding_delay_micros: u64, - + #[serde(rename = "allowValidatorSetChange")] pub allow_validator_set_change: bool, - + #[serde(rename = "votingPowerIncreaseLimitPct")] pub voting_power_increase_limit_pct: u64, - + #[serde(rename = "maxValidatorSetSize")] pub max_validator_set_size: String, } @@ -85,13 +87,13 @@ pub struct ValidatorConfigParams { pub struct StakingConfigParams { #[serde(rename = "minimumStake")] pub minimum_stake: String, - + #[serde(rename = "lockupDurationMicros")] pub lockup_duration_micros: u64, - + #[serde(rename = "unbondingDelayMicros")] pub unbonding_delay_micros: u64, - + #[serde(rename = "minimumProposalStake")] pub minimum_proposal_stake: String, } @@ -100,18 +102,18 @@ pub struct StakingConfigParams { pub struct GovernanceConfigParams { #[serde(rename = "minVotingThreshold")] pub min_voting_threshold: String, - + #[serde(rename = "requiredProposerStake")] pub required_proposer_stake: String, - + #[serde(rename = "votingDurationMicros")] pub voting_duration_micros: u64, } #[derive(Debug, Deserialize, Serialize, Clone)] pub struct RandomnessConfigData { - pub variant: u8, // 0 = Off, 1 = V2 - + pub variant: u8, // 0 = Off, 1 = V2 + #[serde(rename = "configV2")] pub config_v2: ConfigV2Data, } @@ -132,13 +134,51 @@ pub struct ConfigV2Data { pub struct OracleInitParams { #[serde(rename = "sourceTypes")] pub source_types: Vec, - - pub callbacks: Vec, // addresses as hex strings + + pub callbacks: Vec, // addresses as hex strings + + #[serde(default)] + pub tasks: Vec, + + #[serde(rename = "bridgeConfig", default)] + pub bridge_config: BridgeConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct OracleTaskParams { + #[serde(rename = "sourceType")] + pub source_type: u32, + + #[serde(rename = "sourceId")] + pub source_id: u64, + + #[serde(rename = "taskName")] + pub task_name: String, // string, will be hashed or passed as bytes32? User said bytes32. + // But JSON config usually has strings. + // Genesis.sol expects bytes32. + // We should probably accept a string and keccak256 it? + // Or string and 0-pad? + // User instructions: "OracleTaskConfig.setTask(..., bytes32 taskName, ...)" + // jwk_consensus_config used keccak256("events"). + // Implementation plan said: "URI...". + // Let's assume input is string. If it starts with "0x", parse as bytes32. + // Otherwise keccak256 it? + // Actually, `uri_parser` said `task_type` defaults to "events". + // So `keccak256("events")` is likely. + pub config: String, // The URI string +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct BridgeConfig { + pub deploy: bool, + + #[serde(rename = "trustedBridge")] + pub trusted_bridge: String, // address } #[derive(Debug, Deserialize, Serialize, Clone)] pub struct JWKInitParams { - pub issuers: Vec, // hex-encoded bytes + pub issuers: Vec, // hex-encoded bytes pub jwks: Vec>, } @@ -155,24 +195,24 @@ pub struct RSA_JWK_Json { pub struct InitialValidator { pub operator: String, pub owner: String, - + #[serde(rename = "stakeAmount")] pub stake_amount: String, - + pub moniker: String, - + #[serde(rename = "consensusPubkey")] - pub consensus_pubkey: String, // hex bytes - + pub consensus_pubkey: String, // hex bytes + #[serde(rename = "consensusPop")] - pub consensus_pop: String, // hex bytes - + pub consensus_pop: String, // hex bytes + #[serde(rename = "networkAddresses")] - pub network_addresses: String, // human-readable format: /ip4/127.0.0.1/tcp/2024/noise-ik/.../handshake/0 - + pub network_addresses: String, // human-readable format: /ip4/127.0.0.1/tcp/2024/noise-ik/.../handshake/0 + #[serde(rename = "fullnodeAddresses")] - pub fullnode_addresses: String, // human-readable format: /ip4/127.0.0.1/tcp/2024/noise-ik/.../handshake/0 - + pub fullnode_addresses: String, // human-readable format: /ip4/127.0.0.1/tcp/2024/noise-ik/.../handshake/0 + #[serde(rename = "votingPower")] pub voting_power: String, } @@ -190,36 +230,50 @@ sol! { uint64 votingPowerIncreaseLimitPct; uint256 maxValidatorSetSize; } - + struct SolStakingConfigParams { uint256 minimumStake; uint64 lockupDurationMicros; uint64 unbondingDelayMicros; uint256 minimumProposalStake; } - + struct SolGovernanceConfigParams { uint128 minVotingThreshold; uint256 requiredProposerStake; uint64 votingDurationMicros; } - + struct SolConfigV2Data { uint128 secrecyThreshold; uint128 reconstructionThreshold; uint128 fastPathSecrecyThreshold; } - + struct SolRandomnessConfigData { uint8 variant; SolConfigV2Data configV2; } - + + struct SolOracleTaskParams { + uint32 sourceType; + uint256 sourceId; + bytes32 taskName; + bytes config; + } + + struct SolBridgeConfig { + bool deploy; + address trustedBridge; + } + struct SolOracleInitParams { uint32[] sourceTypes; address[] callbacks; + SolOracleTaskParams[] tasks; + SolBridgeConfig bridgeConfig; } - + struct SolRSA_JWK { string kid; string kty; @@ -227,12 +281,12 @@ sol! { string e; string n; } - + struct SolJWKInitParams { bytes[] issuers; SolRSA_JWK[][] jwks; } - + struct SolInitialValidator { address operator; address owner; @@ -244,7 +298,7 @@ sol! { bytes fullnodeAddresses; uint256 votingPower; } - + struct SolGenesisInitParams { SolValidatorConfigParams validatorConfig; SolStakingConfigParams stakingConfig; @@ -258,7 +312,7 @@ sol! { SolJWKInitParams jwkConfig; SolInitialValidator[] validators; } - + contract Genesis { function initialize(SolGenesisInitParams calldata params) external payable; } @@ -269,15 +323,18 @@ sol! { // ============================================================================ fn parse_u256(s: &str) -> U256 { - s.parse::().expect(&format!("Invalid U256 string: {}", s)) + s.parse::() + .expect(&format!("Invalid U256 string: {}", s)) } fn parse_u128(s: &str) -> u128 { - s.parse::().expect(&format!("Invalid u128 string: {}", s)) + s.parse::() + .expect(&format!("Invalid u128 string: {}", s)) } fn parse_address(s: &str) -> Address { - s.parse::
().expect(&format!("Invalid address: {}", s)) + s.parse::
() + .expect(&format!("Invalid address: {}", s)) } fn parse_hex_bytes(s: &str) -> Vec { @@ -304,7 +361,7 @@ pub fn convert_config_to_sol(config: &GenesisConfig) -> SolGenesisInitParams { votingPowerIncreaseLimitPct: config.validator_config.voting_power_increase_limit_pct, maxValidatorSetSize: parse_u256(&config.validator_config.max_validator_set_size), }; - + // Convert StakingConfig let staking_config = SolStakingConfigParams { minimumStake: parse_u256(&config.staking_config.minimum_stake), @@ -312,40 +369,93 @@ pub fn convert_config_to_sol(config: &GenesisConfig) -> SolGenesisInitParams { unbondingDelayMicros: config.staking_config.unbonding_delay_micros, minimumProposalStake: parse_u256(&config.staking_config.minimum_proposal_stake), }; - + // Convert GovernanceConfig let governance_config = SolGovernanceConfigParams { minVotingThreshold: parse_u128(&config.governance_config.min_voting_threshold), requiredProposerStake: parse_u256(&config.governance_config.required_proposer_stake), votingDurationMicros: config.governance_config.voting_duration_micros, }; - + // Convert RandomnessConfig let randomness_config = SolRandomnessConfigData { variant: config.randomness_config.variant, configV2: SolConfigV2Data { secrecyThreshold: config.randomness_config.config_v2.secrecy_threshold, reconstructionThreshold: config.randomness_config.config_v2.reconstruction_threshold, - fastPathSecrecyThreshold: config.randomness_config.config_v2.fast_path_secrecy_threshold, + fastPathSecrecyThreshold: config + .randomness_config + .config_v2 + .fast_path_secrecy_threshold, }, }; - + // Convert OracleConfig let oracle_config = SolOracleInitParams { sourceTypes: config.oracle_config.source_types.clone(), - callbacks: config.oracle_config.callbacks.iter() + callbacks: config + .oracle_config + .callbacks + .iter() .map(|s| parse_address(s)) .collect(), + tasks: config + .oracle_config + .tasks + .iter() + .map(|t| { + // Handle taskName: if it starts with 0x, parse as bytes32, else keccak256 hash of string + let task_name_bytes = if t.task_name.starts_with("0x") { + let s = t.task_name.strip_prefix("0x").unwrap(); + let bytes = hex::decode(s).expect("Invalid hex for taskName"); + let mut b32 = [0u8; 32]; + if bytes.len() > 32 { + panic!("taskName hex too long"); + } + b32[..bytes.len()].copy_from_slice(&bytes); + b32 + } else { + use tiny_keccak::{Hasher, Keccak}; + let mut hasher = Keccak::v256(); + let mut output = [0u8; 32]; + hasher.update(t.task_name.as_bytes()); + hasher.finalize(&mut output); + output + }; + + SolOracleTaskParams { + sourceType: t.source_type, + sourceId: U256::from(t.source_id), + taskName: task_name_bytes.into(), + config: t.config.as_bytes().to_vec().into(), // encode string as bytes + } + }) + .collect(), + bridgeConfig: SolBridgeConfig { + deploy: config.oracle_config.bridge_config.deploy, + trustedBridge: if config.oracle_config.bridge_config.trusted_bridge.is_empty() { + Address::ZERO + } else { + parse_address(&config.oracle_config.bridge_config.trusted_bridge) + }, + }, }; - + // Convert JWKConfig let jwk_config = SolJWKInitParams { - issuers: config.jwk_config.issuers.iter() + issuers: config + .jwk_config + .issuers + .iter() .map(|s| parse_hex_bytes(s).into()) .collect(), - jwks: config.jwk_config.jwks.iter() + jwks: config + .jwk_config + .jwks + .iter() .map(|provider_jwks| { - provider_jwks.iter() + provider_jwks + .iter() .map(|jwk| SolRSA_JWK { kid: jwk.kid.clone(), kty: jwk.kty.clone(), @@ -357,9 +467,11 @@ pub fn convert_config_to_sol(config: &GenesisConfig) -> SolGenesisInitParams { }) .collect(), }; - + // Convert Validators - let validators: Vec = config.validators.iter() + let validators: Vec = config + .validators + .iter() .map(|v| SolInitialValidator { operator: parse_address(&v.operator), owner: parse_address(&v.owner), @@ -373,7 +485,7 @@ pub fn convert_config_to_sol(config: &GenesisConfig) -> SolGenesisInitParams { votingPower: parse_u256(&v.voting_power), }) .collect(); - + SolGenesisInitParams { validatorConfig: validator_config, stakingConfig: staking_config, @@ -391,7 +503,9 @@ pub fn convert_config_to_sol(config: &GenesisConfig) -> SolGenesisInitParams { /// Calculate total stake amount needed for Genesis.initialize (payable) pub fn calculate_total_stake(config: &GenesisConfig) -> U256 { - config.validators.iter() + config + .validators + .iter() .map(|v| parse_u256(&v.stake_amount)) .fold(U256::ZERO, |acc, stake| acc + stake) } @@ -399,7 +513,7 @@ pub fn calculate_total_stake(config: &GenesisConfig) -> U256 { pub fn call_genesis_initialize(genesis_address: Address, config: &GenesisConfig) -> TxEnv { let sol_params = convert_config_to_sol(config); let total_stake = calculate_total_stake(config); - + info!("=== Genesis Initialize Parameters ==="); info!("Genesis address: {:?}", genesis_address); info!("Total stake value: {} wei", total_stake); @@ -407,15 +521,16 @@ pub fn call_genesis_initialize(genesis_address: Address, config: &GenesisConfig) info!("Epoch interval: {} micros", config.epoch_interval_micros); info!("Major version: {}", config.major_version); info!("Randomness variant: {}", config.randomness_config.variant); - info!("Oracle source types: {:?}", config.oracle_config.source_types); + info!( + "Oracle source types: {:?}", + config.oracle_config.source_types + ); info!("JWK issuers count: {}", config.jwk_config.issuers.len()); - - let call_data = Genesis::initializeCall { - params: sol_params, - }.abi_encode(); - + + let call_data = Genesis::initializeCall { params: sol_params }.abi_encode(); + info!("Call data length: {}", call_data.len()); - + // Genesis.initialize is payable - need to send total stake amount new_system_call_txn_with_value(genesis_address, call_data.into(), total_stake) } @@ -436,7 +551,7 @@ sol! { bytes networkAddresses; bytes fullnodeAddresses; } - + function getActiveValidators() external view returns (ValidatorConsensusInfo[] memory); } } @@ -448,12 +563,13 @@ pub fn call_get_active_validators() -> TxEnv { pub fn print_active_validators_result(result: &ExecutionResult, config: &GenesisConfig) { handle_execution_result(result, "getActiveValidators", |output_bytes| { - let decoded = IValidatorManagement::getActiveValidatorsCall::abi_decode_returns(output_bytes, false) - .expect("Failed to decode getActiveValidators result"); - + let decoded = + IValidatorManagement::getActiveValidatorsCall::abi_decode_returns(output_bytes, false) + .expect("Failed to decode getActiveValidators result"); + let validators = &decoded._0; info!("Active validators count: {}", validators.len()); - + // Validate against config if validators.len() != config.validators.len() { error!( @@ -463,19 +579,29 @@ pub fn print_active_validators_result(result: &ExecutionResult, config: &Genesis ); return; } - + for (i, validator) in validators.iter().enumerate() { // Derive account address from consensus pubkey using SHA3-256 - let account_address = derive_account_address_from_consensus_pubkey(&validator.consensusPubkey); - + let account_address = + derive_account_address_from_consensus_pubkey(&validator.consensusPubkey); + info!("--- Validator {} ---", i + 1); info!(" ETH Address: {:?}", validator.validator); - info!(" Account Address (from consensus pubkey): 0x{}", hex::encode(account_address)); - info!(" Consensus Pubkey: 0x{}", hex::encode(&validator.consensusPubkey)); + info!( + " Account Address (from consensus pubkey): 0x{}", + hex::encode(account_address) + ); + info!( + " Consensus Pubkey: 0x{}", + hex::encode(&validator.consensusPubkey) + ); info!(" Index: {}", validator.validatorIndex); info!(" Voting Power: {}", validator.votingPower); } - - info!("🎉 All {} validators initialized successfully!", validators.len()); + + info!( + "🎉 All {} validators initialized successfully!", + validators.len() + ); }); } diff --git a/remappings.txt b/remappings.txt index b47fbe8..006ab29 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ @src/=src/ +@test/=test/ @openzeppelin/=node_modules/@openzeppelin/contracts/ forge-std/=node_modules/forge-std/src/ diff --git a/script/BridgeInteraction.s.sol b/script/BridgeInteraction.s.sol new file mode 100644 index 0000000..a956030 --- /dev/null +++ b/script/BridgeInteraction.s.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import { Script, console } from "forge-std/Script.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { GravityPortal } from "@src/oracle/evm/GravityPortal.sol"; +import { GBridgeSender } from "@src/oracle/evm/native_token_bridge/GBridgeSender.sol"; +import { PortalMessage } from "@src/oracle/evm/PortalMessage.sol"; +import { MockGToken } from "@test/utils/MockGToken.sol"; + +/// @title BridgeInteraction +/// @notice Interactive script to test bridge functionality and capture MessageSent events +/// @dev Run after DeployBridgeLocal. Outputs block number and event content. +contract BridgeInteraction is Script { + function run() external { + // Load contract addresses from environment + address gTokenAddr = vm.envAddress("GTOKEN_ADDRESS"); + address portalAddr = vm.envAddress("PORTAL_ADDRESS"); + address senderAddr = vm.envAddress("SENDER_ADDRESS"); + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + address user = vm.addr(privateKey); + + MockGToken gToken = MockGToken(gTokenAddr); + GravityPortal portal = GravityPortal(portalAddr); + GBridgeSender sender = GBridgeSender(senderAddr); + + console.log("=== Bridge Interaction Test ==="); + console.log("User:", user); + console.log("GToken:", gTokenAddr); + console.log("Portal:", portalAddr); + console.log("Sender:", senderAddr); + console.log(""); + + uint256 amount = 1000 ether; + address recipient = user; // Bridge to self for testing + + vm.startBroadcast(privateKey); + + // 1. Mint tokens to user + gToken.mint(user, amount); + console.log("[Step 1] Minted", amount / 1e18, "G tokens to user"); + + // 2. Approve GBridgeSender + gToken.approve(address(sender), amount); + console.log("[Step 2] Approved GBridgeSender to spend tokens"); + + // 3. Calculate fee + uint256 fee = sender.calculateBridgeFee(amount, recipient); + console.log("[Step 3] Required fee:", fee, "wei"); + + // Record current block before bridge call + uint256 blockBefore = block.number; + + // 4. Start recording logs to capture events + vm.recordLogs(); + + // 5. Call bridgeToGravity + uint128 nonce = sender.bridgeToGravity{ value: fee }(amount, recipient); + + vm.stopBroadcast(); + + // 6. Get recorded logs and parse MessageSent event + Vm.Log[] memory logs = vm.getRecordedLogs(); + + console.log(""); + console.log("=== Bridge Transaction Result ==="); + console.log("Nonce returned:", uint256(nonce)); + console.log("Portal nonce is now:", uint256(portal.nonce())); + console.log(""); + + // Find and display MessageSent event + console.log("=== Event Details ==="); + for (uint256 i = 0; i < logs.length; i++) { + // MessageSent event signature: keccak256("MessageSent(uint128,bytes)") + bytes32 messageSentSig = keccak256("MessageSent(uint128,bytes)"); + if (logs[i].topics[0] == messageSentSig) { + console.log("Event: MessageSent"); + console.log("Block Number:", blockBefore + 1); + console.log("Contract:", logs[i].emitter); + + // topics[1] is the indexed nonce + uint128 eventNonce = uint128(uint256(logs[i].topics[1])); + console.log("Indexed Nonce:", uint256(eventNonce)); + + // Decode payload from event data + bytes memory payload = abi.decode(logs[i].data, (bytes)); + console.log("Payload Length:", payload.length, "bytes"); + + // Decode PortalMessage content + (address msgSender, uint128 msgNonce, bytes memory message) = PortalMessage.decode(payload); + console.log(""); + console.log("=== Decoded PortalMessage ==="); + console.log("Sender:", msgSender); + console.log("Message Nonce:", uint256(msgNonce)); + console.log("Message Length:", message.length, "bytes"); + + // Decode bridge message (amount, recipient) + (uint256 bridgeAmount, address bridgeRecipient) = abi.decode(message, (uint256, address)); + console.log(""); + console.log("=== Decoded Bridge Message ==="); + console.log("Bridge Amount:", bridgeAmount / 1e18, "G tokens"); + console.log("Recipient:", bridgeRecipient); + + // Display raw payload in hex + console.log(""); + console.log("=== Raw Payload (hex) ==="); + console.logBytes(payload); + + break; + } + + // Also capture TokensLocked event from GBridgeSender + bytes32 tokensLockedSig = keccak256("TokensLocked(address,address,uint256,uint128)"); + if (logs[i].topics[0] == tokensLockedSig) { + console.log(""); + console.log("Event: TokensLocked"); + console.log("Contract:", logs[i].emitter); + console.log("From:", address(uint160(uint256(logs[i].topics[1])))); + console.log("Recipient:", address(uint160(uint256(logs[i].topics[2])))); + } + } + + console.log(""); + console.log("=== TEST COMPLETED SUCCESSFULLY ==="); + console.log("The MessageSent event was emitted. Validators would monitor this event"); + console.log("and reach consensus to bridge the message to Gravity chain."); + } +} diff --git a/script/DeployBridgeLocal.s.sol b/script/DeployBridgeLocal.s.sol new file mode 100644 index 0000000..fab3afc --- /dev/null +++ b/script/DeployBridgeLocal.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import { Script, console } from "forge-std/Script.sol"; +import { GravityPortal } from "@src/oracle/evm/GravityPortal.sol"; +import { GBridgeSender } from "@src/oracle/evm/native_token_bridge/GBridgeSender.sol"; +import { MockGToken } from "@test/utils/MockGToken.sol"; + +/// @title DeployBridgeLocal +/// @notice Deploys MockGToken, GravityPortal, and GBridgeSender on local Anvil testnet +/// @dev Run with: forge script script/DeployBridgeLocal.s.sol:DeployBridgeLocal --rpc-url http://localhost:8545 --broadcast +contract DeployBridgeLocal is Script { + uint256 public constant BASE_FEE = 0.001 ether; + uint256 public constant FEE_PER_BYTE = 100; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + console.log("=== Deploying Bridge Contracts ==="); + console.log("Deployer:", deployer); + console.log(""); + + vm.startBroadcast(deployerPrivateKey); + + // 1. Deploy Mock G Token + MockGToken gToken = new MockGToken(); + console.log("MockGToken deployed at:", address(gToken)); + + // 2. Deploy GravityPortal + GravityPortal portal = new GravityPortal(deployer, BASE_FEE, FEE_PER_BYTE, deployer); + console.log("GravityPortal deployed at:", address(portal)); + + // 3. Deploy GBridgeSender + GBridgeSender sender = new GBridgeSender(address(gToken), address(portal), deployer); + console.log("GBridgeSender deployed at:", address(sender)); + + vm.stopBroadcast(); + + console.log(""); + console.log("=== Deployment Complete ==="); + } +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..89edebb --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,64 @@ +# Oracle Bridge Test Scripts + +This directory contains scripts for testing the Oracle EVM Bridge. + +## Script Description + +| Script | Description | +|--------|-------------| +| `start_anvil.sh` | Start Anvil local testnet (port 8546, block-time 1s) and deploy contracts | +| `bridge_test.sh` | Call the bridge contract and display MessageSent event details | +| `stop_anvil.sh` | Stop the Anvil testnet | + +## Usage + +```bash +# 1. Start Anvil and deploy contracts +./scripts/start_anvil.sh + +# 2. Test bridge interaction (can be run multiple times) +./scripts/bridge_test.sh + +# 3. Stop Anvil when done +./scripts/stop_anvil.sh +``` + +## Deployed Contracts + +Since Anvil's default account and nonce are used, contract addresses are fixed: + +| Contract | Address | Nonce | +|----------|---------|-------| +| MockGToken | `0x5FbDB2315678afecb367f032d93F642f64180aa3` | 0 | +| GravityPortal | `0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512` | 1 | +| GBridgeSender | `0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0` | 2 | + +## Rust Unit Tests + +There are corresponding unit tests in `gravity-reth` that can read and parse these events: + +```bash +# 1. First start Anvil and deploy contracts +cd gravity_chain_core_contracts +./scripts/start_anvil.sh +./scripts/bridge_test.sh + +# 2. Run Rust tests +cd gravity-reth +cargo test --package reth-pipe-exec-layer-relayer test_poll_anvil_events -- --ignored --nocapture +``` + +The test will output the parsed event content, including fields such as sender, nonce, amount, recipient, etc. + +## Event Format + +The `GravityPortal` contract emits a `MessageSent` event: + +```solidity +event MessageSent(uint128 indexed nonce, bytes payload); +``` + +The payload uses `PortalMessage` format: +``` +sender (20 bytes) || nonce (16 bytes) || message (variable) +``` diff --git a/scripts/bridge_test.sh b/scripts/bridge_test.sh new file mode 100755 index 0000000..710547b --- /dev/null +++ b/scripts/bridge_test.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# ============================================================================= +# Script 2: Bridge Interaction Test +# ============================================================================= +# This script calls bridge contracts and displays the MessageSent event details. +# Requires: 01_start_anvil_deploy.sh to have been run first. +# +# Usage: ./scripts/02_bridge_interaction.sh +# ============================================================================= + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +ENV_FILE="$SCRIPT_DIR/.bridge_contracts.env" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Step 2: Bridge Interaction Test ║${NC}" +echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}" +echo "" + +# Check if env file exists +if [ ! -f "$ENV_FILE" ]; then + echo -e "${RED}Error: Contract addresses not found.${NC}" + echo -e "Please run ${YELLOW}./scripts/start_anvil.sh${NC} first." + exit 1 +fi + +# Load environment +source "$ENV_FILE" + +# Verify Anvil is running +if ! lsof -i :8546 >/dev/null 2>&1; then + echo -e "${RED}Error: Anvil is not running on port 8546.${NC}" + echo -e "Please run ${YELLOW}./scripts/01_start_anvil_deploy.sh${NC} first." + exit 1 +fi + +echo -e "${YELLOW}Using contracts:${NC}" +echo -e " MockGToken: $GTOKEN_ADDRESS" +echo -e " GravityPortal: $PORTAL_ADDRESS" +echo -e " GBridgeSender: $SENDER_ADDRESS" +echo "" + +cd "$PROJECT_DIR" + +# ============================================================================= +# Run Bridge Interaction +# ============================================================================= +echo -e "${GREEN}[1/2] Executing bridge transaction...${NC}" +echo "" + +PRIVATE_KEY=$PRIVATE_KEY \ +GTOKEN_ADDRESS=$GTOKEN_ADDRESS \ +PORTAL_ADDRESS=$PORTAL_ADDRESS \ +SENDER_ADDRESS=$SENDER_ADDRESS \ +forge script script/BridgeInteraction.s.sol:BridgeInteraction \ + --rpc-url $RPC_URL \ + --broadcast + +echo "" +echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Event Details ║${NC}" +echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}" +echo "" + +# ============================================================================= +# Query and Display Events +# ============================================================================= +echo -e "${GREEN}[2/2] Querying MessageSent events from GravityPortal...${NC}" +echo "" + +EVENTS=$(cast logs --from-block 0 --address $PORTAL_ADDRESS --rpc-url $RPC_URL --json 2>/dev/null) + +if [ -n "$EVENTS" ] && [ "$EVENTS" != "[]" ]; then + # Find MessageSent event (signature: 0x3495d2da67b82080fd7085af57770617e0f3a846a8fe985877b0468cab7bfd2b) + MESSAGE_SENT_SIG="0x3495d2da67b82080fd7085af57770617e0f3a846a8fe985877b0468cab7bfd2b" + + echo "$EVENTS" | jq -r --arg sig "$MESSAGE_SENT_SIG" '.[] | select(.topics[0] == $sig) | + "┌─────────────────────────────────────────────────────────────────┐\n" + + "│ MessageSent Event │\n" + + "├─────────────────────────────────────────────────────────────────┤\n" + + "│ Block Number: \(.blockNumber | ltrimstr("0x") | . as $h | if . == "" then "0" else . end) │\n" + + "│ Transaction Hash: \(.transactionHash[0:42])... │\n" + + "├─────────────────────────────────────────────────────────────────┤\n" + + "│ Topics: │\n" + + "│ [0] Signature: \(.topics[0][0:42])...│\n" + + "│ [1] Nonce: \(.topics[1])│\n" + + "├─────────────────────────────────────────────────────────────────┤\n" + + "│ Data (ABI-encoded payload): │\n" + + "└─────────────────────────────────────────────────────────────────┘"' 2>/dev/null || echo "Error parsing events" + + echo "" + echo -e "${YELLOW}Raw Event Data:${NC}" + echo "$EVENTS" | jq -r --arg sig "$MESSAGE_SENT_SIG" '.[] | select(.topics[0] == $sig) | .data' 2>/dev/null + + echo "" + echo -e "${YELLOW}Payload Structure (PortalMessage format):${NC}" + echo " ┌─────────────────────────────────────────────────────────────┐" + echo " │ sender (20 bytes) || nonce (16 bytes) || message (variable) │" + echo " └─────────────────────────────────────────────────────────────┘" + echo "" + echo " - sender: Address that called GravityPortal.send()" + echo " - nonce: Auto-incrementing uint128 from portal" + echo " - message: abi.encode(amount, recipient) from GBridgeSender" +else + echo -e "${RED}No MessageSent events found.${NC}" +fi + +echo "" +echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Test Complete ║${NC}" +echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${GREEN}✓${NC} Bridge transaction executed successfully" +echo -e "${GREEN}✓${NC} MessageSent event emitted" +echo "" +echo -e "${YELLOW}To run another bridge test:${NC} ./scripts/bridge_test.sh" +echo -e "${YELLOW}To stop Anvil:${NC} ./scripts/stop_anvil.sh" diff --git a/scripts/start_anvil.sh b/scripts/start_anvil.sh new file mode 100755 index 0000000..e459279 --- /dev/null +++ b/scripts/start_anvil.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# ============================================================================= +# Script 1: Start Anvil and Deploy Bridge Contracts +# ============================================================================= +# This script starts a local Anvil testnet and deploys the bridge contracts. +# Contract addresses are saved to .bridge_contracts.env for other scripts. +# +# Usage: ./scripts/01_start_anvil_deploy.sh +# ============================================================================= + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +ENV_FILE="$SCRIPT_DIR/.bridge_contracts.env" + +# Anvil default private key (Account 0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) +PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +RPC_URL="http://localhost:8546" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Step 1: Start Anvil & Deploy Contracts ║${NC}" +echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}" +echo "" + +# Check if Anvil is already running on port 8546 +if lsof -i :8546 >/dev/null 2>&1; then + echo -e "${YELLOW}Warning: Port 8546 is already in use. Stopping existing process...${NC}" + "$SCRIPT_DIR/stop_anvil.sh" 2>/dev/null || true + sleep 1 +fi + +# ============================================================================= +# Step 1: Start Anvil +# ============================================================================= +echo -e "${GREEN}[1/2] Starting Anvil local testnet on port 8546 (block-time: 1s)...${NC}" +anvil --port 8546 --block-time 1 & +ANVIL_PID=$! +sleep 2 + +# Verify Anvil is running +if ! kill -0 $ANVIL_PID 2>/dev/null; then + echo -e "${RED}Error: Failed to start Anvil${NC}" + exit 1 +fi +echo -e " Anvil running at $RPC_URL (PID: $ANVIL_PID)" +echo "" + +cd "$PROJECT_DIR" + +# ============================================================================= +# Step 2: Deploy Contracts +# ============================================================================= +echo -e "${GREEN}[2/2] Deploying contracts...${NC}" +echo "" + +DEPLOY_OUTPUT=$(PRIVATE_KEY=$PRIVATE_KEY forge script script/DeployBridgeLocal.s.sol:DeployBridgeLocal \ + --rpc-url $RPC_URL \ + --broadcast 2>&1) + +# Parse deployed contract addresses +GTOKEN_ADDRESS=$(echo "$DEPLOY_OUTPUT" | grep "MockGToken deployed at:" | awk '{print $NF}') +PORTAL_ADDRESS=$(echo "$DEPLOY_OUTPUT" | grep "GravityPortal deployed at:" | awk '{print $NF}') +SENDER_ADDRESS=$(echo "$DEPLOY_OUTPUT" | grep "GBridgeSender deployed at:" | awk '{print $NF}') + +if [ -z "$GTOKEN_ADDRESS" ] || [ -z "$PORTAL_ADDRESS" ] || [ -z "$SENDER_ADDRESS" ]; then + echo -e "${RED}Error: Failed to parse contract addresses from deployment output${NC}" + echo "$DEPLOY_OUTPUT" + exit 1 +fi + +# Save to env file for other scripts +cat > "$ENV_FILE" << EOF +# Bridge Contract Addresses (auto-generated) +ANVIL_PID=$ANVIL_PID +RPC_URL=$RPC_URL +PRIVATE_KEY=$PRIVATE_KEY +GTOKEN_ADDRESS=$GTOKEN_ADDRESS +PORTAL_ADDRESS=$PORTAL_ADDRESS +SENDER_ADDRESS=$SENDER_ADDRESS +EOF + +echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Deployment Complete ║${NC}" +echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${YELLOW}Anvil:${NC}" +echo -e " PID: $ANVIL_PID" +echo -e " RPC: $RPC_URL" +echo "" +echo -e "${YELLOW}Contract Addresses:${NC}" +echo -e " MockGToken: $GTOKEN_ADDRESS" +echo -e " GravityPortal: $PORTAL_ADDRESS" +echo -e " GBridgeSender: $SENDER_ADDRESS" +echo "" +echo -e "${GREEN}Ready! Run ${YELLOW}./scripts/bridge_test.sh${GREEN} to test bridge.${NC}" diff --git a/scripts/stop_anvil.sh b/scripts/stop_anvil.sh new file mode 100755 index 0000000..477ef26 --- /dev/null +++ b/scripts/stop_anvil.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# ============================================================================= +# Script 3: Stop Anvil +# ============================================================================= +# This script stops the Anvil testnet running on port 8546. +# +# Usage: ./scripts/03_stop_anvil.sh +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/.bridge_contracts.env" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${YELLOW}Stopping Anvil on port 8546...${NC}" + +# Method 1: Kill by PID from env file +if [ -f "$ENV_FILE" ]; then + source "$ENV_FILE" + if [ -n "$ANVIL_PID" ] && kill -0 $ANVIL_PID 2>/dev/null; then + kill $ANVIL_PID 2>/dev/null + echo -e "${GREEN}Killed Anvil process (PID: $ANVIL_PID)${NC}" + fi + rm -f "$ENV_FILE" +fi + +# Method 2: Kill by port (fallback) +PIDS=$(lsof -ti :8546 2>/dev/null) +if [ -n "$PIDS" ]; then + echo "$PIDS" | xargs kill 2>/dev/null || true + sleep 1 + echo -e "${GREEN}Stopped processes on port 8546${NC}" +fi + +# Method 3: Kill anvil by name (fallback) +pkill -f "anvil.*--port 8546" 2>/dev/null || true + +# Verify +if lsof -i :8546 >/dev/null 2>&1; then + echo -e "${RED}Warning: Port 8546 still in use${NC}" +else + echo -e "${GREEN}Anvil stopped successfully${NC}" +fi diff --git a/src/Genesis.sol b/src/Genesis.sol index f76d402..16201cd 100644 --- a/src/Genesis.sol +++ b/src/Genesis.sol @@ -23,6 +23,8 @@ import { Reconfiguration } from "./blocker/Reconfiguration.sol"; import { Blocker } from "./blocker/Blocker.sol"; import { NativeOracle } from "./oracle/NativeOracle.sol"; import { JWKManager, IJWKManager } from "./oracle/jwk/JWKManager.sol"; +import { OracleTaskConfig } from "./oracle/OracleTaskConfig.sol"; +import { GBridgeReceiver } from "./oracle/evm/native_token_bridge/GBridgeReceiver.sol"; /// @title Genesis /// @author Gravity Team @@ -55,9 +57,23 @@ contract Genesis { uint64 votingDurationMicros; } + struct OracleTaskParams { + uint32 sourceType; + uint256 sourceId; + bytes32 taskName; + bytes config; + } + + struct BridgeConfig { + bool deploy; + address trustedBridge; + } + struct OracleInitParams { uint32[] sourceTypes; address[] callbacks; + OracleTaskParams[] tasks; + BridgeConfig bridgeConfig; } struct JWKInitParams { @@ -195,13 +211,45 @@ contract Genesis { OracleInitParams calldata oracleConfig, JWKInitParams calldata jwkConfig ) internal { - if (oracleConfig.sourceTypes.length > 0) { - NativeOracle(SystemAddresses.NATIVE_ORACLE).initialize(oracleConfig.sourceTypes, oracleConfig.callbacks); + // Collect sourceTypes and callbacks + uint256 length = oracleConfig.sourceTypes.length; + uint32[] memory sourceTypes; + address[] memory callbacks; + + if (oracleConfig.bridgeConfig.deploy) { + // Deploy GBridgeReceiver + GBridgeReceiver receiver = new GBridgeReceiver(oracleConfig.bridgeConfig.trustedBridge); + + // Construct new arrays with extra slot for GBridgeReceiver (sourceType=0) + sourceTypes = new uint32[](length + 1); + callbacks = new address[](length + 1); + + for (uint256 i = 0; i < length; i++) { + sourceTypes[i] = oracleConfig.sourceTypes[i]; + callbacks[i] = oracleConfig.callbacks[i]; + } + + sourceTypes[length] = 0; // Blockchain Events + callbacks[length] = address(receiver); + } else { + sourceTypes = oracleConfig.sourceTypes; + callbacks = oracleConfig.callbacks; + } + + if (sourceTypes.length > 0) { + NativeOracle(SystemAddresses.NATIVE_ORACLE).initialize(sourceTypes, callbacks); } if (jwkConfig.issuers.length > 0) { JWKManager(SystemAddresses.JWK_MANAGER).initialize(jwkConfig.issuers, jwkConfig.jwks); } + + // Set Tasks + for (uint256 i = 0; i < oracleConfig.tasks.length; i++) { + OracleTaskParams calldata task = oracleConfig.tasks[i]; + OracleTaskConfig(SystemAddresses.ORACLE_TASK_CONFIG) + .setTask(task.sourceType, task.sourceId, task.taskName, task.config); + } } function _createPoolsAndValidators( diff --git a/src/oracle/IOracleTaskConfig.sol b/src/oracle/IOracleTaskConfig.sol index fce66f5..17a99d9 100644 --- a/src/oracle/IOracleTaskConfig.sol +++ b/src/oracle/IOracleTaskConfig.sol @@ -120,4 +120,33 @@ interface IOracleTaskConfig { uint256 sourceId, uint256 index ) external view returns (bytes32 taskName); + + // ======================================================================== + // SOURCE ENUMERATION + // ======================================================================== + + /// @notice Full task information for batch queries + struct FullTaskInfo { + uint32 sourceType; + uint256 sourceId; + bytes32 taskName; + bytes config; + uint64 updatedAt; + } + + /// @notice Get all registered source types + /// @return sourceTypes Array of source types that have at least one task + function getSourceTypes() external view returns (uint32[] memory sourceTypes); + + /// @notice Get all registered source IDs for a given source type + /// @param sourceType The source type + /// @return sourceIds Array of source IDs that have at least one task + function getSourceIds( + uint32 sourceType + ) external view returns (uint256[] memory sourceIds); + + /// @notice Get all tasks across all sources + /// @dev Use with caution - may be expensive for large task sets + /// @return tasks Array of all task information + function getAllTasks() external view returns (FullTaskInfo[] memory tasks); } diff --git a/src/oracle/NativeOracle.sol b/src/oracle/NativeOracle.sol index 1a6aafe..bf52083 100644 --- a/src/oracle/NativeOracle.sol +++ b/src/oracle/NativeOracle.sol @@ -17,6 +17,7 @@ contract NativeOracle is INativeOracle { // STATE // ======================================================================== + // TODO: refactor? how to upgrade /// @notice Data records: sourceType -> sourceId -> nonce -> DataRecord mapping(uint32 => mapping(uint256 => mapping(uint128 => DataRecord))) private _records; diff --git a/src/oracle/OracleTaskConfig.sol b/src/oracle/OracleTaskConfig.sol index 0167549..97f4d08 100644 --- a/src/oracle/OracleTaskConfig.sol +++ b/src/oracle/OracleTaskConfig.sol @@ -15,6 +15,7 @@ import { EnumerableSet } from "@openzeppelin/utils/structs/EnumerableSet.sol"; /// Only GOVERNANCE can create, update, or remove tasks. contract OracleTaskConfig is IOracleTaskConfig { using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for EnumerableSet.UintSet; // ======================================================================== // STATE @@ -26,6 +27,12 @@ contract OracleTaskConfig is IOracleTaskConfig { /// @notice Task data: sourceType -> sourceId -> taskName -> OracleTask mapping(uint32 => mapping(uint256 => mapping(bytes32 => OracleTask))) private _tasks; + /// @notice Registered source types (for enumeration) + EnumerableSet.UintSet private _registeredSourceTypes; + + /// @notice Registered source IDs per source type: sourceType -> set of sourceIds + mapping(uint32 => EnumerableSet.UintSet) private _registeredSourceIds; + // ======================================================================== // TASK MANAGEMENT (Governance Only) // ======================================================================== @@ -37,12 +44,16 @@ contract OracleTaskConfig is IOracleTaskConfig { bytes32 taskName, bytes calldata config ) external { - requireAllowed(SystemAddresses.GOVERNANCE); + requireAllowed(SystemAddresses.GENESIS, SystemAddresses.GOVERNANCE); if (config.length == 0) { revert Errors.EmptyConfig(); } + // Register source type and source ID for enumeration (no-op if already exists) + _registeredSourceTypes.add(sourceType); + _registeredSourceIds[sourceType].add(sourceId); + // Add task name to the set (no-op if already exists) _taskNames[sourceType][sourceId].add(taskName); @@ -66,6 +77,15 @@ contract OracleTaskConfig is IOracleTaskConfig { // Delete task data delete _tasks[sourceType][sourceId][taskName]; + // Cleanup source registration if no more tasks for this source + if (_taskNames[sourceType][sourceId].length() == 0) { + _registeredSourceIds[sourceType].remove(sourceId); + // If no more sourceIds for this sourceType, remove the sourceType + if (_registeredSourceIds[sourceType].length() == 0) { + _registeredSourceTypes.remove(sourceType); + } + } + emit TaskRemoved(sourceType, sourceId, taskName); } @@ -115,4 +135,65 @@ contract OracleTaskConfig is IOracleTaskConfig { ) external view returns (bytes32 taskName) { return _taskNames[sourceType][sourceId].at(index); } + + // ======================================================================== + // SOURCE ENUMERATION + // ======================================================================== + + /// @inheritdoc IOracleTaskConfig + function getSourceTypes() external view returns (uint32[] memory sourceTypes) { + uint256 length = _registeredSourceTypes.length(); + sourceTypes = new uint32[](length); + for (uint256 i = 0; i < length; i++) { + sourceTypes[i] = uint32(_registeredSourceTypes.at(i)); + } + } + + /// @inheritdoc IOracleTaskConfig + function getSourceIds( + uint32 sourceType + ) external view returns (uint256[] memory sourceIds) { + return _registeredSourceIds[sourceType].values(); + } + + /// @inheritdoc IOracleTaskConfig + function getAllTasks() external view returns (FullTaskInfo[] memory tasks) { + // First, count total tasks + uint256 totalTasks = 0; + uint256 sourceTypesLength = _registeredSourceTypes.length(); + + for (uint256 i = 0; i < sourceTypesLength; i++) { + uint32 sourceType = uint32(_registeredSourceTypes.at(i)); + uint256 sourceIdsLength = _registeredSourceIds[sourceType].length(); + for (uint256 j = 0; j < sourceIdsLength; j++) { + uint256 sourceId = _registeredSourceIds[sourceType].at(j); + totalTasks += _taskNames[sourceType][sourceId].length(); + } + } + + // Allocate result array + tasks = new FullTaskInfo[](totalTasks); + uint256 index = 0; + + // Populate result array + for (uint256 i = 0; i < sourceTypesLength; i++) { + uint32 sourceType = uint32(_registeredSourceTypes.at(i)); + uint256 sourceIdsLength = _registeredSourceIds[sourceType].length(); + for (uint256 j = 0; j < sourceIdsLength; j++) { + uint256 sourceId = _registeredSourceIds[sourceType].at(j); + bytes32[] memory taskNamesList = _taskNames[sourceType][sourceId].values(); + for (uint256 k = 0; k < taskNamesList.length; k++) { + OracleTask storage task = _tasks[sourceType][sourceId][taskNamesList[k]]; + tasks[index] = FullTaskInfo({ + sourceType: sourceType, + sourceId: sourceId, + taskName: taskNamesList[k], + config: task.config, + updatedAt: task.updatedAt + }); + index++; + } + } + } + } } diff --git a/src/oracle/evm/GravityPortal.sol b/src/oracle/evm/GravityPortal.sol index 9f06263..e296eda 100644 --- a/src/oracle/evm/GravityPortal.sol +++ b/src/oracle/evm/GravityPortal.sol @@ -61,7 +61,7 @@ contract GravityPortal is IGravityPortal, Ownable2Step { bytes calldata message ) external payable returns (uint128 messageNonce) { // Assign nonce and increment - messageNonce = nonce++; + messageNonce = ++nonce; // Encode payload: sender (20B) || nonce (16B) || message bytes memory payload = PortalMessage.encodeCalldata(msg.sender, messageNonce, message); diff --git a/test/unit/GenesisTest.t.sol b/test/unit/GenesisTest.t.sol index f7371a7..5bf504f 100644 --- a/test/unit/GenesisTest.t.sol +++ b/test/unit/GenesisTest.t.sol @@ -108,7 +108,9 @@ contract GenesisTest is Test { sourceTypes[0] = 1; address[] memory callbacks = new address[](1); callbacks[0] = SystemAddresses.JWK_MANAGER; - params.oracleConfig = Genesis.OracleInitParams(sourceTypes, callbacks); + Genesis.OracleTaskParams[] memory tasks = new Genesis.OracleTaskParams[](0); + Genesis.BridgeConfig memory bridgeConfig = Genesis.BridgeConfig(false, address(0)); + params.oracleConfig = Genesis.OracleInitParams(sourceTypes, callbacks, tasks, bridgeConfig); // JWK Config bytes[] memory issuers = new bytes[](1); diff --git a/test/unit/oracle/GBridgeSender.t.sol b/test/unit/oracle/GBridgeSender.t.sol index 92c8671..a9ce4c9 100644 --- a/test/unit/oracle/GBridgeSender.t.sol +++ b/test/unit/oracle/GBridgeSender.t.sol @@ -210,7 +210,7 @@ contract GBridgeSenderTest is Test { vm.stopPrank(); // Verify - assertEq(nonce, 0); + assertEq(nonce, 1); assertEq(gToken.balanceOf(alice), INITIAL_BALANCE - amount); assertEq(gToken.balanceOf(address(bridge)), amount); assertEq(portal.nonce(), 1); @@ -224,7 +224,7 @@ contract GBridgeSenderTest is Test { gToken.approve(address(bridge), amount); vm.expectEmit(true, true, true, true); - emit IGBridgeSender.TokensLocked(alice, bob, amount, 0); + emit IGBridgeSender.TokensLocked(alice, bob, amount, 1); bridge.bridgeToGravity{ value: fee }(amount, bob); vm.stopPrank(); } @@ -291,9 +291,9 @@ contract GBridgeSenderTest is Test { vm.startPrank(alice); gToken.approve(address(bridge), amount * 3); - assertEq(bridge.bridgeToGravity{ value: fee }(amount, bob), 0); assertEq(bridge.bridgeToGravity{ value: fee }(amount, bob), 1); assertEq(bridge.bridgeToGravity{ value: fee }(amount, bob), 2); + assertEq(bridge.bridgeToGravity{ value: fee }(amount, bob), 3); vm.stopPrank(); assertEq(gToken.balanceOf(address(bridge)), amount * 3); @@ -322,7 +322,7 @@ contract GBridgeSenderTest is Test { uint128 nonce = bridge.bridgeToGravityWithPermit{ value: fee }(amount, bob, deadline, v, r, s); // Verify - assertEq(nonce, 0); + assertEq(nonce, 1); assertEq(gToken.balanceOf(alice), INITIAL_BALANCE - amount); assertEq(gToken.balanceOf(address(bridge)), amount); } @@ -342,7 +342,7 @@ contract GBridgeSenderTest is Test { vm.prank(alice); vm.expectEmit(true, true, true, true); - emit IGBridgeSender.TokensLocked(alice, bob, amount, 0); + emit IGBridgeSender.TokensLocked(alice, bob, amount, 1); bridge.bridgeToGravityWithPermit{ value: fee }(amount, bob, deadline, v, r, s); } @@ -410,7 +410,7 @@ contract GBridgeSenderTest is Test { uint128 nonce = bridge.bridgeToGravity{ value: fee }(amount, recipient); vm.stopPrank(); - assertEq(nonce, 0); + assertEq(nonce, 1); assertEq(gToken.balanceOf(alice), INITIAL_BALANCE - amount); assertEq(gToken.balanceOf(address(bridge)), amount); } diff --git a/test/unit/oracle/GravityPortal.t.sol b/test/unit/oracle/GravityPortal.t.sol index 353e2bb..5b9bc91 100644 --- a/test/unit/oracle/GravityPortal.t.sol +++ b/test/unit/oracle/GravityPortal.t.sol @@ -67,7 +67,7 @@ contract GravityPortalTest is Test { vm.prank(alice); uint128 nonce = portal.send{ value: fee }(message); - assertEq(nonce, 0); + assertEq(nonce, 1); assertEq(portal.nonce(), 1); } @@ -76,11 +76,11 @@ contract GravityPortalTest is Test { uint256 fee = portal.calculateFee(message.length); // Build expected encoded payload - bytes memory expectedPayload = PortalMessage.encode(alice, 0, message); + bytes memory expectedPayload = PortalMessage.encode(alice, 1, message); vm.prank(alice); vm.expectEmit(true, true, true, true); - emit IGravityPortal.MessageSent(0, expectedPayload); + emit IGravityPortal.MessageSent(1, expectedPayload); portal.send{ value: fee }(message); } @@ -89,9 +89,9 @@ contract GravityPortalTest is Test { uint256 fee = portal.calculateFee(message.length); vm.startPrank(alice); - assertEq(portal.send{ value: fee }(message), 0); assertEq(portal.send{ value: fee }(message), 1); assertEq(portal.send{ value: fee }(message), 2); + assertEq(portal.send{ value: fee }(message), 3); vm.stopPrank(); assertEq(portal.nonce(), 3); @@ -365,7 +365,7 @@ contract GravityPortalTest is Test { vm.prank(alice); uint128 nonce = portal.send{ value: requiredFee + extraFee }(message); - assertEq(nonce, 0); + assertEq(nonce, 1); assertGe(address(portal).balance, requiredFee); } @@ -403,7 +403,7 @@ contract GravityPortalTest is Test { // Verify event topics // topic[0] is the event signature // topic[1] is indexed nonce - assertEq(entries[0].topics[1], bytes32(uint256(0)), "First message nonce should be 0"); + assertEq(entries[0].topics[1], bytes32(uint256(1)), "First message nonce should be 1"); // Verify event data (encoded payload) bytes memory emittedPayload = abi.decode(entries[0].data, (bytes)); @@ -413,7 +413,7 @@ contract GravityPortalTest is Test { PortalMessage.decode(emittedPayload); assertEq(decodedSender, sender, "Sender should match"); - assertEq(decodedNonce, 0, "First message nonce should be 0"); + assertEq(decodedNonce, 1, "First message nonce should be 1"); assertEq(keccak256(decodedMessage), keccak256(message), "Message should match"); } } diff --git a/test/unit/oracle/OracleTaskConfig.t.sol b/test/unit/oracle/OracleTaskConfig.t.sol index 5d6b61d..7429135 100644 --- a/test/unit/oracle/OracleTaskConfig.t.sol +++ b/test/unit/oracle/OracleTaskConfig.t.sol @@ -420,4 +420,167 @@ contract OracleTaskConfigTest is Test { assertFalse(taskConfig.hasTask(sourceType1, sourceId1, TASK_EVENTS)); assertTrue(taskConfig.hasTask(sourceType2, sourceId2, TASK_EVENTS)); } + + // ======================================================================== + // SOURCE ENUMERATION TESTS + // ======================================================================== + + function test_GetSourceTypes_Empty() public view { + uint32[] memory sourceTypes = taskConfig.getSourceTypes(); + assertEq(sourceTypes.length, 0); + } + + function test_GetSourceTypes() public { + bytes memory config = abi.encode("config"); + + vm.startPrank(governance); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_EVENTS, config); + taskConfig.setTask(SOURCE_TYPE_JWK, 0, TASK_EVENTS, config); + vm.stopPrank(); + + uint32[] memory sourceTypes = taskConfig.getSourceTypes(); + assertEq(sourceTypes.length, 2); + + // Check both source types are present + bool hasBlockchain = false; + bool hasJwk = false; + for (uint256 i = 0; i < sourceTypes.length; i++) { + if (sourceTypes[i] == SOURCE_TYPE_BLOCKCHAIN) hasBlockchain = true; + if (sourceTypes[i] == SOURCE_TYPE_JWK) hasJwk = true; + } + assertTrue(hasBlockchain); + assertTrue(hasJwk); + } + + function test_GetSourceIds() public { + bytes memory config = abi.encode("config"); + + vm.startPrank(governance); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_EVENTS, config); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ARBITRUM_SOURCE_ID, TASK_EVENTS, config); + vm.stopPrank(); + + uint256[] memory sourceIds = taskConfig.getSourceIds(SOURCE_TYPE_BLOCKCHAIN); + assertEq(sourceIds.length, 2); + + // Check both source IDs are present + bool hasEthereum = false; + bool hasArbitrum = false; + for (uint256 i = 0; i < sourceIds.length; i++) { + if (sourceIds[i] == ETHEREUM_SOURCE_ID) hasEthereum = true; + if (sourceIds[i] == ARBITRUM_SOURCE_ID) hasArbitrum = true; + } + assertTrue(hasEthereum); + assertTrue(hasArbitrum); + } + + function test_GetSourceIds_Empty() public view { + uint256[] memory sourceIds = taskConfig.getSourceIds(SOURCE_TYPE_BLOCKCHAIN); + assertEq(sourceIds.length, 0); + } + + function test_GetAllTasks() public { + bytes memory ethConfig = abi.encode("ethereum config"); + bytes memory arbConfig = abi.encode("arbitrum config"); + bytes memory jwkConfig = abi.encode("jwk config"); + + vm.startPrank(governance); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_EVENTS, ethConfig); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ARBITRUM_SOURCE_ID, TASK_STATE_ROOTS, arbConfig); + taskConfig.setTask(SOURCE_TYPE_JWK, 0, TASK_EVENTS, jwkConfig); + vm.stopPrank(); + + IOracleTaskConfig.FullTaskInfo[] memory tasks = taskConfig.getAllTasks(); + assertEq(tasks.length, 3); + + // Verify all tasks are present + bool foundEth = false; + bool foundArb = false; + bool foundJwk = false; + for (uint256 i = 0; i < tasks.length; i++) { + if (tasks[i].sourceType == SOURCE_TYPE_BLOCKCHAIN && tasks[i].sourceId == ETHEREUM_SOURCE_ID) { + foundEth = true; + assertEq(tasks[i].taskName, TASK_EVENTS); + assertEq(tasks[i].config, ethConfig); + } + if (tasks[i].sourceType == SOURCE_TYPE_BLOCKCHAIN && tasks[i].sourceId == ARBITRUM_SOURCE_ID) { + foundArb = true; + assertEq(tasks[i].taskName, TASK_STATE_ROOTS); + assertEq(tasks[i].config, arbConfig); + } + if (tasks[i].sourceType == SOURCE_TYPE_JWK && tasks[i].sourceId == 0) { + foundJwk = true; + assertEq(tasks[i].taskName, TASK_EVENTS); + assertEq(tasks[i].config, jwkConfig); + } + } + assertTrue(foundEth); + assertTrue(foundArb); + assertTrue(foundJwk); + } + + function test_GetAllTasks_Empty() public view { + IOracleTaskConfig.FullTaskInfo[] memory tasks = taskConfig.getAllTasks(); + assertEq(tasks.length, 0); + } + + function test_SourceEnumeration_CleanupOnRemove() public { + bytes memory config = abi.encode("config"); + + // Add two tasks to same source + vm.startPrank(governance); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_EVENTS, config); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_STATE_ROOTS, config); + vm.stopPrank(); + + // Verify source is registered + uint32[] memory sourceTypes = taskConfig.getSourceTypes(); + assertEq(sourceTypes.length, 1); + uint256[] memory sourceIds = taskConfig.getSourceIds(SOURCE_TYPE_BLOCKCHAIN); + assertEq(sourceIds.length, 1); + + // Remove first task - source should still be registered + vm.prank(governance); + taskConfig.removeTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_EVENTS); + + sourceTypes = taskConfig.getSourceTypes(); + assertEq(sourceTypes.length, 1); + sourceIds = taskConfig.getSourceIds(SOURCE_TYPE_BLOCKCHAIN); + assertEq(sourceIds.length, 1); + + // Remove second task - source should be cleaned up + vm.prank(governance); + taskConfig.removeTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_STATE_ROOTS); + + sourceTypes = taskConfig.getSourceTypes(); + assertEq(sourceTypes.length, 0); + sourceIds = taskConfig.getSourceIds(SOURCE_TYPE_BLOCKCHAIN); + assertEq(sourceIds.length, 0); + } + + function test_SourceEnumeration_MultipleSourceTypes() public { + bytes memory config = abi.encode("config"); + + vm.startPrank(governance); + taskConfig.setTask(SOURCE_TYPE_BLOCKCHAIN, ETHEREUM_SOURCE_ID, TASK_EVENTS, config); + taskConfig.setTask(SOURCE_TYPE_JWK, 0, TASK_EVENTS, config); + taskConfig.setTask(SOURCE_TYPE_DNS, 1, TASK_EVENTS, config); + vm.stopPrank(); + + uint32[] memory sourceTypes = taskConfig.getSourceTypes(); + assertEq(sourceTypes.length, 3); + + // Remove JWK task - only JWK sourceType should be removed + vm.prank(governance); + taskConfig.removeTask(SOURCE_TYPE_JWK, 0, TASK_EVENTS); + + sourceTypes = taskConfig.getSourceTypes(); + assertEq(sourceTypes.length, 2); + + // Verify JWK is no longer in source types + for (uint256 i = 0; i < sourceTypes.length; i++) { + assertTrue(sourceTypes[i] != SOURCE_TYPE_JWK); + } + } } + diff --git a/test/utils/MockGToken.sol b/test/utils/MockGToken.sol new file mode 100644 index 0000000..c596715 --- /dev/null +++ b/test/utils/MockGToken.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol"; +import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; + +/// @title MockGToken +/// @notice Mock G Token for testing bridge functionality on local testnet +/// @dev ERC20 with public mint function and ERC20Permit for gasless approvals +contract MockGToken is ERC20, ERC20Permit { + constructor() ERC20("Mock G Token", "G") ERC20Permit("Mock G Token") { } + + /// @notice Mint tokens to any address (for testing only) + /// @param to Recipient address + /// @param amount Amount to mint + function mint( + address to, + uint256 amount + ) external { + _mint(to, amount); + } +}