Cosmos EVM Workarounds
Specter is built on a Cosmos SDK + EVM architecture (Ethermint-derived). While this provides the benefits of both ecosystems, there are known compatibility gaps where standard Ethereum JSON-RPC methods do not behave as expected. These are not Specter bugs — they are inherent limitations of running an EVM inside a Cosmos consensus engine.
This page documents the known issues and the recommended workarounds.
eth_getLogs Returns Empty Arrays
The Problem
The standard Ethereum JSON-RPC method eth_getLogs is unreliable on Cosmos EVM chains. Queries that would return results on Ethereum or other EVM chains frequently return empty arrays, even when matching events were clearly emitted in the specified block range.
// This often returns [] even when events exist
const logs = await provider.getLogs({
address: contractAddress,
topics: [commitmentEventTopic],
fromBlock: depositBlockNumber,
toBlock: depositBlockNumber,
});
console.log(logs); // [] — empty, even though the event was emitted
This affects any code that relies on event log queries, including:
- Fetching deposit
Commitmentevents from the privacy pool contract - Monitoring contract events via polling
- Historical event indexing using standard Web3 libraries
The Root Cause
Cosmos EVM implementations store EVM logs differently than native Ethereum clients. The Tendermint block structure and transaction indexing do not map cleanly to the Ethereum log filter API. The EVM module's log indexing is best-effort and may miss events, especially during periods of high throughput or when querying historical ranges.
The Workaround
Use a dedicated indexer service instead of eth_getLogs. The Specter indexer provides a POST /commitment endpoint that reliably returns commitment data:
// Instead of eth_getLogs, use the indexer API
const response = await fetch("https://indexer.specterchain.com/commitment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contractAddress: poolContractAddress,
fromBlock: startBlock,
toBlock: endBlock,
}),
});
const commitments = await response.json();
For non-commitment events, run your own indexer that processes blocks sequentially using eth_getBlockByNumber with full transaction receipts, rather than relying on log filters.
Alternative: Block-by-Block Scanning
If you cannot use the indexer, you can scan blocks individually:
async function getEventsFromBlock(provider, blockNumber, contractAddress, eventTopic) {
const block = await provider.getBlockWithTransactions(blockNumber);
const events = [];
for (const tx of block.transactions) {
if (tx.to?.toLowerCase() === contractAddress.toLowerCase()) {
const receipt = await provider.getTransactionReceipt(tx.hash);
if (receipt && receipt.logs) {
for (const log of receipt.logs) {
if (log.topics[0] === eventTopic) {
events.push(log);
}
}
}
}
}
return events;
}
This is slower but more reliable than eth_getLogs.
eth_getTransactionReceipt Parse Errors
The Problem
Calling eth_getTransactionReceipt for certain transactions returns parse errors or malformed data instead of a valid receipt object. This is particularly common with transactions that involve Cosmos-level operations (e.g., transactions that trigger x/ghostmint minting via the precompile).
try {
const receipt = await provider.getTransactionReceipt(txHash);
// May throw: "could not decode transaction receipt"
// Or return a receipt with missing/malformed fields
} catch (error) {
console.error(error.message);
// "missing response for request" or similar parse error
}
The Root Cause
When an EVM transaction triggers a Cosmos-level state change (such as minting via the x/ghostmint precompile), the resulting transaction receipt may contain fields that standard Ethereum client libraries cannot parse. The Cosmos SDK processes these transactions differently from pure EVM transactions, and the receipt encoding does not always conform to the Ethereum receipt RLP specification.
The Workaround
Instead of relying on transaction receipts to obtain contract addresses or event data, compute the expected values directly.
Computing Contract Addresses
If you need the address of a contract deployed in a transaction, compute it from the deployer address and nonce:
const { ethers } = require("ethers");
// Compute the contract address deterministically
function getDeployedContractAddress(deployerAddress, nonce) {
return ethers.getCreateAddress({
from: deployerAddress,
nonce: nonce,
});
}
// Example: deployer's first contract deployment (nonce = 0)
const contractAddress = getDeployedContractAddress(
"0xDeployerAddress...",
0
);
console.log("Contract deployed at:", contractAddress);
For CREATE2 deployments, compute the address using the factory, salt, and init code hash:
const contractAddress = ethers.getCreate2Address(
factoryAddress,
salt,
initCodeHash
);
Verifying Transaction Success
If you cannot parse the receipt, verify transaction inclusion by checking the transaction itself:
// Check if the transaction was included in a block
const tx = await provider.getTransaction(txHash);
if (tx && tx.blockNumber) {
console.log("Transaction included in block:", tx.blockNumber);
// Verify the expected state change occurred
const balance = await provider.getBalance(recipientAddress);
console.log("Recipient balance:", ethers.formatEther(balance));
}
leafIndex Must Be Obtained On-Chain Before Vanish
The Problem
In the GHOST privacy protocol, when a user deposits tokens into the shielded pool, a Commitment event is emitted containing a leafIndex — the position of the commitment in the Merkle tree. This leafIndex is required to later construct a valid withdrawal proof.
On standard Ethereum, you would retrieve the leafIndex from the transaction receipt's logs. On Specter's Cosmos EVM, this is unreliable due to the eth_getLogs and eth_getTransactionReceipt issues described above.
// This approach is UNRELIABLE on Cosmos EVM
const receipt = await provider.getTransactionReceipt(depositTxHash);
const leafIndex = parseLeafIndexFromReceipt(receipt); // May fail or return wrong value
Why This Is Critical
The leafIndex is essential for constructing the Merkle proof needed for withdrawal. If you lose the leafIndex, you cannot prove that your commitment exists in the tree, and your deposited funds become unrecoverable.
The term "vanish" refers to the commitment data becoming difficult to retrieve after the fact — once the transaction is buried deep enough in the chain history, reconstructing the leafIndex from event logs becomes increasingly unreliable.
The Workaround
Query the leafIndex from the contract's on-chain state immediately after the deposit transaction is confirmed, before relying on event logs.
// Read leafIndex directly from contract state
const pool = new ethers.Contract(poolAddress, poolABI, provider);
// Option 1: Query the next leaf index before and after deposit
const leafIndexBefore = await pool.nextIndex();
// ... submit deposit transaction and wait for confirmation ...
const leafIndexAfter = await pool.nextIndex();
const myLeafIndex = leafIndexBefore; // Your commitment's index
// Option 2: Use the indexer API
const response = await fetch("https://indexer.specterchain.com/commitment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contractAddress: poolAddress,
leafIndex: expectedLeafIndex,
}),
});
const commitmentData = await response.json();
Best Practice: Store Deposit Data Immediately
Always persist the following data locally immediately after a deposit is confirmed:
const depositRecord = {
txHash: depositTx.hash,
blockNumber: depositTx.blockNumber,
leafIndex: myLeafIndex, // Queried on-chain immediately
commitment: commitmentHash, // Computed locally
nullifierHash: nullifierHash, // Computed locally
secret: secret, // User's secret (store securely!)
timestamp: Date.now(),
};
// Save to local storage, encrypted file, etc.
saveDepositRecord(depositRecord);
Do not rely on being able to reconstruct this data later from chain queries.
Summary of Workarounds
| Standard Method | Issue on Cosmos EVM | Workaround |
|---|---|---|
eth_getLogs | Returns empty arrays | Use indexer POST /commitment endpoint or block-by-block scanning |
eth_getTransactionReceipt | Parse errors / malformed data | Compute contract addresses from deployer+nonce; verify state changes directly |
| leafIndex from receipt logs | Unreliable retrieval | Query on-chain state (nextIndex) immediately after deposit; persist locally |
General Recommendations
-
Do not assume Ethereum JSON-RPC parity. Cosmos EVM implements the Ethereum JSON-RPC specification on a best-effort basis. Always test your integrations against a Specter node, not just Hardhat or Ganache.
-
Use the indexer for event data. The Specter indexer processes blocks at the Cosmos level and provides reliable event data. Prefer it over
eth_getLogsfor any production use case. -
Persist critical data client-side. For privacy protocol interactions (deposits, commitments, leaf indices), store all necessary data locally at transaction time. Do not assume you can recover it later from the chain.
-
Verify state changes, not receipts. After submitting a transaction, verify the expected state change occurred (e.g., balance changed, commitment exists in the tree) rather than parsing the receipt.
-
Handle errors gracefully. Wrap all JSON-RPC calls in try/catch blocks and implement retry logic with exponential backoff. Transient failures are more common on Cosmos EVM than on native Ethereum clients.
async function reliableCall(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, i)));
}
}
}
// Usage
const balance = await reliableCall(() =>
provider.getBalance(address)
);
- Report new issues. If you encounter a Cosmos EVM compatibility issue not documented here, report it in the Specter Discord or GitHub repository. These gaps are tracked and addressed in chain upgrades where possible.