Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions orchestrator/gravity/src/ethereum/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use ethers::{
prelude::*,
types::transaction::{eip2718::TypedTransaction, eip712::Eip712},
};
use ethers_gcp_kms_signer::GcpKmsSigner;
use ethers_gcp_kms_signer::{CKMSError, GcpKeyRingRef, GcpKmsProvider, GcpKmsSigner};
use std::{cmp::Ordering, sync::Arc};

pub type EthSignerMiddleware = SignerMiddleware<Provider<Http>, SignerType>;
Expand All @@ -16,6 +16,39 @@ pub enum SignerType {
}

impl SignerType {
/// Construct a signer backed by a Google Cloud KMS asymmetric signing key.
///
/// The private key never leaves KMS: every signature is produced by an
/// AsymmetricSign API call, so no key material is present in argv, in
/// process memory, or on disk. This is the difference that matters versus
/// `SignerType::Local`, whose key is passed in as raw bytes and is
/// therefore readable from `/proc/<pid>/cmdline` by any local user when
/// supplied on a command line.
///
/// The key must be `EC_SIGN_SECP256K1_SHA256`; other algorithms will not
/// produce recoverable Ethereum signatures. Ambient GCP credentials
/// (workload identity, attached service account, or
/// GOOGLE_APPLICATION_CREDENTIALS) must hold
/// `cloudkms.cryptoKeyVersions.useToSign` and
/// `cloudkms.cryptoKeyVersions.viewPublicKey` on the key version.
///
/// `chain_id` is bound at construction because GcpKmsSigner captures it;
/// callers must therefore resolve the chain ID before building the signer.
pub async fn new_gcp_kms(
project_id: &str,
location: &str,
key_ring: &str,
key_name: String,
key_version: u64,
chain_id: u64,
) -> Result<Self, CKMSError> {
let key_ring_ref = GcpKeyRingRef::new(project_id, location, key_ring);
let provider = GcpKmsProvider::new(key_ring_ref).await?;
let signer = GcpKmsSigner::new(provider, key_name, key_version, chain_id).await?;

Ok(SignerType::GcpKms(signer))
}

pub fn normalize(
&self,
message: impl AsRef<[u8]>,
Expand Down Expand Up @@ -57,6 +90,33 @@ impl SignerType {
}
}

/// Maximum time to wait for a single Google Cloud KMS signing call.
///
/// KMS signing is a network RPC. The relayer's main loop drives valset, batch
/// and logic-call relaying under a single `tokio::join!`, so a signing future
/// that never resolves stops *all* relaying indefinitely rather than failing
/// the one submission. The underlying client sets no deadline of its own, so
/// we impose one here and surface a normal signer error, which the existing
/// submission paths already log and retry on a later loop iteration.
const KMS_SIGN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);

/// Apply [`KMS_SIGN_TIMEOUT`] to a KMS signing future, mapping both the
/// signer's own error and a timeout into a `ProviderError`.
async fn with_kms_timeout<T, E: std::fmt::Display>(
what: &str,
fut: impl std::future::Future<Output = Result<T, E>>,
) -> Result<T, ethers::providers::ProviderError> {
match tokio::time::timeout(KMS_SIGN_TIMEOUT, fut).await {
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(ethers::providers::ProviderError::CustomError(e.to_string())),
Err(_) => Err(ethers::providers::ProviderError::CustomError(format!(
"GCP KMS {} timed out after {}s",
what,
KMS_SIGN_TIMEOUT.as_secs()
))),
}
}

#[async_trait::async_trait]
impl Signer for SignerType {
type Error = ethers::providers::ProviderError;
Expand All @@ -75,10 +135,9 @@ impl Signer for SignerType {
.sign_message(message)
.await
.map_err(|e| ethers::providers::ProviderError::CustomError(e.to_string())),
SignerType::GcpKms(signer) => signer
.sign_message(message)
.await
.map_err(|e| ethers::providers::ProviderError::CustomError(e.to_string())),
SignerType::GcpKms(signer) => {
with_kms_timeout("sign_message", signer.sign_message(message)).await
}
}?;

self.normalize(msg, &sig)
Expand All @@ -90,10 +149,9 @@ impl Signer for SignerType {
.sign_transaction(tx)
.await
.map_err(|e| ethers::providers::ProviderError::CustomError(e.to_string())),
SignerType::GcpKms(signer) => signer
.sign_transaction(tx)
.await
.map_err(|e| ethers::providers::ProviderError::CustomError(e.to_string())),
SignerType::GcpKms(signer) => {
with_kms_timeout("sign_transaction", signer.sign_transaction(tx)).await
}
}?;

// Get the transaction hash for recovery
Expand Down Expand Up @@ -131,14 +189,14 @@ impl Signer for SignerType {
.sign_typed_data(payload)
.await
.map_err(|e| ethers::providers::ProviderError::CustomError(e.to_string())),
SignerType::GcpKms(signer) => signer
.sign_typed_data(payload)
.await
.map_err(|e| ethers::providers::ProviderError::CustomError(e.to_string())),
SignerType::GcpKms(signer) => {
with_kms_timeout("sign_typed_data", signer.sign_typed_data(payload)).await
}
}?;

// Get the typed data hash for recovery
let hash = payload.encode_eip712()
let hash = payload
.encode_eip712()
Comment on lines +192 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

ethers-rs Signature recover RecoveryMessage From impl slice hash prefix

💡 Result:

In ethers-rs, you can recover an Ethereum address from a signature using the Signature::recover method [1][2][3]. This method accepts any type that implements Into [2][4]. The RecoveryMessage enum is designed to handle two distinct scenarios [2][5]: 1. Data(Vec): If you provide raw message bytes, ethers-rs automatically handles the EIP-191 prefixing (i.e., hashing the message with the Ethereum Signed Message domain separator) before attempting recovery [2][4]. 2. Hash(H256): If you have already hashed your message (e.g., using Keccak256), you can pass the hash directly. In this case, the library treats it as a precomputed hash and skips the additional EIP-191 prefixing process [2][4]. To recover an address from a slice or hash, you can use the following patterns: // Recovering from raw message bytes (automatic EIP-191 hashing) let message = b"hello"; let recovered_address = signature.recover(&message[..])?; // Recovering from a precomputed hash (no extra prefixing) use ethers::core::types::H256; let hash = H256::from(keccak256("hello")); // Assuming custom hashing let recovered_address = signature.recover(hash)?; The Signature::recover implementation internally calls hash_message if you provide Data, or uses the raw hash if you provide Hash [2][4]. It then utilizes the VerifyingKey::recover_from_prehash method to derive the public key from the signature's recovery ID and the message hash [2][4]. Finally, it performs the standard Ethereum address derivation: taking the Keccak256 hash of the uncompressed public key (excluding the 0x04 prefix) and keeping the last 20 bytes [2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'types\.rs$|cargo\.lock$|Cargo\.toml$' . | sed 's#^\./##' | head -100

echo
echo "== relevant context around mention/sign normalize =="
rg -n "normalize|recover|SignerType::GcpKms|encode_eip712|sign_typed_data|sign_transaction" -S .

echo
echo "== cargo deps ethers version =="
rg -n 'name = "ethers"|name = "ethers-core"|ethers =|ethers-core' Cargo.lock Cargo.toml -S || true

echo
echo "== git diff stat/name status =="
git diff --stat || true
git diff -- orchestrator/gravity/src/ethereum/types.rs | sed -n '1,220p' || true

Repository: PeggyJV/gravity-bridge

Length of output: 9700


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the Ethereum signer implementation and the normalize helpers without running repo code.
echo "== exact implementation slice =="
if [ -f orchestrator/gravity/src/ethereum/types.rs ]; then
  wc -l orchestrator/gravity/src/ethereum/types.rs
  sed -n '1,260p' orchestrator/gravity/src/ethereum/types.rs | nl -ba
fi

echo
echo "== dependency declarations with lock versions =="
if [ -f Cargo.lock ]; then
  awk '
    /^[[package]]\n/ {pkg=1}
    pkg && /^name = / {name = $0}
    pkg && /^version = / {version = $0}
    pkg && /^source = / {source = $0}
    pkg && /^ \/\// {next}
    /^[[package]]/ || /^[^[:space:]]/ || /^$/ {
      if (pkg && (name != "" || version != "")) {
        if (name ~ /ethers(-core)?/) print name version source
      }
      if (/^[[package]]/) { pkg=1; name=""; version=""; source="" }
      else if (/^$/) pkg=0;
    }
  ' Cargo.lock
elif [ -f Cargo.toml ]; then
  cat Cargo.toml | sed -n '1,220p'
fi

Repository: PeggyJV/gravity-bridge

Length of output: 280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== orchestrator top-level files =="
find orchestrator -maxdepth 2 -type f | sed 's#^`#/`#' | sort | head -100

echo
echo "== relevant ethereum types.rs lines =="
python3 - <<'PY'
from pathlib import Path
p = Path("orchestrator/gravity/src/ethereum/types.rs")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 1 <= i <= 210:
        print(f"{i:4}\t{line}")
PY

echo
echo "== dependency/version declarations =="
python3 - <<'PY'
from pathlib import Path
for p in [Path("Cargo.lock"), Path("Cargo.toml"), Path("orchestrator/Cargo.lock"), Path("orchestrator/Cargo.toml"), Path("orchestrator/gravity/Cargo.toml")]:
    if p.exists():
        print(f"\n-- {p} --")
        text = p.read_text()
        for line in text.splitlines():
            if "ethers" in line.lower() or line.startswith("[") and "version" in line:
                print(line)
PY

echo
echo "== local cargo lock crates around ethers =="
python3 - <<'PY' from pathlib import Path
for root in [Path("orchestrator"), Path(".")]:
    lock = root / "Cargo.lock"
    if lock.exists():
        print(f"-- {lock} --")
        lines = lock.read_text().splitlines()
        for i,l in enumerate(lines):
            if 'name = "ethers"' in l or 'name = "ethers-core"' in l or 'name = "ethers-contract"' in l or 'name = "k256"' in l:
                print("\n".join(lines[max(0,i-4):i+6]))
PY

echo
echo "== signature recovery usage in orchestrator tests/utils =="
python3 - <<'PY'
from pathlib import Path
for p in Path("orchestrator").rglob("*.rs"):
    s = p.read_text().splitlines()
    hit=False
    for i,l in enumerate(s,1):
        if "recover" in l and "signature" in p.name:
            hit=True
            start=max(1,i-4); end=min(len(s),i+8)
            print(f"\n-- {p}:{start}-{end} --")
            for n in range(start,end+1):
                print(f"{n:4}\t{s[n-1]}")
PY

Repository: PeggyJV/gravity-bridge

Length of output: 11039


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Cargo.lock relevant crates =="
python3 - <<'PY'
from pathlib import Path
p=Path("orchestrator/Cargo.lock")
lines=p.read_text().splitlines()
for i,l in enumerate(lines):
    if 'name = "ethers"' in l or 'name = "ethers-core"' in l or 'name = "k256"' in l:
        print("\n".join(lines[max(0,i-4):i+6]))
        print()
PY

echo
echo "== check available curl/jq =="
command -v curl || true
command -v jq || true

echo
echo "== fetch ethers v2.0.7 signature implementation from crates.io =="
tmpdir="$(mktemp -d)"
url="https://crates.io/api/v1/crates/ethers-core/2.0.7/download"
status="$(curl -LfsSw '%{http_code}' "$url" -o "$tmpdir/ethers-core.tar.gz")"
echo "fetch_status=$status"
if [ "$status" = "200" ]; then
  tar -xzf "$tmpdir/ethers-core.tar.gz" -C "$tmpdir"
  found="$(find "$tmpdir" -path '*src/types/signature.rs' | head -1)"
  echo "signature_file=$found"
  if [ -n "$found" ]; then
    rg -n "impl From<|enum RecoveryMessage|recover_message|RecoveryMessage::|hash_message|Signature::recover|impl AsRef" "$found" -C 3
    echo
    sed -n '1,220p' "$found"
  fi
else
  echo "could not fetch ethers-core 2.0.7"
fi

Repository: PeggyJV/gravity-bridge

Length of output: 1326


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== actual dependency versions =="
python3 - <<'PY'
from pathlib import Path
p=Path("orchestrator/Cargo.lock")
lines=p.read_text().splitlines()
for i,l in enumerate(lines):
    if ('name = "ethers"' in l or 'name = "ethers-core"' in l) and i+1 < len(lines) and 'version =' in lines[i+1]:
        print(lines[i], lines[i+1])
PY

echo
echo "== fetch ethers-core v2.0.14 signature implementation =="
tmpdir="$(mktemp -d)"
pkg_url="https://crates.io/api/v1/crates/ethers-core/2.0.14/download"
json="$(curl -Lfs "$pkg_url")"
status="$(echo "$json" | jq -r '.version.downloads // empty')"
echo "json_version=$(echo "$json" | jq -r '.version.num // empty')"
echo "downloader=$status"
if [ -n "$status" ]; then
  rm -f "$tmpdir/ethers-core-*"
  echo "$json" | jq -r '.version.tarball' | xargs -r -I{} curl -LfsS {} -o "$tmpdir/ethers-core.tar.gz"
  tar -xzf "$tmpdir/ethers-core.tar.gz" -C "$tmpdir"
  found="$(find "$tmpdir" -path '*src/types/signature.rs' | head -1)"
  echo "signature_file=$found"
  rg -n "impl From<|enum RecoveryMessage|recover_message|RecoveryMessage::|hash_message|Signature::recover|impl AsRef" "$found" -C 4 || true
  echo
  sed -n '1,230p' "$found"
fi

Repository: PeggyJV/gravity-bridge

Length of output: 328


🌐 Web query:

crates.io ethers-core 2.0.14 source signature.rs ethers_core::types::signature

💡 Result:

In the ethers-core crate version 2.0.14, the Signature struct is defined in the source file src/types/signature.rs [1][2]. This struct represents an ECDSA signature and contains the following fields: [3][4] pub struct Signature { pub r: U256, pub s: U256, pub v: u64, } The Signature implementation includes methods for verifying messages and recovering addresses, such as recover, recover_typed_data, and verify [3][4]. These methods typically use Electrum notation for the v value (expected to be 27 or 28) [4]. The source code for this specific version can be accessed directly through the Docs.rs platform [1][2]. Note that the ethers-rs library is in the process of being deprecated, and users are encouraged to look toward newer alternatives [5][6].

Citations:


🌐 Web query:

docs.rs 2.0.14 ethers_core::types::signature Signature recover RecoveryMessage impl From<[u8; 32]>

💡 Result:

In ethers-core 2.0.14, the RecoveryMessage enum is used to represent the message data that can be signed or verified [1][2]. It provides an implementation of the From trait for [u8; 32], which allows a 32-byte array (typically a message hash) to be converted directly into a RecoveryMessage variant [2][3]. Specifically, the implementation is defined as follows: impl From<[u8; 32]> for RecoveryMessage { fn from(hash: [u8; 32]) -> Self { RecoveryMessage::Hash(hash.into) } } When used with the Signature::recover method, which accepts any type M that implements Into, passing a [u8; 32] will treat the data as a precomputed message hash [1][2]. If the input is instead RecoveryMessage::Data(Vec), the library will automatically hash the message using EIP-191 before performing the recovery [1][2].

Citations:


🌐 Web query:

docs.rs ethers-core 2.0.14 source impl From<&[u8]> RecoveryMessage Data prefix

💡 Result:

In ethers-core version 2.0.14, the RecoveryMessage enum is defined in src/types/signature.rs [1][2]. The implementation of From<&[u8]> for RecoveryMessage converts a slice of bytes into a RecoveryMessage::Data variant by converting the slice into a Vec and then into the enum [1][2]. Regarding the specific mention of a data prefix, the RecoveryMessage enum itself does not contain a prefixing implementation within its From conversion [1][2]. Instead, the prefixing logic for Ethereum signed messages is handled by the hash_message function, located in src/utils/hash.rs [3]. This function implements EIP-191 by prepending the prefix "\x19Ethereum Signed Message:\n" and the decimal length of the message to the input bytes before hashing them with Keccak-256 [3]. The source code for the relevant implementation is as follows: // From src/types/signature.rs in ethers-core 2.0.14 [2] impl From<&[u8]> for RecoveryMessage { fn from(s: &[u8]) -> Self { s.to_owned.into } } // From src/utils/hash.rs in ethers-core 2.0.14 [3] pub fn hash_message<T: AsRef<[u8]>>(message: T) -> H256 { const PREFIX: &str = "\x19Ethereum Signed Message:\n"; let message = message.as_ref; let len = message.len; let len_string = len.to_string; let mut eth_message = Vec::with_capacity(PREFIX.len + len_string.len + len); eth_message.extend_from_slice(PREFIX.as_bytes); eth_message.extend_from_slice(len_string.as_bytes); eth_message.extend_from_slice(message); H256(keccak256(&eth_message)) }

Citations:


🌐 Web query:

docs.rs ethers-core 2.0.14 source Signature recover MessageSignature signature.rs line

💡 Result:

In version 2.0.14 of the ethers-core crate, the recover method for the Signature struct is defined within the src/types/signature.rs file [1][2]. The implementation of the recover method, which recovers the Ethereum address used to sign a given message, typically begins around line 91 of the source file [2]. The method accepts a message that implements the Into trait and returns a Result containing an Address or a SignatureError [1][2]. Within this method, the signature is converted into a recoverable signature and a recovery ID using the internal as_signature method [2]. It then utilizes the VerifyingKey::recover_from_prehash function from the underlying elliptic curve library to recover the public key, which is subsequently hashed to derive the Ethereum address [2]. Note that ethers-core is part of a library that is in the process of being deprecated, and it is recommended to use the re-exports directly from the ethers crate instead [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether crates.io supports a stable source tarball URL for ethers-core 2.0.14.
pkg="ethers-core"
ver="2.0.14"
crates_api="$(curl -LfsS "https://crates.io/api/v1/crates/$pkg/$ver/download" | jq -r '.download_url // empty' 2>/dev/null || true)"
echo "api_download_url=$crates_api"
if [ -n "$crates_api" ]; then
  tmpdir="$(mktemp -d)"
  curl -LfsS "$crates_api" -o "$tmpdir/ethers-core.tar.gz"
  tar -xzf "$tmpdir/ethers-core.tar.gz" -C "$tmpdir"
  found="$(find "$tmpdir" -path '*src/types/signature.rs' | head -1)"
  echo "signature_file=$found"
  rg -n "impl From<|enum RecoveryMessage|recover_message|RecoveryMessage::|hash_message|Signature::recover|impl AsRef" "$found" -C 3 || true
  echo "--- signature.rs top ---"
  sed -n '1,180p' "$found"
fi

echo
echo "== available GitHub API crate source refs =="
python3 - <<'PY'
import urllib.request
url="https://crates.io/api/v1/crates/ethers-core/2.0.14"
text=urllib.request.urlopen(url, timeout=20).read().decode()
print(text[:2000])
PY

Repository: PeggyJV/gravity-bridge

Length of output: 2559


Pass an owned hash into the GCP KMS recovery path.

normalize(&hash, &sig) passes a &[u8] into sig.recover(...), and ethers treats &[u8] as raw data that receives the Ethereum signed-message prefix. KMS signs the raw EIP-712 hash, so recovery for typed-data and transaction signatures can return “Invalid signature while normalizing”. Copy the [u8; 32]/H256 value before calling normalize so it is treated as a raw hash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@orchestrator/gravity/src/ethereum/types.rs` around lines 192 - 199, Update
the typed-data recovery flow after `payload.encode_eip712()` so the hash passed
to `normalize` is an owned `[u8; 32]`/`H256` value rather than a borrowed byte
slice. Preserve the existing GCP KMS signature path while ensuring `sig.recover`
treats the KMS-signed EIP-712 hash as raw data without adding the Ethereum
signed-message prefix.

.map_err(|e| ethers::providers::ProviderError::CustomError(e.to_string()))?;
self.normalize(&hash, &sig)
}
Expand Down
Loading
Loading