client now verifies hash and signature on api read
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 39s Details

This commit is contained in:
Ladd Hoffman 2024-03-20 20:00:56 -05:00
parent 956aa9728e
commit e6185fb89f
16 changed files with 322 additions and 63 deletions

View File

@ -3,9 +3,8 @@ const { Level } = require('level');
const { recoverPersonalSignature } = require('@metamask/eth-sig-util');
// const { ecrecover, fromRpcSig, pubToAddress } = require('@ethereumjs/util');
// const { Keccak } = require('sha3');
const {
createHash,
} = require('node:crypto');
const objectHash = require('object-hash');
// const { createHash } = require('node:crypto');
require('dotenv').config();
@ -36,7 +35,8 @@ app.post('/write', async (req, res) => {
}
// Compute content hash
const data = { author, content, signature };
const hash = createHash('sha256').update(JSON.stringify(data)).digest('base64url');
const hash = objectHash(data);
console.log('write', hash);
// Store content
db.put(hash, data);

View File

@ -13,7 +13,8 @@
"axios": "^1.6.7",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"level": "^8.0.1"
"level": "^8.0.1",
"object-hash": "^3.0.0"
},
"devDependencies": {
"eslint": "^8.56.0",
@ -2933,6 +2934,14 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",

View File

@ -13,7 +13,8 @@
"axios": "^1.6.7",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"level": "^8.0.1"
"level": "^8.0.1",
"object-hash": "^3.0.0"
},
"devDependencies": {
"eslint": "^8.56.0",

179
client/package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@helia/dag-json": "^3.0.2",
"@libp2p/websockets": "^8.0.16",
"@metamask/eth-sig-util": "^7.0.1",
"@metamask/sdk-react": "^0.16.0",
"@multiformats/multiaddr": "^12.2.1",
"@tanstack/react-table": "^8.13.2",
@ -17,8 +18,10 @@
"bootstrap": "^5.3.3",
"bootswatch": "^5.3.3",
"buffer": "^6.0.3",
"create-hash": "^1.2.0",
"helia": "^4.1.0",
"ipfs-core": "^0.18.1",
"object-hash": "^3.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.1",
@ -5325,6 +5328,156 @@
"uint8arrays": "^5.0.2"
}
},
"node_modules/@metamask/abi-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@metamask/abi-utils/-/abi-utils-2.0.2.tgz",
"integrity": "sha512-B/A1dY/w4F/t6cDHUscklO6ovb/ztFsrsTXFd8QlqSByk/vyy+QbPE3VVpmmyI/7RX+PA1AJcvBdzCIz+r9dVQ==",
"dependencies": {
"@metamask/utils": "^8.0.0",
"superstruct": "^1.0.3"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@metamask/abi-utils/node_modules/@metamask/utils": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.4.0.tgz",
"integrity": "sha512-dbIc3C7alOe0agCuBHM1h71UaEaEqOk2W8rAtEn8QGz4haH2Qq7MoK6i7v2guzvkJVVh79c+QCzIqphC3KvrJg==",
"dependencies": {
"@ethereumjs/tx": "^4.2.0",
"@noble/hashes": "^1.3.1",
"@scure/base": "^1.1.3",
"@types/debug": "^4.1.7",
"debug": "^4.3.4",
"pony-cause": "^2.1.10",
"semver": "^7.5.4",
"superstruct": "^1.0.3",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@metamask/abi-utils/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@metamask/abi-utils/node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@metamask/abi-utils/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@metamask/abi-utils/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@metamask/eth-sig-util": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-7.0.1.tgz",
"integrity": "sha512-59GSrMyFH2fPfu7nKeIQdZ150zxXNNhAQIUaFRUW+MGtVA4w/ONbiQobcRBLi+jQProfIyss51G8pfLPcQ0ylg==",
"dependencies": {
"@ethereumjs/util": "^8.1.0",
"@metamask/abi-utils": "^2.0.2",
"@metamask/utils": "^8.1.0",
"ethereum-cryptography": "^2.1.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
},
"engines": {
"node": "^16.20 || ^18.16 || >=20"
}
},
"node_modules/@metamask/eth-sig-util/node_modules/@metamask/utils": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.4.0.tgz",
"integrity": "sha512-dbIc3C7alOe0agCuBHM1h71UaEaEqOk2W8rAtEn8QGz4haH2Qq7MoK6i7v2guzvkJVVh79c+QCzIqphC3KvrJg==",
"dependencies": {
"@ethereumjs/tx": "^4.2.0",
"@noble/hashes": "^1.3.1",
"@scure/base": "^1.1.3",
"@types/debug": "^4.1.7",
"debug": "^4.3.4",
"pony-cause": "^2.1.10",
"semver": "^7.5.4",
"superstruct": "^1.0.3",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@metamask/eth-sig-util/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@metamask/eth-sig-util/node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@metamask/eth-sig-util/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@metamask/eth-sig-util/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@metamask/object-multiplex": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-1.3.0.tgz",
@ -20180,6 +20333,14 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@ -20895,6 +21056,14 @@
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"node_modules/pony-cause": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.10.tgz",
"integrity": "sha512-3IKLNXclQgkU++2fSi93sQ6BznFuxSLB11HdvZQ6JW/spahf/P1pAHBQEahr20rs0htZW0UDkM1HmA+nZkXKsw==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@ -23275,6 +23444,16 @@
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
},
"node_modules/tweetnacl-util": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -12,6 +12,7 @@
"dependencies": {
"@helia/dag-json": "^3.0.2",
"@libp2p/websockets": "^8.0.16",
"@metamask/eth-sig-util": "^7.0.1",
"@metamask/sdk-react": "^0.16.0",
"@multiformats/multiaddr": "^12.2.1",
"@tanstack/react-table": "^8.13.2",
@ -19,8 +20,10 @@
"bootstrap": "^5.3.3",
"bootswatch": "^5.3.3",
"buffer": "^6.0.3",
"create-hash": "^1.2.0",
"helia": "^4.1.0",
"ipfs-core": "^0.18.1",
"object-hash": "^3.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.1",

View File

@ -3,7 +3,6 @@ import {
} from 'react';
import { useSDK } from '@metamask/sdk-react';
import { Web3 } from 'web3';
import axios from 'axios';
import Button from 'react-bootstrap/Button';
import Tab from 'react-bootstrap/Tab';
@ -21,9 +20,10 @@ import Web3Context from './contexts/Web3Context';
import DAOArtifact from './assets/DAO.json';
import Work1Artifact from './assets/Work1.json';
import OnboardingArtifact from './assets/Onboarding.json';
import WorkContract from './components/WorkContract';
import AddPostModal from './components/AddPostModal';
import ViewPostModal from './components/ViewPostModal';
import WorkContract from './components/work-contracts/WorkContract';
import AddPostModal from './components/posts/AddPostModal';
import ViewPostModal from './components/posts/ViewPostModal';
import Post from './utils/Post';
function App() {
const {
@ -252,11 +252,8 @@ function App() {
const handleShowAddPost = () => setShowAddPost(true);
const handleShowViewPost = async (post) => {
const res = await axios.get(`/api/read/${post.contentId}`);
const { data } = res;
// TODO: Verify base64url(sha256(JSON.stringify(data))) = contentId
// TODO: Verify data.author = post.author
setViewPostContent(data.content);
const { content } = await Post.read(post.contentId);
setViewPostContent(content);
setShowViewPost(true);
};

View File

@ -2,11 +2,10 @@ import { useCallback, useContext, useState } from 'react';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import Modal from 'react-bootstrap/Modal';
import axios from 'axios';
import PropTypes from 'prop-types';
import { Buffer } from 'buffer/'; // note: the trailing slash is important!
import Web3Context from '../contexts/Web3Context';
import Web3Context from '../../contexts/Web3Context';
import Post from '../../utils/Post';
function AddPostModal({
show, setShow, title, postToBlockchain, onSubmit,
@ -20,28 +19,21 @@ function AddPostModal({
const handleSubmit = useCallback(async () => {
// Upload content to API
// TODO: include metamask signature
const msg = `0x${Buffer.from(content, 'utf8').toString('hex')}`;
const signature = await provider.request({
method: 'personal_sign',
params: [msg, account],
});
const data = {
author: account, content, signature,
};
const res = await axios.post('/api/write', data);
const hash = res.data;
setShow(false);
const post = new Post({ content });
// Include metamask signature
await post.sign(provider, account);
// Clear the input and hide the modal
setContent('');
setShow(false);
// Write to API
await post.write();
// If requested, upload the hash to the blockchain
if (postToBlockchain) {
// Upload hash to blockchain
await DAO.methods.addPost(account, hash).send({
from: account,
gas: 1000000,
});
await post.publish(DAO, account);
}
// If requested, call callback
if (onSubmit) {
onSubmit(hash, data);
onSubmit(post);
}
}, [provider, DAO, account, content, setShow, postToBlockchain, onSubmit]);

View File

@ -2,8 +2,8 @@ import { useCallback, useContext, useEffect } from 'react';
import { PropTypes } from 'prop-types';
import Button from 'react-bootstrap/Button';
import Web3Context from '../contexts/Web3Context';
import WorkContractContext from '../contexts/WorkContractContext';
import Web3Context from '../../contexts/Web3Context';
import WorkContractContext from '../../contexts/WorkContractContext';
const getAvailabilityStatus = (stake) => {
if (stake.reclaimed) return 'Reclaimed';

View File

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { PropTypes } from 'prop-types';
import useList from '../utils/List';
import WorkContractContext from '../contexts/WorkContractContext';
import useList from '../../utils/List';
import WorkContractContext from '../../contexts/WorkContractContext';
import AvailabilityStakes from './AvailabilityStakes';
import WorkRequests from './WorkRequests';

View File

@ -2,15 +2,15 @@ import {
useCallback, useContext, useEffect, useState,
} from 'react';
import { PropTypes } from 'prop-types';
import axios from 'axios';
import Button from 'react-bootstrap/Button';
import Web3 from 'web3';
import Web3Context from '../contexts/Web3Context';
import useList from '../utils/List';
import WorkContractContext from '../contexts/WorkContractContext';
import AddPostModal from './AddPostModal';
import ViewPostModal from './ViewPostModal';
import Web3Context from '../../contexts/Web3Context';
import useList from '../../utils/List';
import WorkContractContext from '../../contexts/WorkContractContext';
import AddPostModal from '../posts/AddPostModal';
import ViewPostModal from '../posts/ViewPostModal';
import Post from '../../utils/Post';
const getRequestStatus = (request) => {
switch (Number(request.status)) {
@ -115,8 +115,7 @@ function WorkRequests({
setShowEvidenceModal(true);
};
const onSubmitRequest = useCallback(async (hash) => {
// TODO: Accept input, upload to API, include hash in contract call
const onSubmitRequest = useCallback(async ({ hash }) => {
const web3 = new Web3(provider);
const priceWei = BigInt(web3.utils.toWei(price, 'ether'));
await workContract.methods.requestWork(hash).send({
@ -126,7 +125,7 @@ function WorkRequests({
});
}, [provider, workContract, account, price]);
const onSubmitEvidence = useCallback(async (hash) => {
const onSubmitEvidence = useCallback(async ({ hash }) => {
await workContract.methods.submitWorkEvidence(currentRequestId, hash).send({
from: account,
gas: 1000000,
@ -134,11 +133,8 @@ function WorkRequests({
}, [workContract, account, currentRequestId]);
const handleShowViewRequestModal = async (request) => {
const res = await axios.get(`/api/read/${request.requestContentId}`);
const { data } = res;
// TODO: Verify base64url(sha256(JSON.stringify(data))) = contentId
// TODO: Verify data.author = post.author
setViewRequestContent(data.content);
const { content } = await Post.read(request.requestContentId);
setViewRequestContent(content);
setShowViewRequestModal(true);
};

View File

@ -1,8 +1,8 @@
{
"localhost": {
"DAO": "0x2D812555F4eF06267406D80E7fA01Ac3288f626c",
"Work1": "0x3CAB55d59af095618F2ee539463E33447cfc97BA",
"Onboarding": "0xaB0c7Cf9A436978F55831C8EdB67892419ABAE62"
"DAO": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
"Work1": "0xC489CE618A049B413CE0AED9Fc7219a04510ddbb",
"Onboarding": "0x3477A098fBFe09aa26693012176baAEa16d9D2DA"
},
"sepolia": {
"DAO": "0xa3b15aBD114C2332652A4fD5f9A43B86315E5078",

75
client/src/utils/Post.js Normal file
View File

@ -0,0 +1,75 @@
import axios from 'axios';
// trailing slash is deliberate, to differentiate this package from the core node module
import { Buffer } from 'buffer/';
import { recoverPersonalSignature } from '@metamask/eth-sig-util';
// import createHash from 'create-hash';
import objectHash from 'object-hash';
// Make Buffer available to recoverPersonalSignature
window.Buffer = Buffer;
class Post {
constructor({
author, content, signature, hash,
}) {
this.author = author;
this.content = content;
this.signature = signature;
this.hash = hash;
}
// Read from API
static async read(hash) {
const { data: { content, author, signature } } = await axios.get(`/api/read/${hash}`);
// Verify hash
const derivedHash = objectHash({ author, content, signature });
if (hash !== derivedHash) {
throw new Error('Hash mismatch');
}
// Verify signature
let recovered;
try {
recovered = recoverPersonalSignature({ data: content, signature });
} catch (e) {
throw new Error('Signature error', e);
}
if (recovered !== author) {
throw new Error('Author mismatch');
}
return new Post({
content, author, signature, hash,
});
}
// Include MetaMask signature
async sign(web3Provider, account) {
this.author = account;
const msg = `0x${Buffer.from(this.content, 'utf8').toString('hex')}`;
this.signature = await web3Provider.request({
method: 'personal_sign',
params: [msg, account],
});
return this;
}
// Write to API
async write() {
const data = {
author: this.author,
content: this.content,
signature: this.signature,
};
const { data: hash } = await axios.post('/api/write', data);
this.hash = hash;
}
// Upload hash to blockchain
async publish(DAO, account) {
await DAO.methods.addPost(account, this.hash).send({
from: account,
gas: 1000000,
});
}
}
export default Post;

View File

@ -1 +1,4 @@
SEPOLIA_PRIVATE_KEY=
SEPOLIA_PRIVATE_KEY=
ETHERSCAN_API_KEY=
WORK1_PRICE="0.001"
ONBOARDING_PRICE="0.001"

View File

@ -1,8 +1,8 @@
{
"localhost": {
"DAO": "0x2D812555F4eF06267406D80E7fA01Ac3288f626c",
"Work1": "0x3CAB55d59af095618F2ee539463E33447cfc97BA",
"Onboarding": "0xaB0c7Cf9A436978F55831C8EdB67892419ABAE62"
"DAO": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
"Work1": "0xC489CE618A049B413CE0AED9Fc7219a04510ddbb",
"Onboarding": "0x3477A098fBFe09aa26693012176baAEa16d9D2DA"
},
"sepolia": {
"DAO": "0xa3b15aBD114C2332652A4fD5f9A43B86315E5078",

View File

@ -3,7 +3,11 @@ const fs = require('fs');
const contractAddresses = require('../contract-addresses.json');
require('dotenv').config();
const network = process.env.HARDHAT_NETWORK;
const work1Price = process.env.WORK1_PRICE || 0.001;
const onboardingPrice = process.env.ONBOARDING_PRICE || '0.001';
async function main() {
const dao = await ethers.deployContract('DAO');
@ -21,8 +25,8 @@ async function main() {
fs.copyFileSync(`./artifacts/contracts/${name}.sol/${name}.json`, `../client/src/assets/${name}.json`);
};
await deployWorkContract('Work1', ethers.parseEther('0.001'));
await deployWorkContract('Onboarding', ethers.parseEther('0.001'));
await deployWorkContract('Work1', ethers.parseEther(work1Price));
await deployWorkContract('Onboarding', ethers.parseEther(onboardingPrice));
fs.writeFileSync('../client/src/contract-addresses.json', JSON.stringify(contractAddresses, null, 2));
console.log('Wrote file', fs.realpathSync('../client/src/contract-addresses.json'));