diff --git a/Cargo.lock b/Cargo.lock index c086426..0c61bab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5666,6 +5666,7 @@ dependencies = [ name = "summit-rpc" version = "0.0.0" dependencies = [ + "alloy-primitives", "anyhow", "axum 0.8.4", "commonware-codec", diff --git a/node/src/bin/stake_and_checkpoint.rs b/node/src/bin/stake_and_checkpoint.rs index e337a7d..60112ee 100644 --- a/node/src/bin/stake_and_checkpoint.rs +++ b/node/src/bin/stake_and_checkpoint.rs @@ -33,10 +33,12 @@ use std::{ thread::JoinHandle, }; use summit::args::{RunFlags, run_node_local}; -use summit::engine::{PROTOCOL_VERSION, VALIDATOR_MINIMUM_STAKE}; +use summit::engine::VALIDATOR_MINIMUM_STAKE; +use summit_types::PROTOCOL_VERSION; use summit_types::checkpoint::Checkpoint; use summit_types::consensus_state::ConsensusState; use summit_types::execution_request::DepositRequest; +use summit_types::execution_request::compute_deposit_data_root; use summit_types::reth::Reth; use tokio::sync::mpsc; use tracing::Level; @@ -764,96 +766,6 @@ where } } -fn compute_deposit_data_root( - node_pubkey: &[u8; 32], - consensus_pubkey: &[u8; 48], - withdrawal_credentials: &[u8; 32], - amount: U256, - node_signature: &[u8; 64], - consensus_signature: &[u8; 96], -) -> [u8; 32] { - /* - Solidity computation: - bytes32 consensus_pubkey_hash = sha256(abi.encodePacked(consensus_pubkey, bytes16(0))); - bytes32 pubkey_root = sha256(abi.encodePacked(node_pubkey, consensus_pubkey_hash)); - bytes32 node_signature_hash = sha256(node_signature); - bytes32 consensus_signature_hash = sha256(abi.encodePacked( - sha256(abi.encodePacked(consensus_signature[:64])), - sha256(abi.encodePacked(consensus_signature[64:], bytes32(0))) - )); - bytes32 signature_root = sha256(abi.encodePacked(node_signature_hash, consensus_signature_hash)); - bytes32 node = sha256(abi.encodePacked( - sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)), - sha256(abi.encodePacked(amount, bytes24(0), signature_root)) - )); - */ - - // 1. consensus_pubkey_hash = sha256(consensus_pubkey || bytes16(0)) - let mut hasher = Sha256::new(); - hasher.update(consensus_pubkey); - hasher.update(&[0u8; 16]); // bytes16(0) - let consensus_pubkey_hash = hasher.finalize(); - - // 2. pubkey_root = sha256(node_pubkey || consensus_pubkey_hash) - let mut hasher = Sha256::new(); - hasher.update(node_pubkey); - hasher.update(&consensus_pubkey_hash); - let pubkey_root = hasher.finalize(); - - // 3. node_signature_hash = sha256(node_signature) - let mut hasher = Sha256::new(); - hasher.update(node_signature); - let node_signature_hash = hasher.finalize(); - - // 4. consensus_signature_hash = sha256(sha256(consensus_signature[0:64]) || sha256(consensus_signature[64:96] || bytes32(0))) - let mut hasher = Sha256::new(); - hasher.update(&consensus_signature[0..64]); - let consensus_sig_part1 = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&consensus_signature[64..96]); - hasher.update(&[0u8; 32]); // bytes32(0) - let consensus_sig_part2 = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&consensus_sig_part1); - hasher.update(&consensus_sig_part2); - let consensus_signature_hash = hasher.finalize(); - - // 5. signature_root = sha256(node_signature_hash || consensus_signature_hash) - let mut hasher = Sha256::new(); - hasher.update(&node_signature_hash); - hasher.update(&consensus_signature_hash); - let signature_root = hasher.finalize(); - - // 3. Convert amount to 8-byte little-endian (gwei) - let amount_gwei = amount / U256::from(10).pow(U256::from(9)); // Convert wei to gwei - let amount_u64 = amount_gwei.to::(); // Convert to u64 (should fit for reasonable amounts) - let amount_bytes = amount_u64.to_le_bytes(); // 8 bytes little-endian - - // 4. node = sha256(sha256(pubkey_root || withdrawal_credentials) || sha256(amount || bytes24(0) || signature_root)) - let mut hasher = Sha256::new(); - hasher.update(&pubkey_root); - hasher.update(withdrawal_credentials); - let left_node = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&amount_bytes); - hasher.update(&[0u8; 24]); // bytes24(0) - hasher.update(&signature_root); - let right_node = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&left_node); - hasher.update(&right_node); - let deposit_data_root = hasher.finalize(); - - let digest_bytes: &[u8] = deposit_data_root.as_ref(); - digest_bytes - .try_into() - .expect("SHA-256 digest is always 32 bytes") -} - fn get_node_flags(node: usize) -> RunFlags { let path = format!("testnet/node{node}/"); diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 032ffef..4dcb17a 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -9,12 +9,13 @@ path = "src/lib.rs" [dependencies] summit-finalizer.workspace = true +summit-types = { workspace = true } anyhow = { workspace = true } +alloy-primitives.workspace = true tokio = { workspace = true, features = ["net"] } tokio-util.workspace = true futures.workspace = true axum = { version = "0.8.4"} -summit-types = { workspace = true } commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } commonware-utils = { workspace = true } diff --git a/rpc/src/routes.rs b/rpc/src/routes.rs index 6fafe5e..8c12738 100644 --- a/rpc/src/routes.rs +++ b/rpc/src/routes.rs @@ -1,18 +1,23 @@ use std::sync::Arc; +use alloy_primitives::{Address, U256, hex::FromHex as _}; use axum::{ - Router, - extract::{Query, State}, + Json, Router, + extract::{Path, Query, State}, routing::{get, post}, }; -use commonware_codec::DecodeExt as _; +use commonware_codec::{DecodeExt as _, Encode as _}; use commonware_consensus::Block as ConsensusBlock; use commonware_consensus::simplex::signing_scheme::Scheme; -use commonware_cryptography::Committable; +use commonware_cryptography::{Committable, Hasher as _, Sha256, Signer as _}; use commonware_utils::{from_hex_formatted, hex}; use serde::{Deserialize, Serialize}; use ssz::Encode; -use summit_types::{KeyPaths, PublicKey, utils::get_expanded_path}; +use summit_types::{ + KeyPaths, PROTOCOL_VERSION, PublicKey, + execution_request::{DepositRequest, compute_deposit_data_root}, + utils::get_expanded_path, +}; use crate::{GenesisRpcState, PathSender, RpcState}; @@ -27,6 +32,16 @@ struct ValidatorBalanceQuery { public_key: String, } +#[derive(Serialize, Deserialize)] +struct DepositTransactionResponse { + node_pubkey: [u8; 32], + consensus_pubkey: Vec, // 48 bytes + withdrawal_credentials: [u8; 32], + node_signature: Vec, // 48 bytes + consensus_signature: Vec, // 96 bytes + deposit_data_root: [u8; 32], +} + pub(crate) struct RpcRoutes; impl RpcRoutes { @@ -48,6 +63,10 @@ impl RpcRoutes { "/get_validator_balance", get(Self::handle_get_validator_balance::), ) + .route( + "/get_deposit_signature/:amount/:address", + get(Self::handle_get_deposit_signature), + ) .with_state(state) } @@ -79,6 +98,81 @@ impl RpcRoutes { serde_json::to_string(&response).map_err(|e| format!("Failed to serialize response: {}", e)) } + async fn handle_get_deposit_signature( + State(state): State>>, + Path((amount, address)): Path<(u64, String)>, + ) -> Result, String> { + // Withdrawal credentials (32 bytes) - 0x01 prefix for execution address withdrawal + // Format: 0x01 || 0x00...00 (11 bytes) || execution_address (20 bytes) + let mut withdrawal_credentials = [0u8; 32]; + withdrawal_credentials[0] = 0x01; // ETH1 withdrawal prefix + // Bytes 1-11 remain zero + // Set the last 20 bytes to the withdrawal address (using the same address as the sender) + let withdrawal_address = Address::from_hex(address).unwrap(); + withdrawal_credentials[12..32].copy_from_slice(withdrawal_address.as_slice()); + + //let amount = VALIDATOR_MINIMUM_STAKE; + + let key_paths = KeyPaths::new(state.key_store_path.clone()); + + let consenus_priv_key = key_paths.consensus_private_key()?; + let consensus_pub = consenus_priv_key.public_key(); + + let node_priv_key = key_paths.node_private_key()?; + let node_pub = node_priv_key.public_key(); + + let req = DepositRequest { + node_pubkey: node_pub.clone(), + consensus_pubkey: consensus_pub.clone(), + withdrawal_credentials, + amount, + node_signature: [0; 64], + consensus_signature: [0; 96], + index: 0, // not included in the signature + }; + + let protocol_version_digest = Sha256::hash(&PROTOCOL_VERSION.to_le_bytes()); + let message = req.as_message(protocol_version_digest); + + // Sign with node (ed25519) key + let node_signature = node_priv_key.sign(None, &message); + let node_signature_bytes: [u8; 64] = node_signature + .as_ref() + .try_into() + .expect("ed25519 sig is alway 64 bytes"); + + // Sign with consensus (BLS) key + let consensus_signature = consenus_priv_key.sign(None, &message); + let consensus_signature_slice: &[u8] = consensus_signature.as_ref(); + let consensus_signature_bytes: [u8; 96] = consensus_signature_slice + .try_into() + .expect("bls sig is alway 96 bytes"); + + let node_pubkey_bytes: [u8; 32] = node_pub.to_vec().try_into().expect("Cannot fail"); + let consensus_pubkey_bytes: [u8; 48] = + consensus_pub.encode().as_ref()[..48].try_into().unwrap(); + + let deposit_amount = U256::from(amount) * U256::from(1_000_000_000u64); // gwei to wei + + let deposit_root = compute_deposit_data_root( + &node_pubkey_bytes, + &consensus_pubkey_bytes, + &withdrawal_credentials, + deposit_amount, + &node_signature_bytes, + &consensus_signature_bytes, + ); + + Ok(Json(DepositTransactionResponse { + node_pubkey: node_pubkey_bytes, + consensus_pubkey: consensus_pubkey_bytes.to_vec(), + withdrawal_credentials, + node_signature: node_signature_bytes.to_vec(), + consensus_signature: consensus_signature_bytes.to_vec(), + deposit_data_root: deposit_root, + })) + } + async fn handle_get_pub_keys_genesis( State(state): State>, ) -> Result { diff --git a/types/src/execution_request.rs b/types/src/execution_request.rs index fa33f0f..920e01c 100644 --- a/types/src/execution_request.rs +++ b/types/src/execution_request.rs @@ -1,5 +1,5 @@ use crate::{Digest, PublicKey}; -use alloy_primitives::Address; +use alloy_primitives::{Address, U256}; use bytes::{Buf, BufMut}; use commonware_codec::{DecodeExt, Encode, Error, FixedSize, Read, Write}; use commonware_cryptography::{Hasher, Sha256, bls12381}; @@ -340,6 +340,96 @@ impl Read for DepositRequest { } } +pub fn compute_deposit_data_root( + node_pubkey: &[u8; 32], + consensus_pubkey: &[u8; 48], + withdrawal_credentials: &[u8; 32], + amount: U256, + node_signature: &[u8; 64], + consensus_signature: &[u8; 96], +) -> [u8; 32] { + /* + Solidity computation: + bytes32 consensus_pubkey_hash = sha256(abi.encodePacked(consensus_pubkey, bytes16(0))); + bytes32 pubkey_root = sha256(abi.encodePacked(node_pubkey, consensus_pubkey_hash)); + bytes32 node_signature_hash = sha256(node_signature); + bytes32 consensus_signature_hash = sha256(abi.encodePacked( + sha256(abi.encodePacked(consensus_signature[:64])), + sha256(abi.encodePacked(consensus_signature[64:], bytes32(0))) + )); + bytes32 signature_root = sha256(abi.encodePacked(node_signature_hash, consensus_signature_hash)); + bytes32 node = sha256(abi.encodePacked( + sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)), + sha256(abi.encodePacked(amount, bytes24(0), signature_root)) + )); + */ + + // 1. consensus_pubkey_hash = sha256(consensus_pubkey || bytes16(0)) + let mut hasher = Sha256::new(); + hasher.update(consensus_pubkey); + hasher.update(&[0u8; 16]); // bytes16(0) + let consensus_pubkey_hash = hasher.finalize(); + + // 2. pubkey_root = sha256(node_pubkey || consensus_pubkey_hash) + let mut hasher = Sha256::new(); + hasher.update(node_pubkey); + hasher.update(&consensus_pubkey_hash); + let pubkey_root = hasher.finalize(); + + // 3. node_signature_hash = sha256(node_signature) + let mut hasher = Sha256::new(); + hasher.update(node_signature); + let node_signature_hash = hasher.finalize(); + + // 4. consensus_signature_hash = sha256(sha256(consensus_signature[0:64]) || sha256(consensus_signature[64:96] || bytes32(0))) + let mut hasher = Sha256::new(); + hasher.update(&consensus_signature[0..64]); + let consensus_sig_part1 = hasher.finalize(); + + let mut hasher = Sha256::new(); + hasher.update(&consensus_signature[64..96]); + hasher.update(&[0u8; 32]); // bytes32(0) + let consensus_sig_part2 = hasher.finalize(); + + let mut hasher = Sha256::new(); + hasher.update(&consensus_sig_part1); + hasher.update(&consensus_sig_part2); + let consensus_signature_hash = hasher.finalize(); + + // 5. signature_root = sha256(node_signature_hash || consensus_signature_hash) + let mut hasher = Sha256::new(); + hasher.update(&node_signature_hash); + hasher.update(&consensus_signature_hash); + let signature_root = hasher.finalize(); + + // 3. Convert amount to 8-byte little-endian (gwei) + let amount_gwei = amount / U256::from(10).pow(U256::from(9)); // Convert wei to gwei + let amount_u64 = amount_gwei.to::(); // Convert to u64 (should fit for reasonable amounts) + let amount_bytes = amount_u64.to_le_bytes(); // 8 bytes little-endian + + // 4. node = sha256(sha256(pubkey_root || withdrawal_credentials) || sha256(amount || bytes24(0) || signature_root)) + let mut hasher = Sha256::new(); + hasher.update(&pubkey_root); + hasher.update(withdrawal_credentials); + let left_node = hasher.finalize(); + + let mut hasher = Sha256::new(); + hasher.update(&amount_bytes); + hasher.update(&[0u8; 24]); // bytes24(0) + hasher.update(&signature_root); + let right_node = hasher.finalize(); + + let mut hasher = Sha256::new(); + hasher.update(&left_node); + hasher.update(&right_node); + let deposit_data_root = hasher.finalize(); + + let digest_bytes: &[u8] = deposit_data_root.as_ref(); + digest_bytes + .try_into() + .expect("SHA-256 digest is always 32 bytes") +} + #[cfg(test)] mod tests { use super::*; diff --git a/types/src/lib.rs b/types/src/lib.rs index 79644be..7797e6f 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -29,6 +29,8 @@ use commonware_consensus::simplex::types::Activity as CActivity; pub type Digest = commonware_cryptography::sha256::Digest; pub type Activity = CActivity; +pub const PROTOCOL_VERSION: u32 = 1; + /// Auxiliary data needed for block construction #[derive(Debug, Clone)] pub struct BlockAuxData {