diff --git a/.gitignore b/.gitignore index 3eb225fe6f9ee49c2fa8077e8f43c749716e3737..5a0e84e16d4892f4743ee5a47dbd0436634290b5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ test2network*/bootnode test2network*/contracts/* test2network/*.pid node_modules +src/*/*.bin +src/*/*.abi diff --git a/bin/installbfa.sh b/bin/installbfa.sh index 1f193a9f5dba5408c8b73396ac4f56d7a3242dc4..5af9f0bd9cf1c87f03b99ce48df33ce7758655e8 100755 --- a/bin/installbfa.sh +++ b/bin/installbfa.sh @@ -259,7 +259,7 @@ grep -q Ubuntu /etc/issue && apt-add-repository multiverse # apt update # development tools -aptinstall dirmngr apt-transport-https curl git curl build-essential sudo +aptinstall dirmngr apt-transport-https curl git curl build-essential sudo software-properties-common aptinstall jq libjson-perl libwww-perl libclass-accessor-perl userconfig nodejsinstall diff --git a/src/Majority.sol b/src/Majority.sol deleted file mode 100644 index 07fafc28a8f116b0d93a080297a49b104ef54fb9..0000000000000000000000000000000000000000 --- a/src/Majority.sol +++ /dev/null @@ -1,250 +0,0 @@ -// Robert Martin-Legene <robert@nic.ar> -// vim:syntax:filetype=javascript:ai:sm - -pragma solidity ^0.5; - -// This contract is supposed to maintain a list of accounts authorized -// to control members of "the club" using a majority (n/1+1). -// For instance, could be useful for -// - a list of sealers -// - a board of directors -// - a local chess club -// - even outsourcing this kind of management from another smart contract. - -contract Majority { - // This struct contains a list of votes and who has voted for each victim/beneficiary. - // Votes taking longer than 30K blocks, will have to restart. - struct Proposal { - address victim; // Whom are we voting about. - uint votestart; // In which block did this vote begin? - bool promotion; // true=promotion, false=demotion - address[] voters; // List of voters. - } - address[] public voters; - Proposal[] public voterproposals; - - event voteCast( address voter, address victim, bool promotionOrDemotion ); - event adminChange( address admin, bool promotionOrDemotion ); - - constructor() public - { - // Don't want anyone to find a way to trigger the constructor again. - if ( voters.length == 0 ) - voters.push( msg.sender ); - } - - // This function is used to know how many voters exist. - function votersLength() public view returns (uint) - { - return voters.length; - } - - // This function is to be used to get the address from a specific position in the list. - // Remember first position is 0. - // Last position is voters.length-1. - function voterPosition( uint idx ) public view returns (address) - { - require( idx < voters.length, "There are not that many entries in the list." ); - return voters[idx]; - } - - function isVoter( address subject ) public view returns (bool) - { - return ( _findAddressInList( voters, subject ) > 0 ); - } - - // Returns an index to the position of the needle inside haystack. - // Beware that: - // 0 = not found. - // 1 = first position in a list (so this is actually the real list position + 1). - function _findAddressInList( address[] memory haystack, address needle ) private pure returns (uint) - { - uint i = haystack.length; - while ( i-- > 0 ) - if ( needle == haystack[i] ) - return i+1; - return 0; - } - - function _remove_proposal( uint idx ) private - { - // Move all items in the list (after idx) one step closer to the - // front of the list. - uint max = voterproposals.length; - while ( ++idx < max ) - voterproposals[idx-1] = voterproposals[idx]; - // "pop" the end of the list, making the list shorter. - voterproposals.length--; - } - - function _del_votes( address victim ) private - { - uint i_max = voterproposals.length; - uint i = i_max; - // check all proposals - while ( i-- > 0 ) - { - // check all voters for every proposal - uint j_max = voterproposals[i].voters.length; - uint j = j_max; - while ( j-- > 0 ) - if ( voterproposals[i].voters[j] == victim ) - { - // Found the victim as voter, but since he is about - // to be deleted, we will remove him from the list. - uint k = j; - while ( ++k < j_max ) - voterproposals[i].voters[k-1] = voterproposals[i].voters[k]; - voterproposals[i].voters.length--; - j_max--; - if ( voterproposals[i].voters.length == 0 ) - { - _remove_proposal( i ); - i_max--; - } - } - } - } - - function _remove_voter( address victim ) private - { - // Remove votes that the victim has already cast. - _del_votes( victim ); - // Move all items in the list (after match) one step closer to the - // front of the list. - uint max = voters.length; - uint i = max; - while ( i-- > 0 ) - if ( voters[i] == victim ) - { - // We could have recycled 'i' here, but for clarity, we don't. - uint j = i; - while ( ++j < max ) - voters[j-1] = voters[j]; - // "pop" the end of the list, making the list shorter. - voters.length--; - return; - } - } - - // This function sees if any proposals have overstayed their welcome and thus - // needs to be removed. This is a bit like spring cleaning. We remove proposals - // that are over 30k blocks old. - function _trimProposals() private - { - uint i = voterproposals.length; - while ( i-- > 0 ) - { - // If a proposal is more than 30K blocks old, then remove it from the list. - if ( voterproposals[i].votestart + 30000 <= block.number ) - _remove_proposal( i ); - } - } - - // We run through the entire list of proposals, checking if they fulfill the - // requirements. Why the whole list? Because if a voter has been removed, whom has - // not yet voted for a proposal, that proposal may now have achieved majority. - function _promotedemote() private - { - uint prevlength = 0; - // Keep looping over the list until the number of proposals stops changing. - while ( prevlength != voterproposals.length ) - { - uint i = voterproposals.length; - prevlength = i; - uint majority = voters.length / 2 + 1; - // Loop over all proposals - while ( i-- > 0 ) - { - // If we have enough votes to perform the actual promotion/demotion - if ( voterproposals[i].voters.length >= majority ) - { - // Is it a promotion or a demotion? - if ( voterproposals[i].promotion ) - // Add victim to member list - voters.push( voterproposals[i].victim ); - else - // Remove victim from member list - _remove_voter( voterproposals[i].victim ); - - // Send notification - emit adminChange( voterproposals[i].victim, - voterproposals[i].promotion ); - // Remove the proposal because the voting is complete. - _remove_proposal( i ); - } - } - } - } - // Returns an index to the position of the proposal inside matching the [victim,promotion] tuple - // Beware that: - // 0 = not found. - // 1 = first position. - function promotionIdx( address victim, bool promotion ) public view returns (uint) - { - uint i = voterproposals.length; - while ( i-- > 0) - if ( voterproposals[i].victim == victim && voterproposals[i].promotion == promotion ) - return i+1; - return 0; - } - - // You can call this for free and know if your promote call - // will get accepted. Save the network, call this first. - function mayVote( address voter, address victim, bool promotion ) public view returns (bool) - { - // Is caller a voter? - if ( !isVoter(voter) ) - return false; - - // Can't promote someone who is already a voter. - bool victimVoter = isVoter( victim ); - if ( victimVoter && promotion ) - return false; - - // Can't demote someone who is not a member. - if ( !victimVoter && !promotion ) - return false; - - // See if the voter is already in the list of voters for this [victim,promotion] tuple - uint proppos = promotionIdx( victim, promotion ); - if ( proppos > 0 && _findAddressInList( voterproposals[proppos-1].voters, voter ) > 0 ) - return false; - return true; - } - - // Calling this function will vote for adding an additional or - // removing an existing member of "the inner circle". - // As per usual, this requires n/2+1 votes. - // The boolean must be true if you want to add a member - // and false if you want to remove one. - function vote( address victim, bool promotion ) public - { - if ( ! mayVote(msg.sender, victim, promotion)) - revert("That seems redundant or is otherwise not allowed."); - _trimProposals(); - - // Send notification of the vote - emit voteCast( msg.sender, victim, promotion ); - - uint proppos = promotionIdx( victim, promotion ); - if ( proppos == 0 ) - { - // This is a new proposal, so we will add it, so we can vote on it. - address[] memory emptyAddressList; - proppos = voterproposals.push( Proposal(victim, block.number, promotion, emptyAddressList ) ); - } - proppos--; - - // Add our vote - voterproposals[proppos].voters.push( msg.sender ); - - // See if we're ready to promote/demote anyone, based on the proposals. - _promotedemote(); - - // If we have no more voters, we have no reason to live. - if ( voters.length == 0 ) - selfdestruct( msg.sender ); - } -} - diff --git a/src/Majority/Majority-test.js b/src/Majority/Majority-test.js new file mode 100755 index 0000000000000000000000000000000000000000..dbf655d2122f0bc84b12a618c75128262ed17dca --- /dev/null +++ b/src/Majority/Majority-test.js @@ -0,0 +1,402 @@ +#!/usr/bin/node + +"use strict" + +const Libbfa = require( process.env.BFAHOME + '/bin/libbfa.js'); +const BigInteger = require( 'big-integer' ); +const fs = require( 'fs' ); +var bfa = new Libbfa(); +var web3 = bfa.newweb3(); +// default +var gasprice = BigInteger(10).pow(9); +// globals +var accounts; +var thecontract; + +function cat(filename) +{ + try { + return fs.readFileSync( filename ).toString(); + } catch (err) { + throw( 'Errors: ' + err.syscall + ' ' + err.path + ': ' + err.code); + } +} + +function plural(num, one, many) +{ + if (num == 1) + return one; + return many; +} + +function isittrue( thebool, errortext ) +{ + return new Promise((resolve,reject) => { + if ( undefined == thebool ) + reject( "thebool is undefined: " + errortext ); + else + if ( thebool == true ) + { + console.log( "Test OK: " + errortext ); + resolve(); + } + else + reject( errortext ); + }) +} + +function echo( txt ) +{ + return new Promise((resolve,reject) => { + console.log( txt ); + resolve(); + }) +} + +function getgasprice() +{ + return new Promise( (resolve, reject) => { + web3.eth.getGasPrice() + .then( + (sat) => { + gasprice = BigInteger(sat); + resolve(); + }, + reject + ) + }) +} + +function unlockall() +{ + return new Promise((resolve,reject) => { + var proms = new Array(); + accounts.forEach( + (addr) => { + proms.push( + web3.eth.personal.unlockAccount( addr, '', 600 ) + ); + } + ) + Promise.all( proms ) + .then( resolve ) + .catch( reject ); + }) +} + +function gettestaccountnames() +{ + return new Promise( (resolve, reject) => { + accounts = new Array(); + web3.bfa.personal.listWallets( + ( walletlist ) => { + walletlist.forEach( + ( wallet ) => { + wallet.accounts.forEach( + ( account ) => { + var addr = account.address; + if ( addr.startsWith('0x7e57') ) + accounts.push( addr ); + } + ) + } + ) + accounts.sort(); + resolve(); + } + ); + }) +} + +function getaccountbalances() +{ + return new Promise((resolve,reject) => { + if ( accounts.length < 4 ) + throw( "Too few test accounts (found " + accounts.length + ", but need 4)." ); + var proms = new Array(); + var minimum = BigInteger(10).pow(9); + var failtxt; + accounts.forEach( + ( acct ) => { + proms.push( + web3.eth.getBalance(acct) + .then( + ( bal ) => { + var val = BigInteger( bal ); + if ( val.lesser( minimum ) ) + { + if ( undefined == failtxt ) + failtxt += + "The minimum balance on each test account must be at least " + + minimum + + " satoshi.\n" + failtxt += + "The account balance of " + + accounts[i] + + " is " + + val + + " satoshi.\n" + } + } + ) + ) + } + ); + Promise.all( proms ) + .then( + () => { + if ( undefined == failtxt ) + resolve(); + else + reject( + failtxt + + "Tests can not be performed without balance on the accounts." + ); + } + ); + }); +} + +function councillistdetails() +{ + return new Promise((resolve,reject) => { + thecontract.methods.councilLength.call() + .then( + (howmany) => { + console.log( + 'The council has ' + + howmany + + ' member' + + plural(howmany,'','s') + + '.'); + return( howmany ); + } + ) + .then( + (howmany) => { + var proms = new Array(); + for ( var i=0 ; i<howmany ; i++ ) + proms.push( thecontract.methods.council(i).call() ); + Promise.all( proms ) + .then( (addrlist) => { + addrlist.forEach( (addr)=>{console.log("Council member: "+addr)} ); + resolve( howmany ); + }); + } + ); + }) +} + +function voteslistdetails() +{ + return new Promise((resolve,reject) => { + thecontract.methods.votesLength.call() + .then( + (howmany) => { + console.log( + 'There ' + + plural(howmany,'is ','are ') + + howmany + + ' vote' + + plural(howmany,'','s') + + ' registered.'); + return( howmany ); + } + ) + .then( + (howmany) => { + var proms = new Array(); + for ( var i=0 ; i<howmany ; i++ ) + proms.push( thecontract.methods.votes(i).call() ); + Promise.all( proms ) + .then( (list) => { + list.forEach( (obj)=>{ + console.log( + 'Lodged vote: ' + + obj.voter + + ' has voted to ' + + (obj.promotion?'promote':'demote') + + ' ' + + obj.victim + ); + } ); + resolve( howmany ); + }); + } + ); + }) +} + +function deploynew() +{ + return new Promise((resolve, reject) => { + var abi = JSON.parse( cat('Majority.abi').trim() ); + var bin = cat('Majority.bin').trim(); + var cAddress; + var fetus = new web3.eth.Contract( abi ); + console.log( "Deploying contract." ); + var timeout = BigInteger( 86400 ); + fetus.deploy( + { + data: '0x'+bin, + arguments: [ '0x'+timeout.toString(16) ] + } + ) + .send( + { + from: accounts[0], + gas: 3000000, + gasPrice: '0x'+gasprice.toString(16), + } + ) + .on( 'transactionHash', (hash) => { + console.log( "Deployed in txhash " + hash + "." ); + }) + .on( 'confirmation', (num,obj) => { + if ( undefined == cAddress ) + { + cAddress = obj.contractAddress; + thecontract = new web3.eth.Contract( abi, cAddress ); + console.log( "Contract is at " + cAddress ); + resolve(); + } + }) + .on( 'error', reject ); + }); +} + +function isCouncil( acctpos ) +{ + return new Promise((resolve,reject) => { + var acct = accounts[acctpos]; + thecontract.methods.isCouncil(acct).call() + .then( + (yesno) => { + var not = 'not '; + if (yesno) + not = ''; + console.log( acct + ' is ' + not + 'a council member.' ); + resolve(yesno); + } + ) + .catch( reject ) + }) +} + +function vote( voterpos, victimpos, promotion ) +{ + return new Promise((resolve,reject) => { + var voter = accounts[voterpos]; + var victim = accounts[victimpos]; + var confirmed = false; + var demote = 'demote'; + if (promotion) + demote = 'promote'; + console.log( voter + " voting to " + demote + " " + victim ); + thecontract.methods.vote( victim, promotion ).send({ + from: voter, + gasPrice: "0x"+gasprice.toString(16), + gas: '0x'+BigInteger(10).pow(6).toString(16) + }) + .on( 'transactionHash', (txhash) => { + console.log( ' - txhash ' + txhash ); + }) + .on( 'receipt', (rcpt) => { + console.log( ' - got a receipt' ); + }) + .on( 'confirmation', (num,obj) => { + if ( ! confirmed ) + { + confirmed = true; + resolve(); + } + }) + .on( 'error', reject ) + }) +} + +function mayVote(voterpos, victimpos, promotion) +{ + return new Promise((resolve,reject) => { + var voter = accounts[voterpos]; + var victim = accounts[victimpos]; + var demote = 'demote'; + if (promotion) + demote = 'promote'; + thecontract.methods.mayVote( voter, victim, promotion ).call() + .then( (may) => { + var not = 'not '; + if ( may ) + not = ''; + console.log( voter + " may " + not + "vote to " + demote + " " + victim ); + resolve( may ); + }, + reject + ); + }) +} + +getgasprice() +.then( gettestaccountnames ) +.then( getaccountbalances ) +.then( unlockall ) +.then( deploynew ) + +// initial conditions after deploying the contract +.then( councillistdetails ) +.then( (n) => {return isittrue( n==1, "There should be 1 account in the council list." )}) +.then( voteslistdetails ) +.then( (n) => {return isittrue( n==0, "There should be no entries in the list of registered votes." )}) +.then( () => {return isCouncil(0)} ) +.then( (b) => {return isittrue( b, "Account should be a voter." )}) +.then( () => {return isCouncil(1)} ) +.then( (b) => {return isittrue( !b, "Account should not be a voter." )}) + +// Adding second account to the council - takes effect immediately, so votes.length == 0 +.then( () => {return mayVote(0,1,true)} ) +.then( (b) => {return isittrue( b, "Account should be allowed to vote for the approval of the new account." )}) +.then( () => {return vote(0,1,true)} ) +.then( voteslistdetails ) +.then( (n) => {return isittrue( n==0, "There should be no entries in the list of registered votes." )}) +.then( councillistdetails ) +.then( (n) => {return isittrue( n==2, "There should be 2 accounts in the council list." )}) +// Start voting to include third account +.then( () => {return mayVote(1,2,true)} ) +.then( (b) => {return isittrue( b, "Account should be allowed to vote for the approval of the new account." )}) +.then( () => {return vote(1,2,true)} ) +.then( voteslistdetails ) +.then( (n) => {return isittrue( n==1, "There should be 1 entry in the list of registered votes." )}) +.then( councillistdetails ) +.then( (n) => {return isittrue( n==2, "There should be 2 accounts in the council list." )}) +// Start voting to remove second account (using second account) +.then( () => {return mayVote(1,1,false)} ) +.then( (b) => {return isittrue( b, "Account should be allowed to vote for the removal of the account." )}) +.then( () => {return vote(1,1,false)} ) +.then( voteslistdetails ) +.then( (n) => {return isittrue( n==2, "There should be 2 entries in the list of registered votes." )}) +.then( councillistdetails ) +.then( (n) => {return isittrue( n==2, "There should be 2 accounts in the council list." )}) +// Finalizing vote to remove second account +.then( () => {return mayVote(0,1,false)} ) +.then( (b) => {return isittrue( b, "Account should be allowed to vote for the removal of the account." )}) +.then( () => {return vote(0,1,false)} ) +.then( voteslistdetails ) +.then( (n) => {return isittrue( n==0, "There should be no entries in the list of registered votes." )}) +.then( councillistdetails ) +.then( (n) => {return isittrue( n==1, "There should be 1 account in the council list." )}) +// Vote to remove 3rd account. +.then( () => {return mayVote(0,2,true)} ) +.then( (b) => {return isittrue( b, "Account should be allowed to vote for the approval of the new account." )}) +.then( () => {return vote(0,2,true)} ) +.then( voteslistdetails ) +.then( (n) => {return isittrue( n==0, "There should be no entries in the list of registered votes." )}) +.then( councillistdetails ) +.then( (n) => {return isittrue( n==2, "There should be 1 account in the council list." )}) + +// this should self-destruct the contract +.then( () => {return vote(0,0,false)} ) +.catch( (x) => {bfa.fatal("Test FAIL: " + x)}) +.finally( () => { + console.log('** All tests completed successfully **'); + process.exit(); +}); diff --git a/src/Majority/Majority.sol b/src/Majority/Majority.sol new file mode 100644 index 0000000000000000000000000000000000000000..31c5bff342c2f688cfa755b436ac1949eaa56492 --- /dev/null +++ b/src/Majority/Majority.sol @@ -0,0 +1,201 @@ +// Robert Martin-Legene <robert@nic.ar> +// vim:syntax:filetype=javascript:ai:sm + +pragma solidity ^0.5; + +// This contract is supposed to maintain a list of accounts authorized +// to control members of "the club" using a majority (n/1+1). We call +// that group the "council". +// For instance, could be useful for +// - a list of sealers +// - a board of directors +// - a local chess club +// - even outsourcing this kind of management from another smart contract. + +contract Majority { + // This struct contains a list of votes and who has voted for each victim/beneficiary. + struct Vote { + address voter; // Voter. + address victim; // Whom are we voting about. + uint voteStart; // When was the vote cast + bool promotion; // true=promotion, false=demotion + } + address[] public council; + Vote[] public votes; + uint public votetimeout = 604800; // default a week + + event voteCast( address voter, address victim, bool promotion ); + event adminChange( address admin, bool promotion ); + + constructor( uint timeout ) public + { + if ( timeout >= 3600 ) + votetimeout = timeout; + council.push( address(msg.sender) ); + } + + function setTimeout( uint timeout ) public + { + if ( ! isCouncil(msg.sender) ) + revert("Only council members may use this function."); + if ( timeout >= 3600 ) + votetimeout = timeout; + } + + function councilLength() public view returns (uint) + { + return council.length; + } + + function votesLength() public view returns (uint) + { + return votes.length; + } + + // True or false if the subject is a member of the council. + function isCouncil( address subject ) public view returns (bool) + { + uint i = council.length; + while ( i-- > 0 ) + if ( subject == council[i] ) + return true; + return false; + } + + // Move all items in the list (after idx) one step closer to the + // front of the list. + function _remove_vote( uint idx ) private + { + uint max = votes.length; + while ( ++idx < max ) + votes[idx-1] = votes[idx]; + // "pop" the end of the list, making the list shorter. + votes.length--; + } + + function _remove_council_member( address exmember ) private + { + // Remove votes that the exmember has already cast. + uint i = votes.length; + while ( i-- > 0 ) + if ( votes[i].voter == exmember ) + _remove_vote( i ); + // Move all items in the council list (after match) one step closer to the + // front of the list. + i = council.length; + while ( i-- > 0 ) + { + if ( council[i] == exmember ) + { + uint idx = i; + uint max = council.length; + while ( ++idx < max ) + council[idx-1] = council[idx]; + // "pop" the end of the list, making the list shorter. + council.length--; + return; + } + } + } + + // We run through the entire list of votes, checking if they fulfill the + // requirements. + function _promotedemote( address victim, bool promotion ) private + { + uint numvotes = 0; + uint majority = council.length / 2 + 1; + uint i = votes.length; + while ( i-- > 0 ) + if (( votes[i].victim == victim ) + && ( votes[i].promotion == promotion ) + ) + numvotes++; + // If we don't have enough votes to perform the actual promotion/demotion + if ( numvotes < majority ) + return; + // Is it a promotion or a demotion? + if ( promotion ) + // Add victim to member list + council.push( victim ); + else + // Remove victim from member list + _remove_council_member( victim ); + // Send notification + emit adminChange( victim, promotion ); + // Remove the vote because the voting is complete. + i = votes.length; + while ( i-- > 0 ) + if (( votes[i].victim == victim ) + && ( votes[i].promotion == promotion ) + ) + _remove_vote( i ); + } + + // You can call this for free and know if your promote call + // will get accepted. Save the network, call this first. + function mayVote( address voter, address victim, bool promotion ) public view returns (bool) + { + bool voterIsOnCouncil = isCouncil( voter ); + bool victimIsOnCouncil = isCouncil( victim ); + + if ( ! voterIsOnCouncil ) + return false; + + // Can't promote someone who is already on the council + if ( victimIsOnCouncil && promotion ) + return false; + + // Can't demote someone who is not a council member. + if ( !victimIsOnCouncil && !promotion ) + return false; + + // See if he is trying to cast a vote already registered + uint ancient = block.timestamp - votetimeout; + uint i = votes.length; + while ( i-- > 0 ) + { + if ( (votes[i].voter == voter) + && (votes[i].victim == victim) + && (votes[i].promotion == promotion) + && (votes[i].voteStart > ancient) + ) + return false; + } + return true; + } + + // Calling this function will vote for adding an additional or + // removing an existing member of council. + // This requires n/2+1 votes (more than 50%). + // The boolean must be true if you want to add a member + // and false if you want to remove one. + function vote( address victim, bool promotion ) public + { + if ( ! mayVote(msg.sender, victim, promotion)) + revert("That seems redundant or is otherwise not allowed."); + // A little house keeping - if a vote is too old, then remove it + uint ancient = block.timestamp - votetimeout; + uint i = votes.length; + while ( i-- > 0 ) + if ( votes[i].voteStart < ancient ) + _remove_vote( i ); + // End of house keeping + // Store the vote + votes.push( + Vote( + msg.sender, + victim, + block.timestamp, + promotion + ) + ); + // Send notification of the vote + emit voteCast( address(msg.sender), victim, promotion ); + // See if we're ready to promote/demote anyone, based on the votes. + _promotedemote( victim, promotion ); + + // If we have no more council members, then we have no reason to continue living. + if ( council.length == 0 ) + selfdestruct( msg.sender ); + } +} diff --git a/src/Majority/README.md b/src/Majority/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6e918289c3d916ef60fb5b7a776f57f4d3d36a76 --- /dev/null +++ b/src/Majority/README.md @@ -0,0 +1,99 @@ +# Majority + +This contract maintains a list of accounts authorized +to control members of "the club" using a majority (n/1+1). +We call that group the "council". + +For instance, could be useful for + * a list of sealers + * a board of directors + * a local chess club + * even outsourcing this kind of management from another smart contract. + +There are a few functions that can be used to see the contents of the +data structures used. Usually, you probably only need to **vote()** and +to ask if some account **isCouncil()** + +See the test suite for examples of how to extract information from the +different datastructures, if you feel you really need that. + +## Events + +### voteCast + +**voteCast( address voter, address victim, bool promotion )** + +Gives the address of who voted, who they voted for and if it should +be a promotion (true) or demotion (false). + +A promotion means to become a member of the council. + +A demotion means to be removed from the list of council members. + +### adminChange + +**adminChange( address admin, bool promotion )** + +This event is emitted when an address has received enough votes to be +promoted or demoted and that action is taken. + +## Functions + +### constructor + +**constructor( uint timeout )** + +This function is called when the contract is getting deployed. + +The deploying address automatically becomes the only council member and +needs to vote to include other's. The contract creator has no special +powers other than other council members, and as such can be voted out +and lose all control over the contract. + +If you specify an integer when deploying the contract, you can change +the time it takes for a vote to time out. The timeout can not be set +lower than one hour. The default is 7 days. + +### setTimeout + +**setTimeout( uint timeout )** + +Change the timeout (in seconds) for validity of votes cast. +Any council member can change the timeout. +The timeout can not be set lower than one hour. + +### councilLength + +**councilLength()** + +Returns a uint telling how many entries are in the council list. + +### votesLength + +**votesLength()** + +Returns a uint telling how many structs are in the votes array. + +### isCouncil + +**isCouncil( address subject )** + +Returns true or false whether the given address is a member of the +council. + +### mayVote + +**mayVote( address voter, address victim, bool promotion )** + +Returns true or false if a certain address may vote a certain vote for +a certain address. + +### vote + +**vote( address victim, bool promotion )** + +Performs actual voting on including (true) or removing (false) an address +from the council. + +If the final member of the council votes to remove itself, the contract +will self-destruct. diff --git a/src/Majority/test.sh b/src/Majority/test.sh new file mode 120000 index 0000000000000000000000000000000000000000..f32190cf204c3de403bb87332ac09b9eea67256b --- /dev/null +++ b/src/Majority/test.sh @@ -0,0 +1 @@ +../test/test-harness-v1.sh \ No newline at end of file diff --git a/src/test/README.md b/src/test/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e8aa778c240521e80ef15f43e05c30bf3c766513 --- /dev/null +++ b/src/test/README.md @@ -0,0 +1,2 @@ +Esta carpeta contiene llaves privadas. +Cuidado usar las llavas para algo importante (fuera de pruebas). diff --git a/src/test/test-harness-v1.sh b/src/test/test-harness-v1.sh new file mode 100755 index 0000000000000000000000000000000000000000..4b2fa254fd00b704c3c51fd0bea6b11bb098443a --- /dev/null +++ b/src/test/test-harness-v1.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +true ${BFAHOME:=$( echo ~bfa/bfa )} +source ${BFAHOME}/bin/libbfa.sh || exit 1 + +function testfatal +{ + test -n "$1" && echo $* + exit 1 +} + +function testcleanup +{ + for filename in ${keyfiles} + do + test -f "${filename}" + rm "${filename}" + done +} + +function writetestkeysto +{ + local destinationdirectory=$1 + for i in {1..4} + do + case $i in + 1) + set 7e5705d6e4fd660ad6d2d6533f3b4b6647dbb0a2 ac6c3c3e7351f858a3ffd9db00dc70b72a16e5ab0723be0a3da6da228778ce29 717f2930a84107df56fcb3308c116cfe 64fc98daae4a96777b8c08396a92bc8dde1117d439c7eb0b9a016a11e512dc0a 89e869cae11f4a9b3d1f8d684119a64d6c1b25451152b5fb413c531813f83b0d 0643d804-b9df-4df9-8819-d38463480f15 + ;; + 2) + set 7e57105001438399e318753387b061993753a545 3d3f422c4c227a37db5728e72c91e43cbd391f7e338fe6aaeb68b65416b33b48 676cf0e07562c52f6c933de714c0506f 8e9869dc6aab090ff389ecc84815d0abc108f437d11c1d56a91c83277c77acd5 87237d3a481ab6a1c50b9ef07b24c0118644e7bf3b27170766ec6b860300890d ee0c6421-b0f1-41db-934e-3cab0c419875 + ;; + 3) + set 7e57215a8af47c70baca431c45ecf14199f4bd9a 84c5a655cdd6a402ef88ea1f64a4c427ce3cdaec88f352c0194254095b8b71c4 80e54ea9307624e2d4ec7179f2fcfa3c 9aaeba47fbb5e7ba8d327a1a544712184424021db8e4d238f60bdba81f02a4ca 7ef19ab55611ea649b65de754fb68d5f810550fe9ccfcfd2f067d3e56b4aee04 4b1c00d1-292f-4610-bbc0-2bb4d6489ea4 + ;; + 4) + set 7e57348aec1b1fd574ef2c0702acaa197c46d613 2d6e3b21ab6c69ad789703acc967f93955690626333c667bc34e48650bf95d59 fc98dd18e1292cb5c6578ecbbc29cbb3 75cc35ea980da5b70aa146ac02fa9c3fd9d0015cdda8b6737ba17ba95f24cd8a fcb3ed29dfe7ec444c929e880ecc822af894ee497de7e29484ecccfcd7187b8b 05d45b22-185e-49d4-bc91-9ab684e8e75a + ;; + *) + false + ;; + esac + local address=$1 + local ciphertext=$2 + local iv=$3 + local salt=$4 + local mac=$5 + local id=$6 + local cipherparams="{\"iv\":\"${iv}\"}" + local kdfparams="{\"dklen\":32,\"n\":262144,\"p\":1,\"r\":8,\"salt\":\"${salt}\"}" + local crypto="{\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"${ciphertext}\",\"cipherparams\":${cipherparams},\"kdf\":\"scrypt\",\"kdfparams\":${kdfparams},\"mac\":\"${mac}\"}" + local thisfilename="${destinationdirectory}/UTC--2018-12-24T12-00-00.000000000Z--${address}" + echo "{\"address\":\"${address}\",\"crypto\":${crypto},\"id\":\"${id}\",\"version\":3}" > ${thisfilename} + keyfiles="${keyfiles} ${thisfilename}" + done +} + +keyfiles= +trap testfatal ERR +trap testcleanup EXIT +contractname=$( ls -1 *.sol | head -1 | sed 's/\.sol$//' ) +test -f ${contractname}.sol +test -r ${contractname}.sol +solc --combined-json abi,bin ${contractname}.sol > ${contractname}.json +jq -r ".contracts.\"${contractname}.sol:${contractname}\".abi" < ${contractname}.json > ${contractname}.abi +jq -r ".contracts.\"${contractname}.sol:${contractname}\".bin" < ${contractname}.json > ${contractname}.bin +rm ${contractname}.json +writetestkeysto ${BFANODEDIR}/keystore +for tester in ${contractname}-test* +do + if [ -n "${tester}" -a -x "${tester}" -a -r "${tester}" ] + then + ./${tester} + fi +done