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
- 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.
-
Use
yarn
to download the packages you needyarn
-
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.
-
-
Enter the hardhat console:
yarn hardhat console --network optimism-goerli
-
Attach to the contract on the Optimism Goerli network:
AttestationStation = await ethers.getContractFactory("AttestationStation") attestationStation = AttestationStation.attach("0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77")
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:
-
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. -
If the raw key is 31 characters long or less, just use it as is.
-
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'
}
-
Create the attestation.
goatAddr = '0x00000000000000000000000000000000000060A7' attendedKey = encodeRawKey("animalfarm.school.attended") attestation = { about: goatAddr, key: attendedKey, val: 1 // for true }
-
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()
-
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.
To read an attestation you need to know three things:
- Creator address (who attested this)
- Subject address (who is being attested about)
- Key
-
Read the value for the attestation you just created.
creatorAddr = (await ethers.getSigner()).address (await attestationStation.attestations(creatorAddr, goatAddr, attendedKey) != '0x')
-
Check the attestation for a different address to see that the default is false
notGoatAddr = '0x000000000000000000000000000000000000BEEF' (await attestationStation.attestations(creatorAddr, notGoatAddr, attendedKey) != '0x')
-
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") } ]
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):
-
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)
-
Get all the events.
events = await attestationStation.queryFilter(aboutGoat)
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.
-
Create a key that includes the two fields we need to check for equality.
event2key = e => `${e.args.key}-${e.args.creator}`
-
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 }
- Get the history and transform it back to a list of events.
attestedHistory = events.reduce(update2Latest, {}) relevantEvents = Object.keys(attestedHistory).map(key => attestedHistory[key])
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.
Follow these steps on the console.
-
Deploy
AttestationProxy
:AttestationProxy = await ethers.getContractFactory("AttestationProxy") attestationProxy = await AttestationProxy.deploy(attestationStation.address)
-
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))
-
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()
-
Make a note of the address of your attestation proxy.
attestationProxy.address
-
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 because0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77
is not the "real" AttestationStation contract, it is a proxy to enable upgrades. -
Click the transaction hash and then the Logs tab to see that the creator of the attestation is indeed the proxy address.
-
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]
-
Change signing authority to
otherSigner
.tx = await attestationProxy.transferOwnership(otherSigner.address) rcpt = await tx.wait()
-
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)
-
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()