implement cached delegation reader into proposal
This commit is contained in:
parent
5af5d523b5
commit
3776e17802
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,6 +188,14 @@ contract ProposalBase is ProposalAbstract, MessageSigned {
|
|||
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];
|
||||
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);
|
||||
}
|
||||
if(delegate == address(0)){
|
||||
delegate = delegation.delegatedToAt(_source, voteBlockEnd); //get delegate chain tail
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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{}
|
||||
|
|
106
test/proposal.js
106
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 = [];
|
||||
|
@ -768,6 +787,65 @@ contract("Proposal", function() {
|
|||
assert.equal(receipt.events.FinalResult.returnValues.result, VOTE_APPROVE)
|
||||
});
|
||||
|
||||
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});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue