From 3776e1780255a1f1b7c2344f1a15282671ca5972 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt Date: Thu, 4 Apr 2019 03:59:52 -0300 Subject: [PATCH] implement cached delegation reader into proposal --- .../democracy/delegation/DelegationReader.sol | 64 +++++++++++ contracts/democracy/proposal/Proposal.sol | 3 +- .../democracy/proposal/ProposalAbstract.sol | 15 ++- contracts/democracy/proposal/ProposalBase.sol | 56 ++++----- contracts/democracy/proposal/ProposalInit.sol | 2 +- test/proposal.js | 108 +++++++++++++++--- 6 files changed, 200 insertions(+), 48 deletions(-) create mode 100644 contracts/democracy/delegation/DelegationReader.sol diff --git a/contracts/democracy/delegation/DelegationReader.sol b/contracts/democracy/delegation/DelegationReader.sol new file mode 100644 index 0000000..dae3905 --- /dev/null +++ b/contracts/democracy/delegation/DelegationReader.sol @@ -0,0 +1,64 @@ +pragma solidity >=0.5.0 <0.6.0; + +import "./Delegation.sol"; + +contract DelegationReader { + Delegation delegation; + mapping(address => address) delegationOf; + + function validDelegate( + address _who + ) + internal + view + returns(bool); + + + function precomputeDelegateOf( + address _who, + uint _block, + bool _revalidate + ) + internal + { + delegationOf[_who] = _revalidate ? delegateOfAt(_who, _block) : cachedDelegateOfAt(_who, _block); + } + + function delegateOfAt( + address _who, + uint _block + ) + internal + view + returns(address delegate) + { + delegate = _who; + do { + delegate = delegation.delegatedToAt(delegate, _block); + } while (!validDelegate(delegate)); + } + + function cachedDelegateOfAt( + address _who, + uint _block + ) + internal + view + returns(address delegate) + { + delegate = _who; + do { + address delegationOfd = delegationOf[delegate]; + if(delegationOfd != address(0)){ + return delegationOfd; + }else { + delegate = delegation.delegatedToAt(delegate, _block); + } + } while (!validDelegate(delegate)); + + } + + + + +} \ No newline at end of file diff --git a/contracts/democracy/proposal/Proposal.sol b/contracts/democracy/proposal/Proposal.sol index ab860cb..c1a29ff 100644 --- a/contracts/democracy/proposal/Proposal.sol +++ b/contracts/democracy/proposal/Proposal.sol @@ -32,7 +32,8 @@ interface Proposal { bytes32[] calldata _proof, bytes calldata _signature ) external; - function tabulateDelegated(address _voter) external; + + function tabulateDelegated(address _source, bool _cached) external; function precomputeDelegation(address _start, bool _clean) external; function finalize() external; function clear() external; diff --git a/contracts/democracy/proposal/ProposalAbstract.sol b/contracts/democracy/proposal/ProposalAbstract.sol index f04b30b..2a91a04 100644 --- a/contracts/democracy/proposal/ProposalAbstract.sol +++ b/contracts/democracy/proposal/ProposalAbstract.sol @@ -4,6 +4,7 @@ import "../../common/Controlled.sol"; import "../../deploy/InstanceAbstract.sol"; import "../../token/MiniMeToken.sol"; import "../delegation/Delegation.sol"; +import "../delegation/DelegationReader.sol"; import "./Proposal.sol"; /** @@ -11,10 +12,9 @@ import "./Proposal.sol"; * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) * Store votes and tabulate results for Democracy. */ -contract ProposalAbstract is InstanceAbstract, Proposal, Controlled { +contract ProposalAbstract is InstanceAbstract, DelegationReader, Proposal, Controlled { MiniMeToken public token; - Delegation public delegation; uint256 public tabulationBlockDelay; bytes32 public dataHash; @@ -27,7 +27,6 @@ contract ProposalAbstract is InstanceAbstract, Proposal, Controlled { //tabulation process uint256 public lastTabulationBlock; - mapping(address => address) public delegationOf; mapping(address => address) public tabulated; mapping(uint8 => uint256) public results; @@ -50,4 +49,14 @@ contract ProposalAbstract is InstanceAbstract, Proposal, Controlled { require(lastTabulationBlock + tabulationBlockDelay < block.number, "Tabulation not ended"); _; } + + function validDelegate( + address _who + ) + internal + view + returns(bool) + { + return voteMap[_who] != Vote.Null; + } } \ No newline at end of file diff --git a/contracts/democracy/proposal/ProposalBase.sol b/contracts/democracy/proposal/ProposalBase.sol index c3573b9..afc867e 100644 --- a/contracts/democracy/proposal/ProposalBase.sol +++ b/contracts/democracy/proposal/ProposalBase.sol @@ -95,30 +95,31 @@ contract ProposalBase is ProposalAbstract, MessageSigned { * @dev might run out of gas, to prevent this, precompute the delegation * Should be called every time a nearer delegate tabulate their vote * @param _source holder which not voted but have a delegate that voted + * @param _cached true if should use lookup values from precomputed */ - function tabulateDelegated(address _source) + function tabulateDelegated(address _source, bool _cached) external tabulationPeriod { - (address _claimer, Vote _vote) = findNearestDelegatable(_source); // try finding first delegate from chain which voted - setTabulation(_source, _claimer, _vote); - } + address claimer = _cached ? cachedDelegateOfAt(_source, voteBlockEnd): delegateOfAt(_source, voteBlockEnd); + setTabulation(_source, claimer, voteMap[claimer]); + } /** * @notice precomputes a delegate vote based on current votes tabulated * @dev to precompute a very long delegate chain, go from the end to start with _clean false. * @param _start who will have delegate precomputed - * @param _clean if true dont use precomputed results + * @param _revalidate if true dont use precomputed results * TODO: fix long delegate chain recompute in case new votes */ function precomputeDelegation( address _start, - bool _clean + bool _revalidate ) external tabulationPeriod { - cacheDelegation(_start,_clean); + precomputeDelegateOf(_start, voteBlockEnd, _revalidate); } /** @@ -186,6 +187,14 @@ contract ProposalBase is ProposalAbstract, MessageSigned { function getVotePrefixedHash(Vote _vote) external view returns (bytes32) { return getSignHash(voteHash(_vote)); } + + function delegateOf(address _who) external view returns(address) { + return delegateOfAt(_who, voteBlockEnd); + } + + function cachedDelegateOf(address _who) external view returns(address) { + return cachedDelegateOfAt(_who, voteBlockEnd); + } /** * @notice get result @@ -245,34 +254,25 @@ contract ProposalBase is ProposalAbstract, MessageSigned { require(vote == Vote.Null, "Not delegatable"); claimer = _source; // try finding first delegate from chain which voted while(vote == Vote.Null) { - address claimerDelegate = delegationOf[claimer]; - if(claimerDelegate == address(0)){ - claimerDelegate = delegation.delegatedToAt(claimer, voteBlockEnd); - } - require(claimer != claimerDelegate, "No delegate vote found"); + address claimerDelegate = delegation.delegatedToAt(claimer, voteBlockEnd); claimer = claimerDelegate; vote = voteMap[claimer]; //loads delegate vote. } } - function cacheDelegation(address _source, bool _clean) private returns (address delegate) { - delegate = _source; - if(voteMap[_source] == Vote.Null) { - if(!_clean) { - delegate = delegationOf[delegate]; - } - if(delegate == address(0)){ - delegate = delegation.delegatedToAt(_source, voteBlockEnd); //get delegate chain tail + function cachedFindNearestDelegatable(address _source) internal view returns (address claimer, Vote vote){ + vote = voteMap[_source]; + require(vote == Vote.Null, "Not delegatable"); + claimer = _source; // try finding first delegate from chain which voted + while(vote == Vote.Null) { + address claimerDelegate = delegationOf[claimer]; + if(claimerDelegate == address(0)){ + claimerDelegate = delegation.delegatedToAt(claimer, voteBlockEnd); } + claimer = claimerDelegate; + vote = voteMap[claimer]; //loads delegate vote. } - - require(delegate != address(0), "No delegate vote found"); - if(voteMap[delegate] == Vote.Null) { - delegate = cacheDelegation(delegate, _clean); - } - delegationOf[_source] = delegate; - return delegate; - } + } \ No newline at end of file diff --git a/contracts/democracy/proposal/ProposalInit.sol b/contracts/democracy/proposal/ProposalInit.sol index 50a83e7..4d3150a 100644 --- a/contracts/democracy/proposal/ProposalInit.sol +++ b/contracts/democracy/proposal/ProposalInit.sol @@ -62,7 +62,7 @@ contract ProposalInit is ProposalAbstract { function voteDirect(Vote) external{} function tabulateDirect(address ) external{} function tabulateSigned(Vote, uint256, bytes32[] calldata, bytes calldata) external{} - function tabulateDelegated(address) external{} + function tabulateDelegated(address,bool) external{} function precomputeDelegation(address, bool) external{} function finalize() external{} function clear() external{} diff --git a/test/proposal.js b/test/proposal.js index 3d99b57..d53405a 100644 --- a/test/proposal.js +++ b/test/proposal.js @@ -37,6 +37,17 @@ config({ } }); + + async function delegationOf(contract, influenceSrc) { + let delegation = []; + var curDelegate = influenceSrc; + do { + delegation.push(curDelegate) + curDelegate = await contract.methods.delegatedTo(curDelegate).call(); + }while(!delegation.includes(curDelegate)); + return delegation; +} + function mintTokens(accounts, amount) { return Promise.all( accounts.map((account) => { @@ -68,7 +79,15 @@ async function tabulateDirect(proposal, account) { } async function tabulateDelegated(proposal, account) { - return addGas(proposal.methods.tabulateDelegated(account), web3.eth.defaultAccount); + let nc = proposal.methods.tabulateDelegated(account, false); + let yc = proposal.methods.tabulateDelegated(account, true); + let ng = await nc.estimateGas(); + let yg = await yc.estimateGas(); + var call = nc; + if(yg < ng && await proposal.methods.delegateOf(account).call() == await proposal.methods.cachedDelegateOf(account).call()){ + call = yc; + } + return addGas(call, web3.eth.defaultAccount); } async function tabulateSigned(proposal, sig) { @@ -103,7 +122,7 @@ contract("Proposal", function() { mintTokens(res, initialBalance).then((mintReceipts) => { newDelegation(utils.zeroAddress, defaultDelegate).then((createdRoot) => { RootDelegation = createdRoot; - newDelegation(RootDelegation._address, utils.zeroAddress).then((createdChild) => { + newDelegation(RootDelegation._address, defaultDelegate).then((createdChild) => { ChildDelegation = createdChild; Promise.all([ // root: 0 -> 1 -> 2 -> 3 (-> 5) @@ -119,7 +138,7 @@ contract("Proposal", function() { RootDelegation.methods.delegate(accounts[9]).send({from: accounts[8]}), RootDelegation.methods.delegate(accounts[6]).send({from: accounts[9]}), // child: 5 -> 6 - ChildDelegation.methods.delegate(accounts[7]).send({from: accounts[5]}) + ChildDelegation.methods.delegate(accounts[6]).send({from: accounts[5]}) ]).then((delegateReceipts) => { done(); }) @@ -227,7 +246,7 @@ contract("Proposal", function() { it("reject tabulateDelegated when voting not ended", async function () { assert.equal( - await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[0])), + await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[0], false)), "Voting not ended") }); @@ -257,8 +276,8 @@ contract("Proposal", function() { it("reject tabulates when no delegate voted", async function () {; assert.equal( - await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[4])), - "No delegate vote found") + await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[4], false)), + "revert") }); it("should not have a lastTabulationBlock", async function () { @@ -308,11 +327,11 @@ contract("Proposal", function() { it("should not tabulate for delegate if voted ", async function () { assert.equal( - await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[2])), - "Not delegatable") + await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[2], false)), + "Voter already tabulated") assert.equal( - await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[5])), - "Not delegatable") + await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[5], false)), + "Voter already tabulated") }); it("tabulates approve influence from direct delegate", async function () { @@ -337,7 +356,7 @@ contract("Proposal", function() { it("should not tabulate influence from circular delegation chain when none voted", async function () { assert.equal( - await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[7])), + await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[7], false)), "revert") }); @@ -458,7 +477,7 @@ contract("Proposal", function() { it("reject tabulateDelegated after finalization", async function () { assert.equal( - await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[0])), + await utils.getEVMException(testProposal.methods.tabulateDelegated(accounts[0], false)), "Tabulation ended" ) }); @@ -577,7 +596,7 @@ contract("Proposal", function() { it("clear after finalization", async function () { await testProposal.methods.clear().send({from: web3.eth.defaultAccount}); }); - }) + }); describe("test simple quorum reject", function() { var sigs = []; @@ -674,7 +693,7 @@ contract("Proposal", function() { it("clear after finalization", async function () { await testProposal.methods.clear().send({from: web3.eth.defaultAccount}); }); - }) + }); describe("test simple quorum approve", function() { var sigs = []; @@ -771,6 +790,65 @@ contract("Proposal", function() { it("clear after finalization", async function () { await testProposal.methods.clear().send({from: web3.eth.defaultAccount}); }); - }) + }); + + describe("test delegate precompute", function() { + var sigs = []; + var testProposal; + var blockStart; + var voteBlockEnd; + it("create proposal by factory", async function () { + blockStart = await web3.eth.getBlockNumber(); + + receipt = await ProposalFactory.methods.createProposal( + MiniMeToken._address, + ChildDelegation._address, + "0xDA0", + tabulationBlockDelay, + blockStart, + blockEndDelay, + QUORUM_SIMPLE + ).send() + testProposal = new web3.eth.Contract(ProposalBase._jsonInterface, receipt.events.InstanceCreated.returnValues.instance); + }); + + it("include direct vote", async function () { + + let receipt = await testProposal.methods.voteDirect(VOTE_APPROVE).send({from: accounts[8]}); + + }); + + it("increases block number to vote block end", async function () { + voteBlockEnd = await testProposal.methods.voteBlockEnd().call(); + await utils.setBlockNumber(+voteBlockEnd+1); + assert(await web3.eth.getBlockNumber() > voteBlockEnd, "Wrong block number") + }); + + it("should precompute delegate", async function () { + let gasBefore = await testProposal.methods.tabulateDelegated(accounts[0], true).estimateGas(); + let call = testProposal.methods.precomputeDelegation(accounts[0],true); + await call.send({from: accounts[0], gas: await call.estimateGas()+ 10000 }); + let gasAfter = await testProposal.methods.tabulateDelegated(accounts[0], true).estimateGas(); + assert.equal(await testProposal.methods.cachedDelegateOf(accounts[0]).call(),await testProposal.methods.delegateOf(accounts[0]).call(), "Rendered wrong delegate"); + assert(gasAfter < gasBefore, "Didn't reduced gas usage"); + await tabulateDelegated(testProposal, accounts[0]); + + }); + + it("increses block to tabulation end", async function (){ + await utils.increaseBlock(+tabulationBlockDelay+1); + let lastTabulationBlock = await testProposal.methods.lastTabulationBlock().call(); + assert(await web3.eth.getBlockNumber() > +lastTabulationBlock+tabulationBlockDelay, "Wrong block number") + }); + + it("finalizes after tabulation end", async function (){ + receipt = await testProposal.methods.finalize().send({from: web3.eth.defaultAccount}); + assert.equal(receipt.events.FinalResult.returnValues.result, VOTE_APPROVE) + }); + + it("clear after finalization", async function () { + await testProposal.methods.clear().send({from: web3.eth.defaultAccount}); + }); + }) }) \ No newline at end of file