Skip to content

Basic Attestation Request#

This tutorial shows basic use of the State Connector protocol.

In this tutorial, you will learn how to:

  • Make a query to the State Connector smart contract.
  • Get the result from an attestation provider.
  • Use a smart contract to verify that the result returned by the attestation provider matches the result agreed upon by the State Connector.

The diagram below shows the process that this tutorial follows. Arrows that match one of the steps in the tutorial are numbered.

State Connector usage process

State Connector usage process

Code#

For easy navigation, numbered comments in the source code (as in // 1.) link to the tutorial sections below.

AddressValidity.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
const FLARE_CONTRACTS = "@flarenetwork/flare-periphery-contract-artifacts";
const FLARE_RPC = "https://coston-api.flare.network/ext/C/rpc";
const ATTESTATION_PROVIDER_URL = "https://attestation-coston.aflabs.net";
const ATTESTATION_PROVIDER_API_KEY = "123456";
const FLARE_CONTRACT_REGISTRY_ADDR =
  "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019";

// You should get your private keys from an external source.
// DO NOT embed them in source code in a production environment!
const PRIVATE_KEY =
  "0x6607fc65548ffe231ce954018b3ee01fedb242281227e42a30a9bffa759557d7";

async function runAddressValidity(network, addressToValidate) {
  const VERIFICATION_ENDPOINT =
    `${ATTESTATION_PROVIDER_URL}/verifier/${network.toLowerCase()}` +
    `/AddressValidity/prepareRequest`;
  const ATTESTATION_ENDPOINT =
    `${ATTESTATION_PROVIDER_URL}/attestation-client/api/proof/` +
    `get-specific-proof`;

  // 1. Set up
  const ethers = await import("ethers");
  const flare = await import(FLARE_CONTRACTS);
  const utils = await import(
    `${FLARE_CONTRACTS}/dist/coston/StateConnector/libs/ts/utils.js`
  );
  const provider = new ethers.JsonRpcProvider(FLARE_RPC);
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);

  // 2. Prepare Attestation Request
  const { encodeAttestationName } = utils;
  const rawAttestationRequest = {
    attestationType: encodeAttestationName("AddressValidity"),
    sourceId: encodeAttestationName(`test${network.toUpperCase()}`),
    requestBody: {
      addressStr: addressToValidate,
    },
  };
  console.log(
    "Preparing attestation request using verifier",
    ATTESTATION_PROVIDER_URL,
    "..."
  );
  console.log("Request:", rawAttestationRequest);

  const verifierResponse = await fetch(VERIFICATION_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": ATTESTATION_PROVIDER_API_KEY,
    },
    body: JSON.stringify(rawAttestationRequest),
  });
  const encodedAttestationRequest = await verifierResponse.json();
  if (encodedAttestationRequest.status !== "VALID") {
    console.log("Received error:", encodedAttestationRequest);
    return;
  }
  console.log(
    "  Received encoded attestation request:",
    encodedAttestationRequest.abiEncodedRequest
  );

  // 3. Access Contract Registry
  const flareContractRegistry = new ethers.Contract(
    FLARE_CONTRACT_REGISTRY_ADDR,
    flare.nameToAbi("FlareContractRegistry", "coston").data,
    provider
  );

  // 4. Retrieve the State Connector Contract Address
  const stateConnectorAddress =
    await flareContractRegistry.getContractAddressByName("StateConnector");
  const stateConnector = new ethers.Contract(
    stateConnectorAddress,
    flare.nameToAbi("StateConnector", "coston").data,
    signer
  );

  // 5. Request Attestation from the State Connector Contract
  console.log("Submitting attestation to State Connector...");
  const attestationTx = await stateConnector.requestAttestations(
    encodedAttestationRequest.abiEncodedRequest
  );
  const receipt = await attestationTx.wait();
  const block = await provider.getBlock(receipt.blockNumber);

  // 6. Calculate Round ID
  const roundOffset = await stateConnector.BUFFER_TIMESTAMP_OFFSET();
  const roundDuration = await stateConnector.BUFFER_WINDOW();
  const submissionRoundID = Number(
    (BigInt(block.timestamp) - roundOffset) / roundDuration
  );

  console.log("  Attestation submitted in round", submissionRoundID);

  // 7. Wait for the Attestation Round to Finalize
  var prevFinalizedRoundID = 0;
  setTimeout(async function poll() {
    const lastFinalizedRoundID = Number(
      await stateConnector.lastFinalizedRoundId()
    );
    if (prevFinalizedRoundID != lastFinalizedRoundID) {
      console.log("  Last finalized round is", lastFinalizedRoundID);
      prevFinalizedRoundID = lastFinalizedRoundID;
    }
    if (lastFinalizedRoundID < submissionRoundID) {
      setTimeout(poll, 10000);
      return;
    }

    // 8. Retrieve Proof
    const proofRequest = {
      roundId: submissionRoundID,
      requestBytes: encodedAttestationRequest.abiEncodedRequest,
    };

    console.log("Retrieving proof from attestation provider...");
    const providerResponse = await fetch(ATTESTATION_ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-KEY": ATTESTATION_PROVIDER_API_KEY,
      },
      body: JSON.stringify(proofRequest),
    });
    const proof = await providerResponse.json();
    if (proof.status !== "OK") {
      console.log("Received error:", proof);
      return;
    }
    console.log("  Received Merkle proof:", proof.data.merkleProof);

    // 9. Send Proof to Verifier Contract
    // Unpacked attestation proof to be used in a Solidity contract.
    const fullProof = {
        merkleProof: proof.data.merkleProof,
        data: {
            ...proof.data,
            ...proof.data.request,
            ...proof.data.response,
            status: proof.status,
        }
    };

    const { isValid } = fullProof.data.responseBody;

    console.log("Sending the proof for verification...");
    const addressVerifier = new ethers.Contract(
      flare.nameToAddress("IAddressValidityVerification", "coston"),
      flare.nameToAbi("IAddressValidityVerification", "coston").data,
      signer
    );
    const isVerified =
      await addressVerifier.verifyAddressValidity(fullProof);
    console.log("  Attestation result:", isVerified);

    // 10. Check if Address is Valid
    if (isVerified) {
      console.log(
        isValid
          ? "Attestation providers agree that the address is valid."
          : "Attestation providers agree that the address is invalid."
      );
    } else {
      console.log(
        "Could not verify attestation. Validity of address is unknown."
      );
    }
  }, 10000);
}

runAddressValidity(
  "btc",
  "tb1p4mdyx3dvgk4dhvv8yv2dtuymf00wxhgkkjheqm7526fu7znnd6msw3qxvj"
);

Source code license.

Run with Node.js

This tutorial has been tested with npm v9.5 and Node.js v18.16.

  1. Create a new folder and move into it.
  2. Copy & paste the code above into a new file called AddressValidity.js.
  3. Initialize project and install dependencies with:
    npm init
    npm install ethers@6.3 @flarenetwork/flare-periphery-contract-artifacts@0.1.7
    
  4. Run the program with:
    node AddressValidity.js
    

Tutorial#

1. Set up#

The tutorial uses the following dependencies:

22
23
24
25
26
  const ethers = await import("ethers");
  const flare = await import(FLARE_CONTRACTS);
  const utils = await import(
    `${FLARE_CONTRACTS}/dist/coston/StateConnector/libs/ts/utils.js`
  );

The periphery package significantly simplifies working with the Flare smart contracts. If you remove this dependency, you must manually provide the signatures for all the methods you want to use.

This tutorial needs to send transactions on the Coston test network, so you will need an account with enough $CFLR tokens to pay for gas. The Getting Started guides explain how to configure your wallet and get test tokens from the faucet.

Warning

For simplicity, this tutorial hard-codes the private key of the wallet being used in the PRIVATE_KEY variable.

10
11
const PRIVATE_KEY =
  "0x6607fc65548ffe231ce954018b3ee01fedb242281227e42a30a9bffa759557d7";

In a production setting, the private key should be retrieved from an external source (such as a .env file) and NOT embedded directly in the code.

2. Prepare Attestation Request#

Requests to the State Connector must be extremely specific. For example, to request a proof that a given transaction exists, the transaction ID alone is not enough. The block number and block timestamp where the transaction was included must also be provided. Furthermore, requests must be encoded into a hex string before being submitted to the State Connector.

You can perform all these operations yourself, but, as a convenience, attestation providers can prepare requests for you, filling in all missing information and taking care of formatting.

The attestation type chosen for this tutorial, AddressValidity, is the simplest one and requires only the address to be validated. However, it is still a good example of the process.

To prepare a request using an Attestation Provider, begin with a raw attestation request:

31
32
33
34
35
36
37
38
  const { encodeAttestationName } = utils;
  const rawAttestationRequest = {
    attestationType: encodeAttestationName("AddressValidity"),
    sourceId: encodeAttestationName(`test${network.toUpperCase()}`),
    requestBody: {
      addressStr: addressToValidate,
    },
  };

The raw attestation request contains:

  • attestationType: A unique identifier for the type of attestation you want, which is just an encoded version of its name. See the list of the currently available attestation types.
  • sourceId: The network on which you want to make the request. Available networks depend on the attestation type and are listed in the documentation of each one. This example uses the Coston test network, so network names are prepended with test.
  • requestBody: A JSON object specific to each attestation type. In this example, it is just the address that you want to validate.

Then obtain an encoded attestation request:

46
47
48
49
50
51
52
53
54
  const verifierResponse = await fetch(VERIFICATION_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": ATTESTATION_PROVIDER_API_KEY,
    },
    body: JSON.stringify(rawAttestationRequest),
  });
  const encodedAttestationRequest = await verifierResponse.json();

This code performs a POST request to the prepareRequest endpoint of the attestation provider, using the standard fetch API.

ATTESTATION_PROVIDER_API_KEY is the API key of the chosen attestation provider, if it needs one. Attestation providers are typically paid services and require an API key to operate.

Finally, encodedAttestationRequest is the returned encoded request ready to be submitted to the State Connector.

3. Access Contract Registry#

The FlareContractRegistry contains the current addresses for all Flare smart contracts, and it is the only recommended way to retrieve them.

Its address is the same on all of Flare's networks, and it is the only Flare address that needs to be hard-coded into any program.

5
6
const FLARE_CONTRACT_REGISTRY_ADDR =
  "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019";
65
66
67
68
69
  const flareContractRegistry = new ethers.Contract(
    FLARE_CONTRACT_REGISTRY_ADDR,
    flare.nameToAbi("FlareContractRegistry", "coston").data,
    provider
  );

Note that this tutorial uses the Coston test network here rather than the main Flare Network.

4. Retrieve the State Connector Contract Address#

Retrieve the State Connector's address from the FlareContractRegistry.

72
73
74
75
76
77
78
  const stateConnectorAddress =
    await flareContractRegistry.getContractAddressByName("StateConnector");
  const stateConnector = new ethers.Contract(
    stateConnectorAddress,
    flare.nameToAbi("StateConnector", "coston").data,
    signer
  );

Use the getContractAddressByName() method from the FlareContractRegistry smart contract to retrieve the address of the StateConnector smart contract.

5. Request Attestation from the State Connector Contract#

Now, request an attestation from the State Connector contract by sending the encoded attestation request from step 2.

Use the requestAttestations() method from the StateConnector smart contract.

82
83
84
85
86
  const attestationTx = await stateConnector.requestAttestations(
    encodedAttestationRequest.abiEncodedRequest
  );
  const receipt = await attestationTx.wait();
  const block = await provider.getBlock(receipt.blockNumber);

attestationTx contains the TransactionResponse. After you wait on it and the transaction is added to the blockchain, you obtain a TransactionReceipt.

From this receipt you finally retrieve the block that includes the request transaction. This block is needed in the next step.

6. Calculate Round ID#

In order to recover the attestation result when it becomes available, you will need the round ID where the request was submitted. This is calculated from the block timestamp:

89
90
91
92
93
  const roundOffset = await stateConnector.BUFFER_TIMESTAMP_OFFSET();
  const roundDuration = await stateConnector.BUFFER_WINDOW();
  const submissionRoundID = Number(
    (BigInt(block.timestamp) - roundOffset) / roundDuration
  );

Attestation rounds last roundDuration seconds, starting roundOffset seconds after the Unix epoch. You will use submissionRoundID later.

Tip

To optimize performance, consider caching the roundOffset and roundDuration values instead of retrieving them from the blockchain every time.

7. Wait for the Attestation Round to Finalize#

You need to wait for the attestation round to finalize, because results are only available after finalization.

 98
 99
100
101
102
103
104
105
106
107
108
109
110
  var prevFinalizedRoundID = 0;
  setTimeout(async function poll() {
    const lastFinalizedRoundID = Number(
      await stateConnector.lastFinalizedRoundId()
    );
    if (prevFinalizedRoundID != lastFinalizedRoundID) {
      console.log("  Last finalized round is", lastFinalizedRoundID);
      prevFinalizedRoundID = lastFinalizedRoundID;
    }
    if (lastFinalizedRoundID < submissionRoundID) {
      setTimeout(poll, 10000);
      return;
    }

Attestation rounds use the Collect-Choose-Commit-Reveal (CCCR) protocol, which requires 270 - 360 seconds, depending on attestation provider's submissions, and the point inside the Collect phase in which the request was submitted.

For this reason, this tutorial polls the State Connector's last finalized round (StateConnector.lastFinalizedRoundID) every 10 seconds so that results are obtained as soon as they are available.

Polling vs Waiting

Polling every 10 seconds is a good tradeoff to minimize the waiting time, but you can choose to always wait the maximum amount of time (360s), or even wait the minimum amount (270s), and then poll.

Proof Accessibility Window

Proofs are kept on-chain for just a week. After this period, the proofs will no longer be accessible, so timely verification is crucial.

8. Retrieve Proof#

It is time now to fetch from the attestation provider the result for the round ID where the request was submitted (submissionRoundID).

The result is a Merkle root, which is the root of a Merkle tree. This tree has been constructed with the hashes of all the requests received during that round that were considered valid by a majority of attestation providers.

If your request was valid, i.e., if the provided address was a valid Bitcoin address, then its hash will be part of the received Merkle root.

113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
    const proofRequest = {
      roundId: submissionRoundID,
      requestBytes: encodedAttestationRequest.abiEncodedRequest,
    };

    console.log("Retrieving proof from attestation provider...");
    const providerResponse = await fetch(ATTESTATION_ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-KEY": ATTESTATION_PROVIDER_API_KEY,
      },
      body: JSON.stringify(proofRequest),
    });
    const proof = await providerResponse.json();

You need to construct a proofRequest and make a POST request to the get-specific-proof endpoint of the attestation provider.

Doing so returns, among other things, a Merkle proof consisting of one or more nodes (hashes). If the Merkle tree is rebuilt using these nodes plus the hash of your request, and the resulting root hash matches the agreed-upon value stored in the State Connector, it means that the proof can be trusted.

You can perform these operations yourself or you can use a verifier smart contract, as shown in the next step.

The received proof already contains a field, proof.data.responseBody.isValid, which indicates whether this particular attestation provider believes the queried address to be valid or not. But this result cannot be trusted until you verify that it matches what the rest of attestation providers submitted, as explained next.

9. Send Proof to Verifier Contract#

Send the proof to the AddressValidityVerification smart contract. This smart contract verifies the request by rebuilding the Merkle root using the hashes contained in the proof.data.merkleProof object and comparing it to the Merkle root stored in the State Connector.

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
    const fullProof = {
        merkleProof: proof.data.merkleProof,
        data: {
            ...proof.data,
            ...proof.data.request,
            ...proof.data.response,
            status: proof.status,
        }
    };

    const { isValid } = fullProof.data.responseBody;

    console.log("Sending the proof for verification...");
    const addressVerifier = new ethers.Contract(
      flare.nameToAddress("IAddressValidityVerification", "coston"),
      flare.nameToAbi("IAddressValidityVerification", "coston").data,
      signer
    );
    const isVerified =
      await addressVerifier.verifyAddressValidity(fullProof);

isVerified contains the result of the verification, analyzed next.

Note

This tutorial uses a verification contract provided by Flare, but dapps can embed the same logic into their own smart contracts if they wish to.

10. Check if the Address is Valid#

Finally, check if the address is valid or invalid according to the attestation providers, but only if the attestation has been verified.

isVerified indicates whether the attestation you received from the attestation provider matches what the majority of them agreed upon. If the value is false, you do not need to look further because the attestation provider is probably lying and its results cannot be trusted. In this case, you need to make the request again, ideally through a different provider.

If isVerified is true, then you can look at the actual result of your request in the isValid field of fullProof.data.responseBody obtained in step 8. If this value is true too, then the queried address is valid.

159
160
161
162
163
164
165
166
167
168
169
    if (isVerified) {
      console.log(
        isValid
          ? "Attestation providers agree that the address is valid."
          : "Attestation providers agree that the address is invalid."
      );
    } else {
      console.log(
        "Could not verify attestation. Validity of address is unknown."
      );
    }

Conclusion#

This tutorial has shown how to:

  • Prepare a State Connector request using the prepareRequest REST endpoint of an attestation provider.
  • Make a request to the State Connector smart contract using requestAttestations().
  • Get the result from an attestation provider by making a POST request to the get-specific-proof REST endpoint.
  • Use the AddressValidityVerification smart contract to verify that the result returned by the attestation provider matches the result agreed upon by the State Connector.

Next Steps#

The State Connector can be used for a host of other things beyond just verifying address correctness. The attestation type of the request selects the type of information you want.

33
    attestationType: encodeAttestationName("AddressValidity"),

Other attestation types include:

  • Payment: Verifies whether a payment transaction occurred in which funds were sent from one address to another address.
  • Balance-decreasing transaction: Verifies whether a transaction that might have decreased a balance occurred.
  • Referenced payment nonexistence: Verifies whether an account did not receive funds from a different account by a specific deadline.
  • Confirmed block height: Verifies whether a block on a certain height exists and was confirmed.

More attestation types are to be added in the future, subject to community approval and support.

Also, recall that the AddressValidity attestation type used in this tutorial is the simplest one to use as it does not require accessing any connected networks. Other attestation types do, however, and so they tend to be more complex to use.

See the state connector attestation types for more information on other attestation types.