Skip to content

Commit

Permalink
feat(src): refactor into files, make level and unit CLI args
Browse files Browse the repository at this point in the history
feat(package): update package json and lockfile, lint
  • Loading branch information
jmcook1186 committed Oct 17, 2024
1 parent 632ffd9 commit fbfa6c5
Show file tree
Hide file tree
Showing 11 changed files with 661 additions and 382 deletions.
628 changes: 434 additions & 194 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"dependencies": {
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@ethereum-attestation-service/eas-sdk": "^2.3.0",
"@ethereum-attestation-service/eas-sdk": "^2.6.1",
"@grnsft/if-core": "^0.0.25",
"axios": "^1.7.2",
"csv-parse": "^5.5.6",
Expand Down
12 changes: 10 additions & 2 deletions src/if-attest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ Attestations are just snippets of data that conform to some predefined schema an

We are using the [Ethereum Attestation Service](https://attest.org/) to bootstrap our attestations. They have deployed the attestation smart contracts on Ethereum and many other associated blockchains, such as layer 2's and testnets. Layer 2's are cheaper, faster blockchains that settle to the main Ethereum blockchain periodically. Testnets are blockchains that use tokens of no real world value and are typically used to test smart contracts befor they are deployed on a "real" network.

Here's an example of a raw attestation. If you run `if-attest` configured to create local attestations, you'll get a text file that looks like this:

```
{"sig":{"version":2,"uid":"0x047d38b6d175fe8a36a597863b7d8d94939aa2fd4b4f19831229c95f5eda5604","domain":{"name":"EAS Attestation","version":"0.26","chainId":"11155111","verifyingContract":"0xC2679fBD37d54388Ce493F1DB75320D236e1815e"},"primaryType":"Attest","message":{"version":2,"recipient":"0xc8317137B5c511ef9CE1762CE498FE16950EF42d","expirationTime":"0","time":"1729173300","revocable":true,"schema":"0x11fdca810433efc2d5b9fe8305b39669e8d0feb81f699a767fe48ce26fcf6a6c","refUID":"0x0000000000000000000000000000000000000000000000000000000000000000","data":"0x000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a020d034e94040bf5b40ff76f9568155cacbc7ed0d488a8fcea8dc37bd24b0d0dd00000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000010323032332d30382d30365430303a3030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010323032332d30382d30365430303a3030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005302e372e30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b736974652d766973697473000000000000000000000000000000000000000000","salt":"0x0951f85ad71286a55562608cfba8cad023820a78cce8fd5df2a1498af0812505"},"types":{"Attest":[{"name":"version","type":"uint16"},{"name":"schema","type":"bytes32"},{"name":"recipient","type":"address"},{"name":"time","type":"uint64"},{"name":"expirationTime","type":"uint64"},{"name":"revocable","type":"bool"},{"name":"refUID","type":"bytes32"},{"name":"data","type":"bytes"},{"name":"salt","type":"bytes32"}]},"signature":{"v":27,"r":"0xea4ac79da011dd364b69cfb45cce6e9b5d71444861d0bb44456d7e50f433f981","s":"0x4106232e9af816c465657e10d39612c958f5b6f552d637c9cff5469ee7634333"}}, "signer":"0xc8317137B5c511ef9CE1762CE498FE16950EF42d"}
```

OK, it's not super human readable. This is because the manifest data is hex-encoded. Everything you need to verify the signatuire and recover the manifest summary data is here.


## Onchain vs offchain attestations

Expand Down Expand Up @@ -143,11 +151,11 @@ SCHEMA_UID: '0x9f074eced91e2c6952fdf5734ec1d8cc05e1e1d07eaa442f07746b7d8a422c0e'
For an onchain attestation

```
npm run if-attest -- -manifest ./manifests/outputs/example.yaml --blockchain true
npm run if-attest -- -manifest ./manifests/outputs/example.yaml --blockchain true --level 3 --unit site-visits
```

For an offchain attestation

```
npm run if-attest -- --manifest ./manifests/outputs/example.yaml --blockchain false
npm run if-attest -- --manifest ./manifests/outputs/example.yaml --blockchain false --level 3 --unit site-visits
```
12 changes: 12 additions & 0 deletions src/if-attest/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ export const CONFIG = {
description:
'[Boolean to toggle posting attestation to blockchain (true to post, false to save locally)]',
},
level: {
type: Number,
optional: true,
alias: 'l',
description: '[Audit level being attested to - integers from 1-5]',
},
unit: {
type: String,
optional: true,
alias: 'u',
description: '[The functional unit used to calculate SCI]',
},
} as ArgumentConfig<IFAttestArgs>,
HELP: {
helpArg: 'help',
Expand Down
200 changes: 18 additions & 182 deletions src/if-attest/index.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,38 @@
#!/usr/bin/env node
/* eslint-disable no-process-exit */
import {ethers, Wallet} from 'ethers';
import {ManifestInfo} from './types/types';
import * as YAML from 'js-yaml';
import {
EAS,
SchemaEncoder,
SignedOffchainAttestation,
// OffchainAttestationVersion,
} from '@ethereum-attestation-service/eas-sdk';
import {execPromise} from '../common/util/helpers';
import {openYamlFileAsObject} from '../common/util/yaml';
import {Manifest} from '../common/types/manifest';
import {EAS} from '@ethereum-attestation-service/eas-sdk';
import {logger} from '../common/util/logger';
import {parseIfAttestArgs} from './util/args';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import {SCHEMA} from './util/schema';

const packageJson = require('../../package.json');
dotenv.config();
import {createSigningWallet} from './util/ethereum-utils';
import {
addSignerInfoToAttestation,
createOffchainAttestaton,
} from './util/offchain-attestation-utils';
import {
sendAttestationTx,
encodeSchema,
getManifestInfo,
} from './util/attestation-utils';

const EAS_CONTRACT_ADDRESS_SEPOLIA: string =
process.env.EAS_CONTRACT_ADDRESS_SEPOLIA ?? '';
const UID = process.env.SCHEMA_UID ?? '';
const PRIVATE_KEY: string = process.env.ETH_PRIVATE_KEY ?? '';
const INFURA_API_KEY: string = process.env.INFURA_API_KEY ?? '';

const createSigningWallet = (): Wallet => {
const provider = new ethers.JsonRpcProvider(
`https://sepolia.infura.io/v3/${INFURA_API_KEY}`
);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
return signer;
};

const IfAttest = async () => {
console.debug('starting attestation');
const commandArgs = await parseIfAttestArgs();

// initialize command args
const manifestPath = commandArgs.manifest;
const level = commandArgs.level;
const functionalUnit = commandArgs.unit;

console.debug('creating Ethereum account');
console.debug('initializing Ethereum account');
const signer = createSigningWallet();
const eas = new EAS(EAS_CONTRACT_ADDRESS_SEPOLIA);
eas.connect(signer);

//todo: make level and functional-unit CLI args
const level = 1;
const functionalUnit = 'site-visits';
console.debug('creating signer object');
eas.connect(signer);

const manifestInfo = await getManifestInfo(
manifestPath,
Expand Down Expand Up @@ -81,155 +66,6 @@ const IfAttest = async () => {
}
};

const createOffchainAttestaton = async (
eas: EAS,
signer: Wallet,
encodedData: string
): Promise<SignedOffchainAttestation> => {
const offchain = await eas.getOffchain();

const attestation = await offchain.signOffchainAttestation(
{
recipient: signer.address, //can provide an ethereum address for the attested org if needed- here it's the signer address
expirationTime: BigInt(0),
time: BigInt(Math.floor(Date.now() / 1000)),
revocable: true, // Be aware that if your schema is not revocable, this MUST be false
schema: UID,
refUID:
'0x0000000000000000000000000000000000000000000000000000000000000000',
data: encodedData,
},
signer,
{
verifyOnchain: false,
}
);
return attestation;
};

const addSignerInfoToAttestation = (
attestation: SignedOffchainAttestation,
signer: Wallet
): string => {
const prefix = '{"sig":';
const suffix = `, "signer":${JSON.stringify(signer.address)}}`;
return (
prefix +
JSON.stringify(attestation, (_, v) =>
typeof v === 'bigint' ? v.toString() : v
) +
suffix
);
};

const sendAttestationTx = async (
eas: EAS,
signer: Wallet,
encodedData: string
): Promise<string> => {
const tx = await eas.attest({
schema: UID,
data: {
recipient: signer.address, //can provide an ethereum address for the attested org if needed- here it's the signer address
expirationTime: BigInt(0),
revocable: true, // Be aware that if your schema is not revocable, this MUST be false
data: encodedData,
},
});
const attestationUID = await tx.wait();
return attestationUID;
};

const encodeSchema = (manifestInfo: ManifestInfo) => {
const schemaEncoder = new SchemaEncoder(SCHEMA);
const encodedData = schemaEncoder.encodeData([
{name: 'start', value: manifestInfo.start, type: 'string'},
{name: 'end', value: manifestInfo.end, type: 'string'},
{name: 'hash', value: manifestInfo.hash, type: 'bytes32'},
{name: 'if', value: manifestInfo.if, type: 'string'},
{name: 'verified', value: manifestInfo.verified, type: 'bool'},
{name: 'sci', value: manifestInfo.sci, type: 'uint64'},
{name: 'energy', value: manifestInfo.energy, type: 'uint64'},
{name: 'carbon', value: manifestInfo.carbon, type: 'uint64'},
{name: 'level', value: manifestInfo.level, type: 'uint8'},
{name: 'quality', value: manifestInfo.quality, type: 'uint8'},
{
name: 'functionalUnit',
value: manifestInfo.functionalUnit,
type: 'string',
},
]);
return encodedData;
};

const getManifestStart = (manifest: Manifest): string => {
const firstChildName = Object.keys(manifest.tree.children)[0] ?? 0;
const manifestStart =
manifest.tree.children[`${firstChildName}`].inputs[0].timestamp ?? 0;
return manifestStart;
};

const getManifestEnd = (manifest: Manifest): string => {
const firstChildName = Object.keys(manifest.tree.children)[0];
const inputsLength =
manifest.tree.children[`${firstChildName}`].inputs.length ?? '';
const manifestEnd =
manifest.tree.children[`${firstChildName}`].inputs[inputsLength - 1]
.timestamp ?? '';
return manifestEnd;
};

const getManifestInfo = async (
manifestPath: string,
level: number,
functionalUnit: string
): Promise<ManifestInfo> => {
const manifest = await openYamlFileAsObject<Manifest>(manifestPath);

// const functionalUnitStub = file.initialize.plugins.sci['global-config']['functional-unit'] ?? '';

const info: ManifestInfo = {
start: getManifestStart(manifest),
end: getManifestEnd(manifest),
hash: GetManifestHash(manifest),
if: GetIfVersion(),
verified: await runIfCheck(manifestPath),
sci: manifest.tree.aggregated.sci ?? 0,
energy: manifest.tree.aggregated.energy ?? 0,
carbon: manifest.tree.aggregated.carbon ?? 0,
level: level,
quality: 1, // quality score not yet functional in IF,
functionalUnit: functionalUnit,
};

return info;
};

const GetManifestHash = (manifest: Manifest): string => {
const manifestAsString = YAML.dump(manifest).toString();
const manifestAsBytes: Uint8Array = ethers.toUtf8Bytes(manifestAsString);
const manifestHash = ethers.keccak256(manifestAsBytes);
return manifestHash;
};

const GetIfVersion = (): string => {
return packageJson.version;
};

const runIfCheck = async (manifestPath: string): Promise<boolean> => {
const response = await execPromise(`npm run if-check -- -m ${manifestPath}`, {
cwd: process.env.CURRENT_DIR || process.cwd(),
});

if (response.stdout.includes('if-check could not verify the manifest')) {
console.log('IF-CHECK: verification was unsuccessful. Files do not match');
return false;
}
console.log('IF-CHECK: verification was successful');

return true;
};

IfAttest().catch(error => {
if (error instanceof Error) {
logger.error(error);
Expand Down
2 changes: 2 additions & 0 deletions src/if-attest/types/process-args.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface IFAttestArgs {
manifest: string;
blockchain?: boolean;
level: number;
unit: string;
}
4 changes: 2 additions & 2 deletions src/if-attest/util/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const validateAndParseIfAttestArgs = () => {
* Checks if the `manifests` command is provided and they are valid manifests files or a folder.
*/
export const parseIfAttestArgs = async () => {
const {manifest, blockchain} = validateAndParseIfAttestArgs();
const {manifest, blockchain, level, unit} = validateAndParseIfAttestArgs();

const response = prependFullFilePath(manifest);
const isManifestFileExists = await isFileExists(response);
Expand All @@ -49,5 +49,5 @@ export const parseIfAttestArgs = async () => {
throw new CliSourceFileError(MANIFEST_IS_NOT_YAML(manifest));
}

return {manifest, blockchain};
return {manifest, blockchain, level, unit};
};
Loading

0 comments on commit fbfa6c5

Please sign in to comment.