Skip to main content

Address Validity

The AddressValidity attestation type validates whether a string represents a valid address on supported blockchain networks (BTC, DOGE, and XRP). This validation ensures addresses meet chain-specific formatting and checksum requirements before they're used in transactions or smart contracts. The full specification is available on the official specification repo.

The primary contract interface for this attestation type is IAddressValidity. Let's walk through validating a Bitcoin testnet address using the FDC protocol. We will use the address mg9P9f4wr9w7c1sgFeiTC5oMLYXCc2c7hs as an example throughout this guide. You can swap this with any valid testnet address from the supported chains. You can follow this tutorial with any other valid address - just make sure it is a valid testnet address.

This validation process works identically for BTC, DOGE, and XRP addresses, with only minor chain-specific parameter adjustments which we'll highlight throughout the guide.

In this guide, we will follow the steps outlined in the FDC overview.

Our implementation requires handling the FDC voting round finalization process. To manage this, we will create separate scripts in script/fdcExample/AddressValidity.s.sol that handle different stages of the validation process:

scrip/fdcExample/AddressValidity.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Script} from "dependencies/forge-std-1.9.5/src/Script.sol";
...

// Configuration constants
string constant attestationTypeName = "AddressValidity";
string constant dirPath = "data/";

contract PrepareAttestationRequest is Script {
...
}

contract SubmitAttestationRequest is Script {
...
}

contract RetrieveDataAndProof is Script {
...
}

contract Deploy is Script {
...
}
...

The names of included contracts mostly mirror the steps described in the FDC overview.

To bridge the separate script executions, we will save the relevant data of each script to a file in the dirPath folder. Each succeeding script will then read that file to load the data.

Prepare request

A JSON request to the verifier follows the same structure for all attestation types, with field values varying per type.

Required Fields

  • attestationType: UTF8 hex string encoding of the attestation type name, zero-padded to 32 bytes.
  • sourceId: UTF8 hex string encoding of the data source identifier name, zero-padded to 32 bytes.
  • requestBody: Specific to each attestation type.

For AddressValidity, requestBody contains a single field:

  • addressString: The address to verify.

Reference Documentation

Example Values

  • attestationType: UTF8 hex encoding of AddressValidity, zero-padded to 32 bytes.
  • sourceId: UTF8 hex encoding of testBTC, zero-padded to 32 bytes.
    • "test" prefix denotes Bitcoin testnet.
    • Supports deployment on Flare testchains (Coston or Coston2).
    • Replace testBTC with testDOGE or testXRP for other chains.
  • addressString: mg9P9f4wr9w7c1sgFeiTC5oMLYXCc2c7hs.

Encoding Functions

To encode values into UTF8 hex:

  • toUtf8HexString: Converts a string to UTF8 hex.
  • toHexString: Zero-right-pads the string to 32 bytes.

These functions are included in the Base library within the example repository, but they can also be defined locally in your contract or script.

scrip/fdcExample/Base.s.sol
function toHexString(
bytes memory data
) public pure returns (string memory) {
bytes memory alphabet = "0123456789abcdef";

bytes memory str = new bytes(2 + data.length * 2);
str[0] = "0";
str[1] = "x";
for (uint i = 0; i < data.length; i++) {
str[2 + i * 2] = alphabet[uint(uint8(data[i] >> 4))];
str[3 + i * 2] = alphabet[uint(uint8(data[i] & 0x0f))];
}
return string(str);
}
scrip/fdcExample/Base.s.sol
function toUtf8HexString(
string memory _string
) internal pure returns (string memory) {
string memory encodedString = toHexString(
abi.encodePacked(_string)
);
uint256 stringLength = bytes(encodedString).length;
require(stringLength <= 64, "String too long");
uint256 paddingLength = 64 - stringLength + 2;
for (uint256 i = 0; i < paddingLength; i++) {
encodedString = string.concat(encodedString, "0");
}
return encodedString;
}

We also define a helper function for formatting data into a JSON string.

scrip/fdcExample/Base.s.sol
function prepareAttestationRequest(
string memory attestationType,
string memory sourceId,
string memory requestBody
) internal view returns (string[] memory, string memory) {
// We read the API key from the .env file
string memory apiKey = vm.envString("VERIFIER_API_KEY");

// Preparing headers
string[] memory headers = prepareHeaders(apiKey);
// Preparing body
string memory body = prepareBody(
attestationType,
sourceId,
requestBody
);

console.log(
"headers: %s",
string.concat("{", headers[0], ", ", headers[1]),
"}\n"
);
console.log("body: %s\n", body);
return (headers, body);
}

function prepareHeaders(
string memory apiKey
) internal pure returns (string[] memory) {
string[] memory headers = new string[](2);
headers[0] = string.concat('"X-API-KEY": ', apiKey);
headers[1] = '"Content-Type": "application/json"';
return headers;
}

function prepareBody(
string memory attestationType,
string memory sourceId,
string memory body
) internal pure returns (string memory) {
return
string.concat(
'{"attestationType": ',
'"',
attestationType,
'"',
', "sourceId": ',
'"',
sourceId,
'"',
', "requestBody": ',
body,
"}"
);
}

In the example repository, these are once again included within the Base library file.

Thus, the part of the script that prepares the verifier request looks like:

scrip/fdcExample/AddressValidity.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {console} from "dependencies/forge-std-1.9.5/src/console.sol";
import {Script} from "dependencies/forge-std-1.9.5/src/Script.sol";
import {Base} from "./Base.s.sol";
...

string constant attestationTypeName = "AddressValidity";
string constant dirPath = "data/";

contract PrepareAttestationRequest is Script {
using Surl for *;

// Setting request data
string public addressStr = "mg9P9f4wr9w7c1sgFeiTC5oMLYXCc2c7hs"; // Id of the Bitcoin address to be validated
string public baseSourceName = "btc"; // Part of verifier URL
string public sourceName = "testBTC"; // Bitcoin chain ID

function prepareRequestBody(
string memory addressStr
) private pure returns (string memory) {
return string.concat('{"addressStr": "', addressStr, '"}');
}

function run() external {
// Preparing request data
string memory attestationType = Base.toUtf8HexString(
attestationTypeName
);
string memory sourceId = Base.toUtf8HexString(sourceName);
string memory requestBody = prepareRequestBody(addressStr);
(string[] memory headers, string memory body) = Base
.prepareAttestationRequest(attestationType, sourceId, requestBody);

// TODO change key in .env
// string memory baseUrl = "https://testnet-verifier-fdc-test.aflabs.org/";
string memory baseUrl = vm.envString("VERIFIER_URL_TESTNET");
string memory url = string.concat(
baseUrl,
"verifier/",
baseSourceName,
"/",
attestationTypeName,
"/prepareRequest"
);
console.log("url: %s", url);

(string[] memory headers, string memory body) = prepareAttestationRequest(attestationType, sourceId, requestBody);

...
}
}

...

If you are accessing a different chain, replace the baseSourceName with an appropriate value, doge or xrp.

The code above differs slightly from the starter example. But, if we remove the ellipses ... signifying missing code, we can still run the script.

Because of the console.log commands it will produce JSON strings that represent valid requests; we can then pass this to the interactive verifier to check what the response is.

We can run the script by calling the following commands in the console.

source .env
forge script script/fdcExample/AddressValidity.s.sol:PostRequest --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast  --ffi

The prerequisite for this is that the .env file is not missing the PRIVATE KEY and COSTON2_RPC_URL values. The script can also access other chains; that can be achieved by replacing the --rpc-url value with COSTON_RPC_URL, FLARE_RPC_URL, or SONGBIRD_RPC_URL.

Post request to verifier

Before submitting address validation requests to the FDC protocol, we first need to prepare and send them to a verifier server. This section walks through the request submission process using the surl package. We place using Surl for *; at the start of our PostRequest contract, and then call its post method on the verifier URL.

scrip/fdcExample/AddressValidity.s.sol
(, bytes memory data) = url.post(headers, body);

We construct the URL by appending to the verifier address https://fdc-verifiers-testnet.flare.network/ the path verifier/btc/AddressValidity/prepareRequest. We can do so dynamically with the following code.

scrip/fdcExample/AddressValidity.s.sol
string memory baseUrl = "https://fdc-verifiers-testnet.flare.network/";
string memory url = string.concat(
baseUrl,
"verifier/",
baseSourceName,
"/",
attestationTypeName,
"/prepareRequest"
);
console.log("url: %s", url);
string memory requestBody = string.concat(
'{"addressStr": "',
addressStr,
'"}'
);

Lastly, we parse the return data from the verifier server. Using the Foundry parseJson shortcode, and a custom struct AttestationResponse, we decode the returned data and extract from it the ABI encoded request.

scrip/fdcExample/Base.s.sol
function parseAttestationRequest(
bytes memory data
) internal pure returns (AttestationResponse memory) {
string memory dataString = string(data);
bytes memory dataJson = vm.parseJson(dataString);

AttestationResponse memory response = abi.decode(
dataJson,
(AttestationResponse)
);

console.log("response status: %s\n", response.status);
console.log("response abiEncodedRequest: ");
console.logBytes(response.abiEncodedRequest);
console.log("\n");

return response;
}
info

If everything went right, the abiEncodedRequest should look something like this.

0x
4164647265737356616c69646974790000000000000000000000000000000000
7465737442544300000000000000000000000000000000000000000000000000
7d2ef938d4ffd2392f588bf46563e07ab885b15fead91c1bb99b16f465b71a68
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000022
6d6739503966347772397737633173674665695443356f4d4c59584363326337
6873000000000000000000000000000000000000000000000000000000000000

Let's break it down line by line:

  • First line: toUtf8HexString("AddressValidity")
  • Second line: toUtf8HexString("testBTC")
  • Third line: message integrity code (MIC), a hash of the whole response salted with a string "Flare", ensures the integrity of the attestation
  • Remaining lines: ABI encoded AddressValidity.RequestBody Solidity struct

What this demonstrates is that, with some effort, the abiEncodedRequest can be constructed manually.

Submit request to FDC

This step transitions from off-chain request preparation to on-chain interaction with the FDC protocol. Now, we submit the validated request to the blockchain using deployed smart contracts.

Submit request

The entire submission process requires only five key steps:

script/fdcExample/Base.s.sol
function submitAttestationRequest(
bytes memory abiEncodedRequest
) internal {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
.getFdcRequestFeeConfigurations();
uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(
abiEncodedRequest
);
console.log("request fee: %s\n", requestFee);
vm.stopBroadcast();

vm.startBroadcast(deployerPrivateKey);

IFdcHub fdcHub = ContractRegistry.getFdcHub();
console.log("fcdHub address:");
console.log(address(fdcHub));
console.log("\n");

fdcHub.requestAttestation{value: requestFee}(abiEncodedRequest);
vm.stopBroadcast();
}

Step-by-Step Breakdown

  1. Load Private Key The private key is read from the .env file using Foundry's envUint function:
       uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
  1. Obtain Request Fee We retrieve the required requestFee from the FdcRequestFeeConfigurations contract:
        IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
.getFdcRequestFeeConfigurations();
uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(
response.abiEncodedRequest
);

This is done in a separate broadcast to ensure requestFee is available before submitting the request.

  1. Access FdcHub Contract Using the ContractRegistry library (from flare-periphery), we fetch the FdcHub contract:
   IFdcHub fdcHub = ContractRegistry.getFdcHub();
console.log("fcdHub address:");
console.log(address(fdcHub));
console.log("\n");
  1. Submit the Attestation Request We send the attestation request with the required fee:
 fdcHub.requestAttestation{value: requestFee}(response.abiEncodedRequest);
  1. Calculate the Voting Round Number To determine the voting round in which the attestation request is processed, we query the FlareSystemsManager contract:
       // Calculating roundId
IFlareSystemsManager flareSystemsManager = ContractRegistry
.getFlareSystemsManager();

uint32 roundId = flareSystemsManager.getCurrentVotingEpochId();
console.log("roundId: %s\n", Strings.toString(roundId));

This can be done within the existing broadcast or in a new one (as done in the demo repository for better code organization).

Wait for response

We wait for the round to finalize. This takes no more than 90 seconds.

You can check if the request was submitted successfully on the AttestationRequests page on the Flare Systems Explorer website. To check if the round has been finalized, go to Finalizations page.

If you want to learn more about how the FDC protocol works, check here.

Prepare proof request

We prepare the proof request in a similar manner as in the step Prepare the request, by string concatenation. We import two new variables from the .env file; the URL of a verifier server and the corresponding API key. note

scrip/fdcExample/AddressValidity.s.sol
string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
string memory apiKey = vm.envString("X_API_KEY");

Also, by repeatedly using the Foundry shortcode vm.readLine, we read the data, saved to a file in the previous step, to variables.

string memory votingRoundId = vm.readLine(filePath);
string memory requestBytes = vm.readLine(filePath);

The code is as follows.

scrip/fdcExample/AddressValidity.s.sol
contract RetrieveDataAndProof is Script {
using Surl for *;

function run() external {
string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
string memory apiKey = vm.envString("X_API_KEY");

string memory requestBytes = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_abiEncodedRequest",
".txt"
)
);
string memory votingRoundId = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_votingRoundId",
".txt"
)
);

console.log("votingRoundId: %s\n", votingRoundId);
console.log("requestBytes: %s\n", requestBytes);

string[] memory headers = Base.prepareHeaders(apiKey);
string memory body = string.concat(
'{"votingRoundId":',
votingRoundId,
',"requestBytes":"',
requestBytes,
'"}'
);
console.log("body: %s\n", body);
console.log(
"headers: %s",
string.concat("{", headers[0], ", ", headers[1]),
"}\n"
);


...
}
}

Post proof request to DA Layer

We post the proof request to a chosen DA Layer provider server also with the same code as we did in the previous step.

scrip/fdcExample/AddressValidity.s.sol
string memory url = string.concat(
daLayerUrl,
// "api/v0/fdc/get-proof-round-id-bytes"
"api/v1/fdc/proof-by-request-round-raw"
);
console.log("url: %s\n", url);

(, bytes memory data) = postAttestationRequest(url, headers, body);

Parsing the returned data requires the definition of an auxiliary struct.

scrip/fdcExample/Base.s.sol
struct ParsableProof {
bytes32 attestationType;
bytes32[] proofs;
bytes responseHex;
}

The field attestationType holds the UTF8 encoded hex string of the attestation type name, padded to 32 bytes. Thus, it should match the value of the attestationType parameter in the Prepare the request step. In our case, that value is 0x4164647265737356616c69646974790000000000000000000000000000000000.

The array proofs holds the Merkle proofs of our attestation request.

Lastly, responseHex is the ABI encoding of the chosen attestation type response struct. In this case, it is the IAddressValidity.Response struct. We retrieve this data as follows.

scrip/fdcExample/AddressValidity.s.sol
bytes memory dataJson = Base.parseData(data);
ParsableProof memory proof = abi.decode(dataJson, (ParsableProof));

IAddressValidity.Response memory proofResponse = abi.decode(
proof.responseHex,
(IAddressValidity.Response)
);

Verify proof

FDC optimizes on-chain storage costs by implementing a hybrid data verification system. Instead of storing complete datasets on-chain, it stores only Merkle proofs, while maintaining the actual data through trusted off-chain providers. This approach significantly reduces gas costs while preserving data integrity.

When requested, data providers supply the original data along with its corresponding Merkle proof. The protocol verifies data authenticity by comparing the provided Merkle proof against the on-chain Merkle root. A successful match confirms the data's integrity and authenticity within the FDC system.

While data verification is optional if you trust your data provider, FDC ensures transparency by making verification possible at any time. This capability is crucial for maintaining system integrity and allowing users to independently verify data when needed, particularly in production environments.

FDC provides verification functionality through the FdcVerification contract. To verify address validity, we first format our data using the IAddressValidity.Proof struct, which contains both the Merkle proof and the response data.

scrip/fdcExample/AddressValidity.s.sol
IAddressValidity.Proof memory _proof = IAddressValidity.Proof(
proof.proofs,
proofResponse
);

We then access the FdcVerification contract through the ContractRegistry, and feed it the proof. If we proof is valid, the function verifyAddressValidity will return true, otherwise false. As before, we wrap the whole thing into a broadcast environment, using the PRIVATE_KEY variable from our .env file.

scrip/fdcExample/AddressValidity.s.sol
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

bool isValid = ContractRegistry
.getFdcVerification()
.verifyAddressValidity(proof);
console.log("proof is valid: %s\n", StringsBase.toString(isValid));

vm.stopBroadcast();

Use the data

We will now define a simple contract, that will demonstrate how the data can be used onchain. The contract will receive an address and proof, and decide if the address is valid. If the address is valid, the contract will add it to an array of valid addresses. Otherwise, it will raise an error.

The code for this contract is as follows.

src/fdcExample/AddressValidity.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {console} from "dependencies/forge-std-1.9.5/src/console.sol";
import {Strings} from "@openzeppelin-contracts/utils/Strings.sol";
import {ContractRegistry} from "dependencies/flare-periphery-test-0.0.7/src/coston2/ContractRegistry.sol";
import {IFdcHub} from "dependencies/flare-periphery-test-0.0.7/src/coston2/IFdcHub.sol";
import {IAddressValidity} from "dependencies/flare-periphery-test-0.0.7/src/coston2/IAddressValidity.sol";
import {IFdcVerification} from "dependencies/flare-periphery-test-0.0.7/src/coston2/IFdcVerification.sol";
import {FdcStrings} from "src/utils/fdcStrings/AddressValidity.sol";

struct EventInfo {
address sender;
uint256 value;
bytes data;
}

contract AddressValidity {
string[] public verifiedAddresses;

function isAddressValidityProofValid(
IAddressValidity.Proof calldata transaction
) public view returns (bool) {
// Use the library to get the verifier contract and verify that this transaction was proved by the state connector
IFdcVerification fdc = ContractRegistry.getFdcVerification();
console.log("transaction: %s\n", FdcStrings.toJsonString(transaction));
// return true;
return fdc.verifyAddressValidity(transaction);
}

function registerAddress(
string calldata _addressStr,
IAddressValidity.Proof calldata _transaction
) external {
// 1. FDC Logic
// Check that this AddressValidity has indeed been confirmed by the FDC
require(
isAddressValidityProofValid(_transaction),
"Invalid transaction proof"
);

// 2. Business logic
string provedAddress = _transaction.data.requestBody.addressStr;
require(
Strings.equal(provedAddress, _addressStr),
string.concat(
"Invalid address.\n\tProvided: ",
_addressStr,
"\n\tProoved: ",
provedAddress
)
);
verifiedAddresses.push(provedAddress);
}
}

The function registerAddress takes as parameters a string representing an address, and a proof. If the proof is valid, and if the given address matches the one in the proof, the address is added to an array of verified addresses.

We deploy this contract through a script in script/AddressValidity.s.sol.

scrip/fdcExample/AddressValidity.s.sol

contract Deploy is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

AddressValidity addressValidity = new AddressValidity();

vm.stopBroadcast();
}
}

We run the above script with the following console command.

forge script script/fdcExample/AddressValidity.s.sol:Deploy --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast --ffi