implement cached delegation reader into proposal

This commit is contained in:
Ricardo Guilherme Schmidt 2019-04-04 03:59:52 -03:00
parent 5af5d523b5
commit 3776e17802
No known key found for this signature in database
GPG Key ID: BFB3F5C8ED618A94
6 changed files with 200 additions and 48 deletions

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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{}

View File

@ -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});
});
})
})