first cut at backend part of rollup implementation
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 40s Details

This commit is contained in:
Ladd Hoffman 2024-05-01 22:24:53 -05:00
parent 33a458aba1
commit 073f6e61aa
45 changed files with 858 additions and 280 deletions

View File

@ -1,14 +1,14 @@
{
"localhost": {
"DAO": "0x21E65E57a2F7DF8B85E0F59b57afc024595dD833",
"Work1": "0x0686417a476C37701734dd405e381b7d9B247d22",
"Onboarding": "0x495c5AF3fD0B1c2bA3e959f11A48d6FC8C246633",
"Proposals": "0x43e45a19FAD2932D08c8D9B07f6830d1250DA71D",
"Rollup": "0x5E614c4d8C956A937e4a6acC6a9459CAAE193feA",
"Work2": "0xE3CC69EF45312959F9F752C971C35F553a165559",
"Reputation": "0xb8C047c11eF88A02cd2C706120ef27D67586785a",
"Forum": "0xdAA95487F0027C67473A4A560dD6628c49d218A9",
"Bench": "0x05d4aE5d0097d47C9FcC0191d7f68F175a8122Db"
"DAO": "0xc7E04c11eD94E375857b885b3e6E1Db30C061348",
"Work1": "0x1bEffEB10E9f5714a8e385FfcA84046688677eA8",
"Onboarding": "0xFC40076c675693441C6e553FEdDD3A3348db81E4",
"Proposals": "0xa1349A27D43d0F71CeDD75904ACc8f8CF8F81582",
"Rollup": "0x1361c87D5972a71cBCA34f6EAD928358deaC750D",
"Work2": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
"Reputation": "0xfC979dbae6Cd0f35CC240889663B523B35c5F101",
"Forum": "0xaf247e316A081871e713F492279D2360bd162401",
"Bench": "0xC8BCE8171e626d07E5095256F703B1df23a67362"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,8 @@
"fastq": "^1.17.1",
"level": "^8.0.1",
"matrix-bot-sdk": "^0.7.1",
"object-hash": "^3.0.0"
"object-hash": "^3.0.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"eslint": "^8.56.0",

View File

@ -20,7 +20,8 @@
"fastq": "^1.17.1",
"level": "^8.0.1",
"matrix-bot-sdk": "^0.7.1",
"object-hash": "^3.0.0"
"object-hash": "^3.0.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"eslint": "^8.56.0",

View File

@ -13,8 +13,8 @@ const {
const login = async () => {
console.log(`MATRIX_HOMESERVER_URL="${MATRIX_HOMESERVER_URL}"`);
const auth = new MatrixAuth(MATRIX_HOMESERVER_URL);
const client = await auth.passwordLogin(MATRIX_USER, MATRIX_PASSWORD);
console.log(`MATRIX_ACCESS_TOKEN="${client.accessToken}"`);
const matrixClient = await auth.passwordLogin(MATRIX_USER, MATRIX_PASSWORD);
console.log(`MATRIX_ACCESS_TOKEN="${matrixClient.accessToken}"`);
};
login();

View File

@ -1,28 +1,13 @@
const { getMatrixClient } = require('../matrix-bot');
const { matrixClient } = require('../matrix-bot');
const { matrixUserToAuthorAddress } = require('../util/db');
const write = require('../util/forum/write');
const { dao, wallet } = require('../util/contracts');
const { wallet } = require('../util/contracts');
const addPostWithRetry = require('../util/add-post-with-retry');
const {
ETH_NETWORK,
} = process.env;
const addPostWithRetry = async (authors, hash, citations, retryDelay = 5000) => {
try {
await dao.addPost(authors, hash, citations);
} catch (e) {
if (e.code === 'REPLACEMENT_UNDERPRICED') {
console.log('retry delay (sec):', retryDelay / 1000);
await Promise.delay(retryDelay);
return addPostWithRetry(authors, hash, citations, retryDelay * 2);
} if (e.reason === 'A post with this postId already exists') {
return { alreadyAdded: true };
}
throw e;
}
return { alreadyAdded: false };
};
module.exports = async (req, res) => {
const {
body: {
@ -43,8 +28,7 @@ module.exports = async (req, res) => {
console.log('roomId', roomId);
console.log('eventId', eventId);
const client = getMatrixClient();
const event = await client.getEvent(roomId, eventId);
const event = await matrixClient.getEvent(roomId, eventId);
console.log('event', event);
let authorAddress;
@ -82,7 +66,7 @@ module.exports = async (req, res) => {
} else {
console.log(`Added post to blockchain for matrix event ${eventUri}`);
// Send matrix event reply to the targeted event, notifying of this blockchain post
await client.replyNotice(roomId, event, `Added to ${ETH_NETWORK} blockchain as post ${hash}`);
await matrixClient.replyNotice(roomId, event, `Added to ${ETH_NETWORK} blockchain as post ${hash}`);
}
res.json({ postId: hash, alreadyAdded });

View File

@ -1,4 +1,4 @@
const { registerMessageHandler } = require('../matrix-bot');
const { registerMatrixMessageHandler } = require('../matrix-bot');
const { setTargetRoomId } = require('../matrix-bot/outbound-queue');
const {
@ -31,7 +31,7 @@ const handleCommand = async (client, roomId, event) => {
if (instanceId === BOT_INSTANCE_ID) {
setTargetRoomId(roomId);
await appState.put('targetRoomId', roomId);
await client.replyNotice(roomId, event, `Proposal events will be sent to this room for network ${ETH_NETWORK}`);
await client.replyNotice(roomId, event, `Events will be sent to this room (${roomId}) for network ${ETH_NETWORK}`);
}
} else if (proposalRegex.test(body)) {
const [, , proposalIndexStr] = proposalRegex.exec(body);
@ -55,7 +55,7 @@ const handleCommand = async (client, roomId, event) => {
};
const start = () => {
registerMessageHandler(handleCommand);
registerMatrixMessageHandler(handleCommand);
};
module.exports = {

View File

@ -1,6 +1,7 @@
const { proposals } = require('../util/contracts');
const read = require('../util/forum/read');
const { sendNewProposalEvent } = require('../matrix-bot/outbound-queue');
const { sendMatrixText } = require('../matrix-bot/outbound-queue');
const { proposalEventIds } = require('../util/db');
// Subscribe to proposal events
const start = () => {
@ -29,8 +30,16 @@ const start = () => {
message += `\n\n${JSON.stringify(post.embeddedData, null, 2)}`;
}
// The outbound queue handles deduplication
sendNewProposalEvent(proposalIndex, message);
try {
await proposalEventIds.get(Number(proposalIndex));
// If this doesn't throw, it means we already sent a message for this proposal
} catch (e) {
if (e.status === 404) {
console.log('sending new proposal event to room', { message });
const { eventId } = await sendMatrixText(message);
await proposalEventIds.put(Number(proposalIndex), eventId);
}
}
});
proposals.on('ProposalAccepted', async (proposalIndex) => {

View File

@ -4,7 +4,7 @@ const {
matrixUserToAuthorAddress,
authorAddressToMatrixUser,
} = require('../util/db');
const { registerEventHandler } = require('../matrix-bot');
const { registerMatrixEventHandler } = require('../matrix-bot');
const handleRegisterIdentity = async (client, roomId, event) => {
if (event.type !== 'io.dgov.identity.register') return;
@ -34,7 +34,7 @@ const handleRegisterIdentity = async (client, roomId, event) => {
};
const start = () => {
registerEventHandler(handleRegisterIdentity);
registerMatrixEventHandler(handleRegisterIdentity);
};
module.exports = {

View File

@ -0,0 +1,293 @@
const Promise = require('bluebird');
const { v4: uuidv4 } = require('uuid');
const { getContractAddressByNetworkName } = require('../util/contract-config');
const { registerDecider } = require('./validation-pools');
const { registerMatrixEventHandler, sendMatrixEvent, sendMatrixText } = require('../matrix-bot');
const { matrixPools, matrixUserToAuthorAddress, applicationData } = require('../util/db');
const {
rollup, wallet, work2, dao,
} = require('../util/contracts');
const read = require('../util/forum/read');
const write = require('../util/forum/write');
const addPostWithRetry = require('../util/add-post-with-retry');
const {
ETH_NETWORK,
} = process.env;
let batchWorker;
const rollupAddress = getContractAddressByNetworkName(ETH_NETWORK, 'Rollup');
const getBatchPostAuthorWeights = async (batchItems) => {
const weights = {};
await Promise.each(batchItems, async (postId) => {
// TODO: try/catch
const post = await read(postId);
// TODO: try/catch
const matrixPool = await matrixPools.get(postId);
const { fee, result: { votePasses, quorumMet } } = matrixPool;
post.authors.forEach(({ authorAddress, weightPPM }) => {
weights[authorAddress] = weights[authorAddress] ?? 0;
if (votePasses && quorumMet) {
// scale by matrix pool outcome and strength
weights[authorAddress] += weightPPM * fee;
}
// TODO: Rewards for policing
// TODO: Propagation via references
});
// Rescale author weights so they sum to 1000000
const sumOfWeights = Object.values(weights).reduce((t, v) => t + v, 0);
const scaledWeights = weights
.map((weight) => Math.floor((weight * 1000000) / sumOfWeights));
const sumOfScaledWeights = Object.values(scaledWeights).reduce((t, v) => t + v, 0);
scaledWeights[0] += 1000000 - sumOfScaledWeights;
const authors = Object.entries(scaledWeights)
.map(([authorAddress, weightPPM]) => ({ authorAddress, weightPPM }));
return authors;
});
};
const submitBatchPost = async (batchItems) => {
const authors = await getBatchPostAuthorWeights(batchItems);
// TODO: Compute citations as aggregate of the citations of posts in the batch
const citations = [];
const content = `Batch of ${batchItems.length} items`;
const embeddedData = {
matrixPools: batchItems.map((x) => x.postId),
nonce: uuidv4().replace(/-/i, ''),
};
const sender = await wallet.getAddress();
const contentToVerify = `${content}\n\n${JSON.stringify(embeddedData, null, 2)}`;
const signature = await wallet.signMessage(contentToVerify);
// Write to the forum database
const { hash: batchPostId } = await write({
sender, authors, citations, content, embeddedData, signature,
});
// Add rollup post on-chain
await addPostWithRetry(authors, batchPostId, citations);
// Call Rollup.submitBatch
const poolDuration = 60;
await rollup.submitBatch(batchPostId, batchItems.length, poolDuration);
};
const evaluateMatrixPoolOutcome = async (postId) => {
const matrixPool = await matrixPools.get(postId);
// This should already contain all the info we need to evaluate the outcome
const { stakes, quorum, winRatio } = matrixPool;
const stakedFor = stakes
.filter((x) => x.inFavor)
.reduce((total, { amount }) => total + amount, 0);
const stakedAgainst = stakes
.filter((x) => !x.inFavor)
.reduce((total, { amount }) => total + amount, 0);
const votePasses = stakedFor * winRatio[1] >= (stakedFor + stakedAgainst) * winRatio[0];
const totalSupply = await dao.totalSupply();
const quorumMet = BigInt(stakedFor + stakedAgainst) * quorum[1] >= totalSupply * quorum[0];
const result = {
stakedFor, stakedAgainst, totalSupply, votePasses, quorumMet,
};
console.log('Matrix pool outcome evaluated', result);
matrixPool.result = result;
await matrixPools.put(postId, matrixPool);
sendMatrixEvent('io.dgov.pool.result', result);
// Even if we're not the current batch worker, keep track of batch items
let batchItems;
try {
batchItems = await applicationData.get('batchItems');
} catch (e) {
batchItems = [];
}
batchItems.push(postId);
await applicationData.put('batchItems', batchItems);
if (batchWorker === '0x0000000000000000000000000000000000000000') {
// TODO: If there's no batch worker, we should stake our availability
// and then submit the batch immediately.
await submitBatchPost(batchItems);
}
if (batchWorker === await wallet.getAddress()) {
// TODO: If we are the batch worker, we should wait an appropriate amout of time /
// number of matrix pools before submitting a batch.
}
};
const validateWorkEvidence = async (sender, post) => {
let valid = false;
if (sender === work2.target) {
const expectedContent = 'This is a work evidence post';
valid = post.content.startsWith(expectedContent);
}
console.log(`Work evidence ${valid ? 'matched' : 'did not match'} the expected content`);
return valid;
};
const start = async () => {
console.log('registering validation pool decider for rollup');
registerDecider(async (pool, post) => {
// If this is not sent by the work1 contract, it's not of interest here.
if (pool.sender !== rollupAddress) return false;
// A rollup post should contain
// - a list of off-chain validation pools
// - authorship corresponding to the result of those off-chain pools
if (!post.embeddedData?.matrixPools) return false;
// Our task here is to check whether the posted result agrees with our own computations
const expectedAuthors = await getBatchPostAuthorWeights(post.embeddedData.matrixPools);
if (expectedAuthors.length !== post.authors.length) return false;
return post.authors.every(({ authorAddress, weightPPM }) => {
const expectedAuthor = expectedAuthors.find((x) => x.authorAddress === authorAddress);
return weightPPM === expectedAuthor.weightPPM;
});
});
// Check for an assigned batch worker
batchWorker = await rollup.batchWorker();
console.log('At startup', { batchWorker });
rollup.on('BatchWorkerAssigned', async (batchWorker_) => {
console.log('Batch worker assigned:', batchWorker);
batchWorker = batchWorker_;
});
/// `sender` is the address that called Rollup.addItem on chain, i.e. the Work2 contract.
rollup.on('BatchItemAdded', async (postId, sender, fee) => {
// If we are the batch worker or there is no batch worker, initiate a matrix pool
if (batchWorker === await wallet.getAddress()
|| batchWorker === '0x0000000000000000000000000000000000000000') {
const duration = 20;
const quorum = [1, 3];
const winRatio = [1, 2];
const params = {
sender, fee, duration, quorum, winRatio,
};
let post;
try {
post = await read(postId);
} catch (e) {
console.error(`Post ID ${postId} not found`);
return;
}
// Initialize a matrix pool
try {
await matrixPools.get(postId);
// If this doesn't throw, it means we or someone else already sent this event
console.log(`Matrix pool start event has already been sent for postId ${postId}`);
} catch (e) {
if (e.status === 404) {
console.log('sending matrix pool start event');
const { roomId, eventId } = sendMatrixEvent('io.dgov.pool.start', {
postId,
...params,
});
// Register our own stake and send a message
const currentRep = await dao.balanceOf(await wallet.getAddress());
const valid = await validateWorkEvidence(sender, post);
const stake = { amount: currentRep, account: await wallet.getAddress(), inFavor: valid };
sendMatrixEvent('io.dgov.pool.stake', { postId, amount: currentRep, inFavor: valid });
await matrixPools.put(postId, {
postId,
roomId,
eventId,
...params,
stakes: [stake],
});
} else {
throw e;
}
// Since we're assuming responsibility as the batch worker,
// set a timeout to evaulate the outcome
setTimeout(() => evaluateMatrixPoolOutcome(postId), duration * 1000);
}
}
});
registerMatrixEventHandler(async (client, roomId, event) => {
switch (event.type) {
case 'io.dgov.pool.start': {
const { postId, sender, ...params } = event.content;
// We can use LevelDB to store information about validation pools
const eventId = event.event_id;
console.log('Matrix pool started', { postId, ...params });
// Validate the target post, and stake for/against
let post;
try {
post = await read(postId);
} catch (e) {
console.error(`Post ID ${postId} not found`);
break;
}
const currentRep = await dao.balanceOf(await wallet.getAddress());
const valid = await validateWorkEvidence(sender, post);
const stake = { amount: currentRep, account: await wallet.getAddress(), inFavor: valid };
sendMatrixEvent('io.dgov.pool.stake', { postId, amount: currentRep, inFavor: valid });
await matrixPools.put(postId, {
roomId, eventId, ...params, stakes: [stake],
});
break;
}
case 'io.dgov.pool.stake': {
const { postId, amount, inFavor } = event.content;
let account;
try {
account = await matrixUserToAuthorAddress(event.sender);
} catch (e) {
// Error, sender has not registered their matrix identity
sendMatrixText(`Matrix user ${event.sender} has not registered their wallet address`);
return;
}
let matrixPool;
try {
matrixPool = await matrixPools.get(postId);
} catch (e) {
// Error. matrix pool not found
sendMatrixText(`Received stake for unknown matrix pool, for post ${postId}`);
return;
}
const stake = { account, amount, inFavor };
matrixPool.stakes = matrixPool.stakes ?? [];
matrixPool.stakes.push(stake);
await matrixPools.put(postId, matrixPool);
console.log(`registered stake in matrix pool for post ${postId} by ${account}`);
break;
}
case 'io.dgov.pool.result': {
// This should be sent by the current batch worker
// TODO: Compare batch worker's result with ours to verify and provide early warning
break;
}
case 'io.dgov.rollup.submit': {
// This should include the identifier of the on-chain validation pool
// TODO: Compare batch worker's result with ours to verify
break;
}
default:
}
});
// Put in our availability stake to do the rollup work
// Then check if there is a rollup worker.
// It's possible we're the first or only worker;
// if there is no worker assigned yet, we can submit the first one.
// The procedure will be the same every time we are the rollup worker:
// - Stake availability to be the next rollup worker;
// - Create a rollup post -- compute authorship based on matrix pool results
// - Send matrix event 'io.dgov.rollup.submit'? May not be necessary,
// as there will be notification from the contract itself.
// However, it would be nice to have in the tiemline of the room,
// and it could be nice to give the participants a heads up to expect the batch.
// We could even require it as part of batch validation.
// - Call DAO.addPost(authors, postId)
// - Call Rollup.submitBatch(postId, batchSize, poolDuration)
};
module.exports = {
start,
};

View File

@ -50,13 +50,13 @@ const start = async () => {
return;
}
// Stake all available reputation
console.log(`STAKING ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex}`);
const currentRep = await dao.balanceOf(wallet.getAddress());
const currentRep = await dao.balanceOf(await wallet.getAddress());
console.log(`STAKING ${currentRep} ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex}`);
try {
await dao.stakeOnValidationPool(poolIndex, currentRep, inFavor);
} catch (e) {
// Maybe the end time passed?
console.error(`STAKING ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex} failed, reason: ${e.reason}`);
console.error(`STAKING failed, reason: ${e.reason}`);
}
});
};

View File

@ -2,7 +2,7 @@ require('dotenv').config();
const api = require('./api');
const matrixBot = require('./matrix-bot');
const topics = require('./topics');
const eventHandlers = require('./event-handlers');
const {
ENABLE_API,
@ -17,4 +17,4 @@ if (ENABLE_MATRIX !== 'false') {
matrixBot.start();
}
topics.start();
eventHandlers.start();

View File

@ -15,7 +15,7 @@ const {
const storageProvider = new SimpleFsStorageProvider(BOT_STORAGE_PATH);
const cryptoProvider = new RustSdkCryptoStorageProvider(BOT_CRYPTO_STORAGE_PATH);
console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL);
const client = new MatrixClient(
const matrixClient = new MatrixClient(
MATRIX_HOMESERVER_URL,
MATRIX_ACCESS_TOKEN,
storageProvider,
@ -23,38 +23,42 @@ const client = new MatrixClient(
);
let joinedRooms;
const { startOutboundQueue } = require('./outbound-queue');
const { startOutboundQueue, sendMatrixEvent, sendMatrixText } = require('./outbound-queue');
const start = async () => {
// Automatically join a room to which we are invited
AutojoinRoomsMixin.setupOnClient(client);
AutojoinRoomsMixin.setupOnClient(matrixClient);
joinedRooms = await client.getJoinedRooms();
joinedRooms = await matrixClient.getJoinedRooms();
console.log('joined rooms:', joinedRooms);
client.start().then(() => {
console.log('Bot started!');
matrixClient.start().then(() => {
console.log('Matrix bot started!');
// Start the outbound queue
startOutboundQueue(client);
startOutboundQueue(matrixClient);
});
};
const registerMessageHandler = (eventHandler) => {
client.on('room.message', (roomId, event) => eventHandler(client, roomId, event));
const registerMatrixMessageHandler = (eventHandler) => {
matrixClient.on('room.message', async (roomId, event) => {
if (event.sender === await matrixClient.getUserId()) return;
eventHandler(matrixClient, roomId, event);
});
};
const registerEventHandler = (eventHandler) => {
client.on('room.event', (roomId, event) => {
const registerMatrixEventHandler = (eventHandler) => {
matrixClient.on('room.event', async (roomId, event) => {
if (event.sender === await matrixClient.getUserId()) return;
if (event.state_key !== undefined) return; // state event
eventHandler(client, roomId, event);
eventHandler(matrixClient, roomId, event);
});
};
const getMatrixClient = () => client;
module.exports = {
start,
getMatrixClient,
registerMessageHandler,
registerEventHandler,
matrixClient,
registerMatrixMessageHandler,
registerMatrixEventHandler,
sendMatrixEvent,
sendMatrixText,
};

View File

@ -1,32 +1,30 @@
const fastq = require('fastq');
const {
proposalEventIds,
} = require('../util/db');
const { applicationData } = require('../util/db');
let client;
let matrixClient;
let targetRoomId;
const setTargetRoomId = (roomId) => {
const setTargetRoomId = async (roomId) => {
targetRoomId = roomId;
console.log('target room ID:', targetRoomId);
await applicationData.put('targetRoomId', targetRoomId);
};
const processOutboundQueue = async ({ type, ...args }) => {
console.log('processing outbound queue item');
console.log('processing outbound queue item', { type, args });
if (!targetRoomId) return;
switch (type) {
case 'NewProposal': {
const { proposalIndex, text } = args;
try {
await proposalEventIds.get(Number(proposalIndex));
// If this doesn't throw, it means we already sent a message for this proposal
} catch (e) {
if (e.status === 404) {
console.log('sending to room', targetRoomId, { text });
const eventId = await client.sendText(targetRoomId, text);
await proposalEventIds.put(Number(proposalIndex), eventId);
}
}
case 'MatrixEvent': {
const { eventType, content, onSend } = args;
const eventId = await matrixClient.sendEvent(targetRoomId, eventType, content);
onSend(targetRoomId, eventId);
break;
}
case 'MatrixText': {
const { text, onSend } = args;
const eventId = await matrixClient.sendText(targetRoomId, text);
onSend(targetRoomId, eventId);
break;
}
default:
@ -34,22 +32,45 @@ const processOutboundQueue = async ({ type, ...args }) => {
};
const outboundQueue = fastq(processOutboundQueue, 1);
// Pause until client is set
// Pause until matrixClient is set
outboundQueue.pause();
const startOutboundQueue = (c) => {
client = c;
// Resume now that client is set
const startOutboundQueue = async (matrixClient_) => {
matrixClient = matrixClient_;
try {
targetRoomId = await applicationData.get('targetRoomId');
console.log('target room ID:', targetRoomId);
} catch (e) {
// No target room set
console.warn('target room ID is not set -- will not be able to send messages until it is set. Use !target <bot-id>');
}
outboundQueue.resume();
};
const sendNewProposalEvent = (proposalIndex, text) => {
outboundQueue.push({ type: 'NewProposal', proposalIndex, text });
};
const sendMatrixEvent = async (type, content) => new Promise((resolve) => {
outboundQueue.push({
eventType: 'MatrixEvent',
content,
onSend: ((roomId, eventId) => {
resolve({ roomId, eventId });
}),
});
});
const sendMatrixText = async (text) => new Promise((resolve) => {
outboundQueue.push({
eventType: 'MatrixText',
text,
onSend: ((roomId, eventId) => {
resolve({ roomId, eventId });
}),
});
});
module.exports = {
setTargetRoomId,
outboundQueue,
startOutboundQueue,
sendNewProposalEvent,
sendMatrixEvent,
sendMatrixText,
};

View File

@ -1,60 +0,0 @@
const { getContractAddressByNetworkName } = require('../util/contract-config');
const { registerDecider } = require('./validation-pools');
const { registerEventHandler } = require('../matrix-bot');
const { matrixPools } = require('../util/db');
const {
ETH_NETWORK,
} = process.env;
const rollupAddress = getContractAddressByNetworkName(ETH_NETWORK, 'Rollup');
const start = async () => {
console.log('registering validation pool decider for rollup');
registerDecider((pool, post) => {
// If this is not sent by the work1 contract, it's not of interest here.
if (pool.sender !== rollupAddress) return false;
// A rollup post should contain
// - a list of off-chain validation pools
// - authorship corresponding to the result of those off-chain pools
if (!post.embeddedData?.matrixPools) return false;
// TODO: Compute expected result by fetching off-chain pool matrix events
// TODO: Or precompute? Since we want to be computing that anyway.
return false;
});
// We can use LevelDB to store information about validation pools
registerEventHandler(async (client, roomId, event) => {
switch (event.type) {
case 'io.dgov.pool.start': {
// Use the event id as the validation pool id
const poolId = event.event_id;
// TODO: This event should include validation pool params
console.log('Matrix pool started', { poolId });
await matrixPools.put(poolId, {});
break;
}
case 'io.dgov.pool.stake': {
// TODO: Keep track of stakes
break;
}
case 'io.dgov.pool.result': {
// This should be sent by the current batch worker
// TODO: Compare batch worker's result with ours to verify and provide early warning
break;
}
case 'io.dgov.rollup.submit': {
// TODO: Compare batch worker's result with ours to verify
break;
}
default:
}
});
};
module.exports = {
start,
};

View File

@ -0,0 +1,19 @@
const { dao } = require('./contracts');
const addPostWithRetry = async (authors, hash, citations, retryDelay = 5000) => {
try {
await dao.addPost(authors, hash, citations);
} catch (e) {
if (e.code === 'REPLACEMENT_UNDERPRICED') {
console.log('retry delay (sec):', retryDelay / 1000);
await Promise.delay(retryDelay);
return addPostWithRetry(authors, hash, citations, retryDelay * 2);
} if (e.reason === 'A post with this postId already exists') {
return { alreadyAdded: true };
}
throw e;
}
return { alreadyAdded: false };
};
module.exports = addPostWithRetry;

View File

@ -3,6 +3,8 @@ const ethers = require('ethers');
const { getContractAddressByNetworkName } = require('./contract-config');
const DAOArtifact = require('../../contractArtifacts/DAO.json');
const ProposalsArtifact = require('../../contractArtifacts/Proposals.json');
const RollupArtifact = require('../../contractArtifacts/Rollup.json');
const Work2Artifact = require('../../contractArtifacts/Work2.json');
const {
ETH_NETWORK,
@ -41,4 +43,14 @@ module.exports = {
ProposalsArtifact.abi,
wallet,
),
rollup: new ethers.Contract(
getContractAddressByNetworkName(ETH_NETWORK, 'Rollup'),
RollupArtifact.abi,
wallet,
),
work2: new ethers.Contract(
getContractAddressByNetworkName(ETH_NETWORK, 'Work2'),
Work2Artifact.abi,
wallet,
),
};

View File

@ -3,6 +3,7 @@ const { Level } = require('level');
const dataDir = process.env.LEVEL_DATA_DIR || 'data';
module.exports = {
applicationData: new Level(`${dataDir}/applicationData}`, { valueEncoding: 'json' }),
forum: new Level(`${dataDir}/forum`, { valueEncoding: 'json' }),
authorAddresses: new Level(`${dataDir}/authorAddresses`),
authorPrivKeys: new Level(`${dataDir}/authorPrivKeys`),

View File

@ -1,14 +1,14 @@
{
"localhost": {
"DAO": "0x21E65E57a2F7DF8B85E0F59b57afc024595dD833",
"Work1": "0x0686417a476C37701734dd405e381b7d9B247d22",
"Onboarding": "0x495c5AF3fD0B1c2bA3e959f11A48d6FC8C246633",
"Proposals": "0x43e45a19FAD2932D08c8D9B07f6830d1250DA71D",
"Rollup": "0x5E614c4d8C956A937e4a6acC6a9459CAAE193feA",
"Work2": "0xE3CC69EF45312959F9F752C971C35F553a165559",
"Reputation": "0xb8C047c11eF88A02cd2C706120ef27D67586785a",
"Forum": "0xdAA95487F0027C67473A4A560dD6628c49d218A9",
"Bench": "0x05d4aE5d0097d47C9FcC0191d7f68F175a8122Db"
"DAO": "0xc7E04c11eD94E375857b885b3e6E1Db30C061348",
"Work1": "0x1bEffEB10E9f5714a8e385FfcA84046688677eA8",
"Onboarding": "0xFC40076c675693441C6e553FEdDD3A3348db81E4",
"Proposals": "0xa1349A27D43d0F71CeDD75904ACc8f8CF8F81582",
"Rollup": "0x1361c87D5972a71cBCA34f6EAD928358deaC750D",
"Work2": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
"Reputation": "0xfC979dbae6Cd0f35CC240889663B523B35c5F101",
"Forum": "0xaf247e316A081871e713F492279D2360bd162401",
"Bench": "0xC8BCE8171e626d07E5095256F703B1df23a67362"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

View File

@ -8,17 +8,21 @@ contract Rollup is Availability {
constructor(DAO dao) Availability(dao) {}
struct BatchItem {
address author;
address sender;
address worker;
uint stakeAmount;
uint fee;
string postId;
}
mapping(uint => BatchItem) items;
uint itemCount;
address batchWorker;
mapping(uint => BatchItem) public items;
uint public itemCount;
address public batchWorker;
uint batchWorkerStakeIndex;
event BatchItemAdded(string postId, address sender, uint fee);
event BatchWorkerAssigned(address batchWorker);
/// Instead of initiating a validation pool, call this method to include
/// the stakes and fee in the next batch validation pool
function addItem(
@ -27,10 +31,12 @@ contract Rollup is Availability {
string calldata postId
) public payable {
BatchItem storage item = items[itemCount++];
item.author = author;
item.sender = msg.sender;
item.worker = author;
item.stakeAmount = stakeAmount;
item.fee = msg.value;
item.postId = postId;
emit BatchItemAdded(postId, item.sender, item.fee);
}
/// To be called by the currently assigned batch worker,
@ -67,7 +73,7 @@ contract Rollup is Availability {
for (uint i = 0; i < itemCount; i++) {
dao.delegatedStakeOnValidationPool(
poolIndex,
items[i].author,
items[i].worker,
items[i].stakeAmount,
true
);
@ -95,5 +101,6 @@ contract Rollup is Availability {
// Select the next worker
batchWorkerStakeIndex = assignWork();
batchWorker = stakes[batchWorkerStakeIndex].worker;
emit BatchWorkerAssigned(batchWorker);
}
}

View File

@ -90,6 +90,10 @@ contract DAO {
return rep.balanceOf(account);
}
function totalSupply() public view returns (uint256) {
return rep.totalSupply();
}
function allowance(
address owner,
address spender
@ -110,7 +114,8 @@ contract DAO {
address to,
uint256 amount
) public {
return rep.forwardAllowance(owner, to, amount);
rep.spendAllowance(owner, msg.sender, amount);
rep.approve(owner, to, rep.allowance(owner, to) + amount);
}
/// Authorize a contract to transfer REP, and call that contract's acceptAvailability method
@ -119,15 +124,11 @@ contract DAO {
uint256 value,
uint duration
) external returns (bool) {
rep.approve(msg.sender, to, allowance(msg.sender, to) + value);
rep.approve(msg.sender, to, rep.allowance(msg.sender, to) + value);
IAcceptAvailability(to).acceptAvailability(msg.sender, value, duration);
return true;
}
function totalSupply() public view returns (uint256) {
return rep.totalSupply();
}
function propagateReputation(string memory postId, int amount) public {
forum.propagateReputation(postId, amount, false, 0);
}

View File

@ -50,19 +50,6 @@ contract Reputation is ERC20("Reputation", "REP") {
revert("REP transfer is not allowed");
}
function forwardAllowance(
address owner,
address to,
uint256 amount
) public {
require(
msg.sender == address(dao),
"Only DAO contract may call spendAllowance"
);
_spendAllowance(owner, msg.sender, amount);
_approve(owner, to, allowance(owner, to) + amount);
}
function spendAllowance(
address owner,
address spender,
@ -78,7 +65,7 @@ contract Reputation is ERC20("Reputation", "REP") {
function approve(address owner, address spender, uint256 value) public {
require(
msg.sender == address(dao),
"Only DAO contract may call update"
"Only DAO contract may call approve"
);
_approve(owner, spender, value);
}

View File

@ -1,14 +1,14 @@
{
"localhost": {
"DAO": "0x21E65E57a2F7DF8B85E0F59b57afc024595dD833",
"Work1": "0x0686417a476C37701734dd405e381b7d9B247d22",
"Onboarding": "0x495c5AF3fD0B1c2bA3e959f11A48d6FC8C246633",
"Proposals": "0x43e45a19FAD2932D08c8D9B07f6830d1250DA71D",
"Rollup": "0x5E614c4d8C956A937e4a6acC6a9459CAAE193feA",
"Work2": "0xE3CC69EF45312959F9F752C971C35F553a165559",
"Reputation": "0xb8C047c11eF88A02cd2C706120ef27D67586785a",
"Forum": "0xdAA95487F0027C67473A4A560dD6628c49d218A9",
"Bench": "0x05d4aE5d0097d47C9FcC0191d7f68F175a8122Db"
"DAO": "0xc7E04c11eD94E375857b885b3e6E1Db30C061348",
"Work1": "0x1bEffEB10E9f5714a8e385FfcA84046688677eA8",
"Onboarding": "0xFC40076c675693441C6e553FEdDD3A3348db81E4",
"Proposals": "0xa1349A27D43d0F71CeDD75904ACc8f8CF8F81582",
"Rollup": "0x1361c87D5972a71cBCA34f6EAD928358deaC750D",
"Work2": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
"Reputation": "0xfC979dbae6Cd0f35CC240889663B523B35c5F101",
"Forum": "0xaf247e316A081871e713F492279D2360bd162401",
"Bench": "0xC8BCE8171e626d07E5095256F703B1df23a67362"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,13 @@ function AddPostModal({
const handleSubmit = useCallback(async () => {
// Upload content to API
const post = new Post({ content, authors: [{ weightPPM: 1000000, authorAddress: account }] });
const post = new Post({
content,
authors: [{ weightPPM: 1000000, authorAddress: account }],
embeddedData: {
nonce: crypto.randomUUID().replaceAll('-', ''),
},
});
// Include metamask signature
await post.sign(provider, account);
// Clear the input and hide the modal

View File

@ -289,6 +289,17 @@ function MainTabs() {
showRequestWork
/>
)}
{work2 && (
<WorkContract
workContract={work2}
showAvailabilityActions={false}
showAvailabilityAmount={false}
onlyShowAvailable
title="Work Contract 2"
verb="Work"
showRequestWork
/>
)}
</Tab>
<Tab eventKey="proposals" title="Proposals">
<Proposals />

View File

@ -34,9 +34,14 @@ function ProposePriceChangeModal({
const handleClose = () => setShow(false);
const handleSubmit = useCallback(async () => {
const post = new Post({ content, authors: [{ weightPPM: 1000000, authorAddress: account }] });
// Include price as embedded data
post.embeddedData = { proposedPrice };
const post = new Post({
content,
authors: [{ weightPPM: 1000000, authorAddress: account }],
embeddedData: {
proposedPrice,
nonce: crypto.randomUUID().replaceAll('-', ''),
},
});
// Include metamask signature
await post.sign(provider, account);
// Clear the input and hide the modal