Skip to content
Global documentation search

Migration Guide

The latest version, v2, introduces significant architectural improvements over version v1.

  • Prover Contracts - Smart contracts acting as oracle interfaces (e.g., IPublicProver)
  • STARK Proving - Cryptographic proofs generated using Cairo programs and Stakeware’s stone prover
  • Prover Services - Separate off-chain services that generated proofs
  • Escrow/Payment System - Payment held in escrow until proof verification
  • Query Submission Flow - Users submit queries → provers generate STARK proofs → proofs verified on-chain

Significantly simplified:

  • Native Query Verification Precompile - Direct on-chain verification at 0x0FD2
  • No STARK Proofs Required - Native verification without separate cryptographic proofs
  • No Prover Contracts - Direct precompile calls, no need to deploy oracle interface contracts
  • No Escrow System - Direct verification without payment escrow
  • Simplified Flow - Direct verification calls with immediate results

1. From Prover Contracts to Native Precompile

Section titled “1. From Prover Contracts to Native Precompile”
  • Prover contracts with escrow/payment system
  • Off-chain STARK proof generation
  • Query submission → wait → read results workflow
  • Direct precompile calls to 0x0FD2
  • Native verification (no STARK proofs)
  • Immediate verification results
  • 0x0FD1 (4049) - SubstrateTransferPrecompile
  • 0x13B9 (5049) - SignatureVerifierPrecompile
  • 0x0FD1 (4049) - SubstrateTransferPrecompile (unchanged)
  • 0x13B9 (5049) - SignatureVerifierPrecompile (unchanged)
  • 0x0FD2 (4050) - BlockProverPrecompile (New - Native Query Verifier)
  • 0x0FD3 (4051) - ChainInfoPrecompile (New)
  • verifyAndEmit() - State-changing function that emits TransactionVerified events
  • verify() - View function for read-only verification
  • Batch verification overloads for both functions

New Event (optional; emitted with verifyAndEmit()):

Section titled “New Event (optional; emitted with verifyAndEmit()):”
event TransactionVerified(
uint64 indexed chainKey,
uint64 indexed height,
uint64 transactionIndex
);

See Quickstart for network endpoints and configuration.

In this section, we provide a brief overview of what all changes you will notice moving from v1 to v2.

V1 uses prover contracts that store query results. The complete flow involves multiple steps across different files and toolchains:

Step 1: Submit query with payment

Off-chain code submits a query to the prover contract with payment (snippet from submit_query.ts):

// Calculate query cost
const computedQueryCost = await ccNextPublicClient.readContract({
address: proverContractAddress as `0x${string}`,
abi: PROVER_ABI,
functionName: 'computeQueryCost',
args: [query],
});
// Submit query with payment
const { request } = await ccNextPublicClient.simulateContract({
address: proverContractAddress as `0x${string}`,
account: ccNextWalletClient.account,
abi: PROVER_ABI,
functionName: 'submitQuery',
args: [query, ccNextWalletClient.account?.address!],
value: computedQueryCost,
});
const txHash = await ccNextWalletClient.writeContract(request);

Step 2: Wait for prover to generate STARK proof and submit

The prover contract emits QuerySubmitted event. Off-chain provers listen to this event, generate STARK proofs, and submit them back to the contract. This step happens asynchronously off-chain.

Step 3: Listen for QueryProofVerified event

Off-chain code listens for the QueryProofVerified event indicating the proof was verified and results are stored (snippet from submit_query.ts):

const queryProofVerified = PROVER_ABI.find(
(abiElement) =>
abiElement.type === 'event' && abiElement.name === 'QueryProofVerified'
);
const proverFilter = await ccNextPublicClient.createEventFilter({
address: proverContractAddress as `0x${string}`,
events: [queryProofVerified],
fromBlock: BigInt(startBlock - BLOCK_LAG),
toBlock: BigInt(currentBlock - BLOCK_LAG),
});
const proverLogs = await ccNextPublicClient.getFilterLogs({
filter: proverFilter,
});
for (const log of proverLogs) {
const decodedLog = decodeEventLog({
abi: PROVER_ABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'QueryProofVerified') {
// Query proof verified, results are now available in contract storage
console.log(`Query proving complete. QueryId: ${decodedLog.args.queryId}`);
}
}

Step 4: Read results from contract storage

Contracts inherit from UniversalSmartContract_Core and read ResultSegment[] from prover contract storage (snippet from MintableUSCBridge.sol):

import {ICreditcoinPublicProver} from "@gluwa/creditcoin-public-prover/contracts/sol/Prover.sol";
import {UniversalSmartContract_Core} from "./UniversalSmartContract_Core.sol";
abstract contract MintableUSCBridge is UniversalSmartContract_Core, ERC20 {
mapping(bytes32 => bool) public processedQueries;
function mintFromQuery(
address proverContractAddr, // Prover contract address required
bytes32 queryId // Query ID from prover contract
) external {
if (processedQueries[queryId]) revert QueryAlreadyProcessed();
processedQueries[queryId] = true;
// Read ResultSegment[] from prover contract storage
(bytes32 functionSig, ResultSegment[] memory eventSegments) =
_processUSCQuery(proverContractAddr, queryId);
// Extract data from segments (not full transaction receipts)
if (bytes4(functionSig) != TRANSFER_EVENT_SIG) revert InvalidFunctionSignature();
address from = address(uint160(uint256(bytes32(eventSegments[0].abiBytes))));
address to = address(uint160(uint256(bytes32(eventSegments[1].abiBytes))));
uint256 amount = uint256(bytes32(eventSegments[2].abiBytes));
// Process extracted segments
require(to == address(0), "Invalid burn address");
_mint(from, amount);
emit TokensMinted(address(this), from, amount, queryId);
}
}

The _processUSCQuery function reads from prover contract storage (snippet from UniversalSmartContract_Core.sol):

function _processUSCQuery(
address proverContractAddr,
bytes32 queryId
) internal returns(bytes32 functionSignature, ResultSegment[] memory eventSegments) {
ICreditcoinPublicProver prover = ICreditcoinPublicProver(proverContractAddr);
QueryDetails memory queryDetails = prover.getQueryDetails(queryId);
ResultSegment[] memory resultSegments = queryDetails.resultSegments;
// Extract function signature and event segments from stored results
require(resultSegments.length >= 8, "Invalid result length");
functionSignature = resultSegments[4].abiBytes;
uint256 resultLength = resultSegments.length;
eventSegments = new ResultSegment[](resultLength - 5);
for (uint256 i = 5; i < resultLength;) {
eventSegments[i] = resultSegments[i];
unchecked { ++i; }
}
}

In v2 this process is really simplified by direct precompile calls with full transaction data (snippet from SimpleMinterUSC.sol):

import {EvmV1Decoder} from "./EvmV1Decoder.sol";
contract SimpleMinterUSC is ERC20 {
INativeQueryVerifier public immutable VERIFIER =
INativeQueryVerifier(0x0000000000000000000000000000000000000FD2);
mapping(bytes32 => bool) public processedQueries;
function mintFromQuery(
uint64 chainKey,
uint64 blockHeight,
bytes calldata encodedTransaction,
bytes32 merkleRoot,
INativeQueryVerifier.MerkleProofEntry[] calldata siblings,
bytes32 lowerEndpointDigest,
bytes32[] calldata continuityRoots
) external returns (bool) {
//Calculate transaction index from merkle proof
uint256 transactionIndex = _calculateTransactionIndex(siblings);
bytes32 txKey = keccak256(abi.encodePacked(chainKey, blockHeight, transactionIndex));
require(!processedQueries[txKey], "Query already processed");
//Build proofs and verify directly
INativeQueryVerifier.MerkleProof memory merkleProof =
INativeQueryVerifier.MerkleProof({root: merkleRoot, siblings: siblings});
INativeQueryVerifier.ContinuityProof memory continuityProof =
INativeQueryVerifier.ContinuityProof({
lowerEndpointDigest: lowerEndpointDigest,
roots: continuityRoots
});
bool verified = VERIFIER.verifyAndEmit(
chainKey, blockHeight, encodedTransaction, merkleProof, continuityProof
);
require(verified, "Verification failed");
processedQueries[txKey] = true;
//Decode and validate receipt status (full transaction receipt available)
EvmV1Decoder.ReceiptFields memory receipt =
EvmV1Decoder.decodeReceiptFields(encodedTransaction);
require(receipt.receiptStatus == 1, "Transaction did not succeed");
//Extract Transfer events from full receipt logs
bytes32 TRANSFER_EVENT_SIGNATURE = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
EvmV1Decoder.LogEntry[] memory transferLogs =
EvmV1Decoder.getLogsByEventSignature(receipt, TRANSFER_EVENT_SIGNATURE);
require(transferLogs.length > 0, "No transfer events found");
//Process transfer events from full receipt
EvmV1Decoder.CommonTxFields memory txFields =
EvmV1Decoder.decodeCommonTxFields(encodedTransaction);
bool found = _processTransferLogs(transferLogs, txFields.from);
require(found, "No valid burn transfer found");
_mint(msg.sender, MINT_AMOUNT);
emit TokensMinted(address(this), msg.sender, MINT_AMOUNT, txKey);
return true;
}
}

To better understand how to use version v2, see Universal Smart Contracts for in-depth explanation. Details example contracts are also available in the examples repository.

1. “Proof not found” / 404 from Proof API Server

Problem: Proof generation API server cannot find the transaction or block

Solutions:

  • Check if proof server is running via: /api/v1/health
  • Verify transaction hash/index is correct
  • Check if block height exists on source chain and attestation block has been created

2. “Attestation not found” / “Block not ready”

Problem: Block hasn’t been attested yet by the attestor network

Solutions:

  • Wait for attestors to attest the block
  • Verify attestor network is running and check attestation status on Creditcoin node

3. “Attestations missing for chain {chain_key}”

Problem: No attestations exist for the specified chainKey

Solutions:

  • Verify attestor network is running and configured for this chain
  • Check if attestations exist on-chain and verify chainKey matches source chain configuration

4. “Proof server down” / Connection refused

Problem: Proof generation API server is not running or unreachable

Solutions:

  • Check if proof server is running: curl <http://localhost:3100/api/v1/health>
  • Check server logs and verify RPC connections for issues

5. “No attestation or checkpoint found after block {height}”

Problem: Continuity proof requires an attestation/checkpoint after the query block, but it doesn’t exist yet

Solutions:

  • Wait for next attestation to be created (check attestation interval, e.g., every 10 blocks)

6. “Transaction hash not found”

Problem: Proof API cannot find transaction by hash

Solutions:

  • Try using block height and transaction index instead: /api/v1/proof/{chainKey}/{height}/{txIndex}

7. “Query height out of range”

Problem: Requested block height is invalid

Solutions:

  • Verify height is between lower and upper attestation bounds (not at or outside them)
  • Check if query height matches an attestation/checkpoint block (may need to adjust)
  • Ensure attestations exist for the chain and query height is within attested range

8. “Proof generation server returns 500 error for transactions”

Problem: Proof generation API server returns HTTP 500 (Internal Server Error) when requesting proofs

Solutions:

  • Check proof server logs for specific error details (database errors, RPC failures, Merkle proof generation issues)
  • Verify the transaction block has been attested (see issue #2 above)
  • Ensure source chain RPC is accessible and responding correctly
  • Check database connectivity if proof server uses caching
  • Verify the transaction exists on the source chain and is finalized
  • Wait for attestation before requesting proof (see Worker Flow below)

9. Worker Flow: Understanding targetHeight

Problem: Confusion about when workers should request proofs and what height to wait for

Clarification:

  • targetHeight is not the current source chain head
  • targetHeight = transactionBlockNumber + 1
  • Workers should wait until latestAttestation >= targetHeight
  • This guarantees the transaction block itself is attested before proof generation

This matters because if a transaction is in block N, waiting for block N+1 to be attested ensures block N is attested. Requesting proofs before the block is attested will result in “Block not ready” or 500 errors. This is related to issue #8 above - premature proof requests cause server errors

10. “Gas estimation failed: execution reverted”

Problem: Gas estimation fails because it simulates the transaction execution, and during simulation the precompile may revert. This is a known issue with pallet-evm. Note that the gas estimation failure does not necessarily mean the transaction will fail.

Workaround: Catch gas estimation failures and calculate gas based on continuity proof size instead:

async function computeGasLimit(
provider: JsonRpcApiProvider,
contract: Contract,
data: string,
from: string,
continuityLength: number
): Promise<bigint> {
console.log('⏳ Estimating gas...');
let gasLimit;
try {
const estimatedGas = await provider.estimateGas({
to: contract.getAddress(),
data,
from,
});
gasLimit = (estimatedGas * BigInt(135)) / BigInt(100); // Add 35% buffer
console.log(` Estimated gas: ${estimatedGas.toString()}, Gas limit with buffer: ${gasLimit.toString()}`);
} catch (error: any) {
// Calculate a reasonable estimate based on continuity proof size (matching Rust logic)
// Base: 21000 (tx) + ~5000 per continuity block + ~20000 for merkle + overhead
const calculatedGas = 21000 + continuityLength * 5000 + 20000;
console.warn(` Gas estimation failed: ${error.shortMessage || error.message}`);
console.log(
` Using calculated gas limit based on proof size: ${calculatedGas} (${continuityLength} continuity blocks)`
);
gasLimit = BigInt(calculatedGas);
}
return gasLimit;
}

This workaround is already implemented in the computeGasLimit() function in the examples repository.

  • Check proof server logs for detailed error messages
  • Verify all services are running: Creditcoin node, attestor network, proof server, database
  • Use proof API health endpoint: /api/v1/health
  • Check attestation status via Creditcoin RPC or Polkadot.js
  • Enable verbose logging in proof server for more details

If you encounter issues during migration:

  1. Check the troubleshooting section above
  2. Read the docs carefully once, especially the DApp builder section
  3. Checkout the example code in examples repository
  4. Check the Creditcoin repositories and public channels for latest updates