Skip to content

Files

Latest commit

 

History

History

OP-attest-medical-reports-hub

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

Attestation Hub Contract for Medical Reports

The Attestation Hub smart contract contains a public attestations mapping that doctors, radioloigists or medical professionals can write to and read from. We are extending the AttestationStation smart contract [1] to develop the Attestation Hub smart contract for medical reports.

[1] AttestationStation Tutorial and OP Eco-system Repository at github: https://github.com/ethereum-optimism/optimism-tutorial/tree/main/ecosystem/attestation-station

Prerequisites

  • You have Node.js running on your computer, as well as yarn.
  • There is network connectivity to a provider on the Optimism Goerli network, and to the npm package registry.

Setup

  1. Use yarn to download the packages you need

    yarn
  2. Copy .env.example to .env and modify the parameters:

    • MNEMONIC is the mnemonic to an account that has enough ETH to pay for the transaction.

    • ALCHEMY_API_KEY is the API key for an Optimism Goerli app on Alchemy, our preferred provider.

    • OPTIMISM_GOERLI_URL is the URL for Optimism Goerli, if you use a different node provider.

  3. Enter the hardhat console:

    yarn hardhat console --network optimism-goerli
  4. Attach to the contract on the Optimism Goerli network:

    AttestationStation = await ethers.getContractFactory("AttestationStation")
    attestationStation = AttestationStation.attach("0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77")

Key values

Every attestation has these fields:

  • Creator address (who attested this)
  • Subject address (who is being attested about)
  • Key
  • Value

The first two are self-explanatory, and the value can be any number of bytes in whatever format is applicable. For the key we propose this convention:

  1. The raw key is a dot separated value, going from general to specific. For example,

    op.retropgf.szn-2.can-vote
    

    Means that the value attested is permission to vote in season 2 of the RetroPGF distribution of The Optimism Foundation.

    Note that there is no need for a central registry of top level names, such as op., because if different addresses attest the same key they do not affect each other.

  2. If the raw key is 31 characters long or less, just use it as is.

  3. If the raw key is longer than 31 characters, use a different key (one that has the least significant bit turned on so it won't appear to be a regular key). For example, the key can be a hash of the raw key, with the least significant byte replaced by 0xFF. To record the raw key, create another attestation with these value:

    Parameter Value
    key Encoded key
    about 0
    val Raw key

You can use this function to encode raw keys.

encodeRawKey = rawKey => {
   if (rawKey.length<32) 
      return ethers.utils.formatBytes32String(rawKey)

   const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(rawKey))
   return hash.slice(0,64)+'ff'
}

Write an attestation

  1. Create the attestation.

    goatAddr = '0x00000000000000000000000000000000000060A7'
    attendedKey = encodeRawKey("animalfarm.school.attended")
    attestation = {
        about: goatAddr,
        key: attendedKey,
        val: 1   // for true
    }
  2. Send the attestation. Note that attestationStation.attest accepts an array as parameter, so you'll be able to attest to many facts in a single transaction.

    tx = await attestationStation.attest([attestation])
    rcpt = await tx.wait()
  3. If you want to see the key, you can use the hash to find your transaction on Etherscan (or just use my transaction), click Click to see More, and then View Input As > UTF-8.

Read an attestation

To read an attestation you need to know three things:

  • Creator address (who attested this)
  • Subject address (who is being attested about)
  • Key
  1. Read the value for the attestation you just created.

    creatorAddr = (await ethers.getSigner()).address
    (await attestationStation.attestations(creatorAddr, goatAddr, attendedKey) != '0x')
  2. Check the attestation for a different address to see that the default is false

    notGoatAddr = '0x000000000000000000000000000000000000BEEF'
    (await attestationStation.attestations(creatorAddr, notGoatAddr, attendedKey) != '0x')
  3. Read an attestation created by a different user (this one is a grade, so it's a string)

    historyKey = encodeRawKey("animal-farm.school.grades.history")
    hex = await attestationStation.attestations('0xBCf86Fd70a0183433763ab0c14E7a760194f3a9F', goatAddr, historyKey)
    ethers.utils.toUtf8String(hex)

    Note: To create the attestation with an ascii value, and record the raw key to interpret it, I used this data structure:

    attestations = [
       {
          about: goatAddr, 
          key: historyKey, 
          val: ethers.utils.toUtf8Bytes("A+")
       },
       {
          about: '0x'.padEnd(42,'0'),
          key: historyKey,
          val: ethers.utils.toUtf8Bytes("animal-farm.school.grades.history")
       }
    ]

Read all relevant attestations

If you want to read all the attestations about a specific address, you need to look at the emitted AttestationCreated events.

You can do it using MSilb7's adapter for Flipside Crypto. You can also do it using any Optimism node using JavaScript (see below):

  1. Create a filter. You can filter based on any combination of:

    • Creator address (who attested this)
    • Subject address (who is being attested about)
    • Key
    • Value

    If any value should match the filter, use null for it.

    aboutGoat = attestationStation.filters.AttestationCreated(null,goatAddr,null,null)
  2. Get all the events.

    events = await attestationStation.queryFilter(aboutGoat)

Out of date information

One problem with using events is that they may contain out of date information. For example, look at our goat again, just at key and creator values:

events.map(x => [x.args.key, x.args.creator])

The results were (when I wrote this):

[
   [
      '0x616e696d616c6661726d2e7363686f6f6c2e617474656e646564000000000000',
      '0xBCf86Fd70a0183433763ab0c14E7a760194f3a9F'
   ],
   [
      '0x881a8d71a6dabe50856e9c9753e46aaa5c552185e26a834d9111472ebd494aff',
      '0xBCf86Fd70a0183433763ab0c14E7a760194f3a9F'
   ],
   [
      '0x616e696d616c6661726d2e7363686f6f6c2e617474656e646564000000000000',
      '0x8Ff966Ab0DadaDC70C901dD5cDc2C708d3A229AA'
   ],
   [
      '0x616e696d616c6661726d2e7363686f6f6c2e617474656e646564000000000000',
      '0xBCf86Fd70a0183433763ab0c14E7a760194f3a9F'
   ]
]   

We see that the same (key, creator) value is specified twice. This means two different attestations, and only the latest is still applicable. We can solve this with a function that only updates data only if it finds newer information.

  1. Create a key that includes the two fields we need to check for equality.

    event2key = e => `${e.args.key}-${e.args.creator}`
  2. Create a function that updates history unless it finds the history already includes newer info.

    update2Latest = (history, event) => {
       key = event2key(event)
       if ((history[key] == null) || (history[key].blockNumber < event.blockNumber)) {
          history[key] = event
          return history   // including this event
       }
       return history      // without this event
    } 
    1. Get the history and transform it back to a list of events.
    attestedHistory = events.reduce(update2Latest, {})
    relevantEvents = Object.keys(attestedHistory).map(key => attestedHistory[key])

Separating creator, signer, and transaction payer

In some circumstances it is useful for the attestation transactions to be signed by one address, attested by another, and paid for by a third one. For example, you might want attestations signed by a single EOA (externally owned account) right now, but with the freedom to upgrade to a multisig later while still attesting as the same creator. Or maybe you want users to pay for their own attestations.

You can achieve this using an AttestationProxy. An attestation proxy receives an attestation in a transaction (that could be sent by any account or smart contract), verifies that they are signed by the correct signer, and then attest them. Attestations are created by msg.sender, so if a smart contract calls attest the attestation's creator is the smart contract, not the transaction's origin.

Seeing it in action

Follow these steps on the console.

  1. Deploy AttestationProxy:

    AttestationProxy = await ethers.getContractFactory("AttestationProxy")
    attestationProxy = await AttestationProxy.deploy(attestationStation.address)
  2. Create an attestation and sign it.

    msgHash = ethers.utils.solidityKeccak256(
       ["address", "bytes32", "bytes"],
       [
             goatAddr,
             historyKey,
             ethers.utils.toUtf8Bytes("A+")
       ]
    )
    signer = await ethers.getSigner()
    sig = await signer.signMessage(ethers.utils.arrayify(msgHash))   
  3. Send the attestation to AttestationProxy. You are able to do it, because as the contract deployer you are the initial owner.

    tx = await attestationProxy.attest(goatAddr, historyKey, ethers.utils.toUtf8Bytes("A+"), sig)
    rcpt = await tx.wait()
  4. Make a note of the address of your attestation proxy.

    attestationProxy.address
  5. View the attestation station contract on Optimism Goerli to see the new attestation. Note that it is an internal transaction, because AttestationStation is called by a contract, AttestationProxy, rather than directly. Every transaction appears twice because 0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77 is not the "real" AttestationStation contract, it is a proxy to enable upgrades.

  6. Click the transaction hash and then the Logs tab to see that the creator of the attestation is indeed the proxy address.

  7. The mnemonic in .env can generate multiple signers. From this point, we will use a different signer to show how the signer and payer can be different. The first step is to get that signer.

    otherSigner = (await ethers.getSigners())[1]
  8. Change signing authority to otherSigner.

    tx = await attestationProxy.transferOwnership(otherSigner.address)
    rcpt = await tx.wait()
  9. Try to submit an attestation signed by yourself again, see that it now fails.

    tx = await attestationProxy.attest(goatAddr, historyKey, ethers.utils.toUtf8Bytes("A+"), sig)
  10. Get otherSigner's signature and resubmit the transaction. See that it is successful.

    sig = await otherSigner.signMessage(ethers.utils.arrayify(msgHash))
    tx = await attestationProxy.attest(goatAddr, historyKey, ethers.utils.toUtf8Bytes("A+"), sig)
    rcpt = await tx.wait()