Skip to content

Commit

Permalink
Merge pull request #11 from ar-io/PE-7653
Browse files Browse the repository at this point in the history
feat(vote): remove votes of removed controllers PE-7653
  • Loading branch information
arielmelendez authored Feb 12, 2025
2 parents 4b86803 + 0491be5 commit acb489b
Show file tree
Hide file tree
Showing 3 changed files with 592 additions and 25 deletions.
144 changes: 121 additions & 23 deletions process/src/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ local function addEventingHandler(handlerName, pattern, handleFn, critical, prin
Handlers.add(handlerName, pattern, function(msg)
-- add an TEvent to the message if it doesn't exist
msg.aoEvent = msg.aoEvent or TEvent(msg)
msg.subAoEvents = msg.subAoEvents or {}

-- global handler for all eventing errors, so we can log them and send a notice to the sender for non critical errors and discard the memory on critical errors
local status, resultOrError = eventingPcall(msg.aoEvent, function(error)
--- non critical errors will send an invalid notice back to the caller with the error information, memory is not discarded
Expand All @@ -115,6 +117,9 @@ local function addEventingHandler(handlerName, pattern, handleFn, critical, prin
end
if printEvent then
msg.aoEvent:printEvent()
for _, subAoEvent in pairs(msg.subAoEvents) do
subAoEvent:printEvent()
end
end
end)
end
Expand Down Expand Up @@ -154,41 +159,104 @@ end, function(msg)
updateLastKnownMessage(msg)
end, CRITICAL, false)

--- @param controller WalletAddress
--- @param excludedProposalNames ProposalName[]
--- @param aoEvent TEvent
local function removeVotesForController(controller, excludedProposalNames, aoEvent)
local excludedProposalNamesLookup = utils.createLookupTable(excludedProposalNames)
local removedYays = {}
local removedNays = {}
for proposalName, proposal in pairs(Proposals) do
if not excludedProposalNamesLookup[proposalName] then
if proposal.yays[controller] then
print("Removing yay vote by " .. controller .. " from " .. proposalName)
proposal.yays[controller] = nil
table.insert(removedYays, tostring(proposal.proposalNumber))
end
if proposal.nays[controller] then
print("Removing nay vote by " .. controller .. " from " .. proposalName)
proposal.nays[controller] = nil
table.insert(removedNays, tostring(proposal.proposalNumber))
end
end
end
if #removedYays > 0 then
aoEvent:addField("Removed-Yays", removedYays)
aoEvent:addField("Removed-Yays-Count", #removedYays)
end
if #removedNays > 0 then
aoEvent:addField("Removed-Nays", removedNays)
aoEvent:addField("Removed-Nays-Count", #removedNays)
end
end

--- @param msg ParsedMessage
--- @param excludedProposalNames ProposalName[]
local function reassessQuorumOnAllProposals(msg, excludedProposalNames)
local excludedProposalNamesLookup = utils.createLookupTable(excludedProposalNames)

-- First sort the proposals by proposal ID so that there's a deterministic order of operations here
local sortedProposalIds = {}
local proposalIdToProposalName = {}
for proposalName, proposal in pairs(Proposals) do
table.insert(sortedProposalIds, proposal.proposalNumber)
proposalIdToProposalName[proposal.proposalNumber] = proposalName
end
table.sort(sortedProposalIds)

for _, proposalId in ipairs(sortedProposalIds) do
local proposalName = proposalIdToProposalName[proposalId]
if not excludedProposalNamesLookup[proposalName] then
local subAoEvent = TEvent(msg)
handleMaybeVoteQuorum(proposalName, msg, subAoEvent)
end
end
end

--- @param proposalName ProposalName
--- @param msg ParsedMessage
function handleMaybeVoteQuorum(proposalName, msg)
--- @param aoEvent TEvent?
function handleMaybeVoteQuorum(proposalName, msg, aoEvent)
aoEvent = aoEvent or msg.aoEvent

-- Check whether the proposal has passed...
local proposal = Proposals[proposalName]
local yaysCount = utils.lengthOfTable(proposal.yays)
local naysCount = utils.lengthOfTable(proposal.nays)
local passThreshold = math.floor(utils.lengthOfTable(Controllers) / 2) + 1
local controllersCount = utils.lengthOfTable(Controllers)
local failThreshold = math.max(controllersCount - passThreshold, 1)

msg.aoEvent:addField("Controllers-Count", utils.lengthOfTable(Controllers))
msg.aoEvent:addField("Controllers", utils.getTableKeys(Controllers))
msg.aoEvent:addField("Proposal-Number", proposal.proposalNumber)
msg.aoEvent:addField("Proposal-Name", proposalName)
msg.aoEvent:addField("Proposal-Type", proposal.type)
msg.aoEvent:addField("Yays-Count", yaysCount)
msg.aoEvent:addField("Yays", utils.getTableKeys(proposal.yays))
msg.aoEvent:addField("Nays-Count", naysCount)
msg.aoEvent:addField("Nays", utils.getTableKeys(proposal.nays))
msg.aoEvent:addField("Pass-Threshold", passThreshold)
msg.aoEvent:addField("Fail-Threshold", failThreshold)
local failThreshold = controllersCount - passThreshold + 1

aoEvent:addField("Controllers-Count", utils.lengthOfTable(Controllers))
aoEvent:addField("Controllers", utils.getTableKeys(Controllers))
aoEvent:addField("Proposal-Number", proposal.proposalNumber)
aoEvent:addField("Proposal-Name", proposalName)
aoEvent:addField("Proposal-Type", proposal.type)
aoEvent:addField("Yays-Count", yaysCount)
aoEvent:addField("Yays", utils.getTableKeys(proposal.yays))
aoEvent:addField("Nays-Count", naysCount)
aoEvent:addField("Nays", utils.getTableKeys(proposal.nays))
aoEvent:addField("Pass-Threshold", passThreshold)
aoEvent:addField("Fail-Threshold", failThreshold)
if proposal.controller then
msg.aoEvent:addField("Controller", proposal.controller)
aoEvent:addField("Controller", proposal.controller)
end
if proposal.processId then
msg.aoEvent:addField("Process-Id", proposal.processId)
aoEvent:addField("Process-Id", proposal.processId)
end

--- @param accepted boolean
local function notifyProposalComplete(accepted)
--- @param recipients table<WalletAddress, any>
local function notifyProposalComplete(accepted, recipients)
-- Send additional events if other proposals were completed
if msg.aoEvent ~= aoEvent then
table.insert(msg.subAoEvents, aoEvent)
end

local returnData = utils.deepCopy(proposal)
--- @diagnostic disable-next-line: inject-field
returnData.proposalName = proposalName
for address, _ in pairs(Controllers) do
for address, _ in pairs(recipients) do
Send(msg, {
Target = address,
Action = accepted and "Proposal-Accepted-Notice" or "Proposal-Rejected-Notice",
Expand All @@ -198,12 +266,29 @@ function handleMaybeVoteQuorum(proposalName, msg)
end

if yaysCount >= passThreshold then
print(
"Proposal "
.. proposalName
.. " with passThreshold "
.. passThreshold
.. " and failThreshold "
.. failThreshold
.. " has passed with "
.. yaysCount
.. " yays and "
.. naysCount
.. " nays"
)
-- Proposal has passed
msg.aoEvent:addField("Proposal-Status", "Passed")
aoEvent:addField("Proposal-Status", "Passed")
local notificationRecipients = utils.deepCopy(Controllers)
if proposal.type == "Add-Controller" then
Controllers[proposal.controller] = true
elseif proposal.type == "Remove-Controller" then
Controllers[proposal.controller] = nil
removeVotesForController(proposal.controller, { proposalName }, aoEvent)
-- Side effect: removed controller will not know the outcome of these proposals
reassessQuorumOnAllProposals(msg, { proposalName })
elseif proposal.type == "Eval" then
Send(msg, {
Target = proposal.processId,
Expand All @@ -216,15 +301,28 @@ function handleMaybeVoteQuorum(proposalName, msg)
end

Proposals[proposalName] = nil
notifyProposalComplete(true)
notifyProposalComplete(true, notificationRecipients)
elseif naysCount >= failThreshold then
print(
"Proposal "
.. proposalName
.. " with passThreshold "
.. passThreshold
.. " and failThreshold "
.. failThreshold
.. " has failed with "
.. yaysCount
.. " yays and "
.. naysCount
.. " nays"
)
-- Proposal has failed
msg.aoEvent:addField("Proposal-Status", "Failed")
aoEvent:addField("Proposal-Status", "Failed")
Proposals[proposalName] = nil
notifyProposalComplete(false)
notifyProposalComplete(false, Controllers)
else
-- No quorum yet
msg.aoEvent:addField("Proposal-Status", "In Progress")
aoEvent:addField("Proposal-Status", "In Progress")
end
end

Expand Down
25 changes: 24 additions & 1 deletion process/tests/helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export async function rubberStampProposal({
}){
const controllers = await getControllers(memory);
const passThreshold = Math.floor(controllers.length / 2) + 1;
let finalResult;
const proposeResult = await handle({
options: {
Tags: [
Expand All @@ -107,6 +108,7 @@ export async function rubberStampProposal({
},
mem: memory,
});
finalResult = proposeResult;
const proposalNumber = JSON.parse(proposeResult.Messages[0].Data).proposalNumber;
let workingMemory = proposeResult.Memory;
for (const controller of controllers.slice(1, passThreshold)) {
Expand All @@ -122,13 +124,34 @@ export async function rubberStampProposal({
},
mem: workingMemory,
});
finalResult = voteResult;
workingMemory = voteResult.Memory;
}
const proposals = await getProposals(workingMemory);
const maybeProposal = Object.values(proposals).filter((p) => p.proposalNumber === proposalNumber).shift();
assert(!maybeProposal, "Proposal not successfully rubber stamped!");
return {
memory: workingMemory,
proposalNumber
proposalNumber,
result: finalResult,
};
}

// NOTE: Presumes that input array ordering is not precious
export function normalizeObject(obj) {
if (Array.isArray(obj)) {
// Recursively normalize array elements and sort the array
return obj.map(normalizeObject).sort();
} else if (obj !== null && typeof obj === "object") {
// Get keys in alphabetical order
const sortedKeys = Object.keys(obj).sort();

// Create a new object with sorted keys
const normalized = {};
for (const key of sortedKeys) {
normalized[key] = normalizeObject(obj[key]); // Recursively normalize values
}
return normalized;
}
return obj; // Return primitives as-is
}
Loading

0 comments on commit acb489b

Please # to comment.