Migration Guide
Overview
Section titled “Overview”The latest version, v2, introduces significant architectural improvements over version v1.
V1 Architecture
Section titled “V1 Architecture”- 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
V2 Architecture
Section titled “V2 Architecture”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
Breaking Changes
Section titled “Breaking Changes”1. From Prover Contracts to Native Precompile
Section titled “1. From Prover Contracts to Native Precompile”V1 had:
Section titled “V1 had:”- Prover contracts with escrow/payment system
- Off-chain STARK proof generation
- Query submission → wait → read results workflow
V2 has:
Section titled “V2 has:”- Direct precompile calls to
0x0FD2 - Native verification (no STARK proofs)
- Immediate verification results
2. New Precompile Addresses added
Section titled “2. New Precompile Addresses added”V1 had:
Section titled “V1 had:”0x0FD1(4049) - SubstrateTransferPrecompile0x13B9(5049) - SignatureVerifierPrecompile
V2 has:
Section titled “V2 has:”0x0FD1(4049) - SubstrateTransferPrecompile (unchanged)0x13B9(5049) - SignatureVerifierPrecompile (unchanged)0x0FD2(4050) - BlockProverPrecompile (New - Native Query Verifier)0x0FD3(4051) - ChainInfoPrecompile (New)
3. Interface Function Changes
Section titled “3. Interface Function Changes”V2 Functions:
Section titled “V2 Functions:”verifyAndEmit()- State-changing function that emitsTransactionVerifiedeventsverify()- 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);Network Endpoints
Section titled “Network Endpoints”See Quickstart for network endpoints and configuration.
Smart Contract Migration
Section titled “Smart Contract Migration”In this section, we provide a brief overview of what all changes you will notice moving from v1 to v2.
V1 Approach:
Section titled “V1 Approach:”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 costconst computedQueryCost = await ccNextPublicClient.readContract({ address: proverContractAddress as `0x${string}`, abi: PROVER_ABI, functionName: 'computeQueryCost', args: [query],});
// Submit query with paymentconst { 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; } }}V2 Approach:
Section titled “V2 Approach:”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; }}Code Migration Examples
Section titled “Code Migration Examples”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.
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”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
chainKeymatches 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:
targetHeightis not the current source chain headtargetHeight = 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.
Debugging Tips
Section titled “Debugging Tips”- 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
Support
Section titled “Support”If you encounter issues during migration:
- Check the troubleshooting section above
- Read the docs carefully once, especially the DApp builder section
- Checkout the example code in examples repository
- Check the Creditcoin repositories and public channels for latest updates