The purpose of this contract is to support the EOS Referendum system by storing proposals and their related votes in-RAM in the blockchain's state.
It's also possible to create related posts and statuses, but they are not stored in-RAM in the blockchain's state. It allows authenticated messages to go through, where they are visible in the transaction history of the chain. Off-chain tools are needed to sort, display, aggregate, and report on the outputs of the post and status actions.
The propose action is first called providing the proposer's account, proposal's name (its
id among all other proposals), proposal's title, a JSON string for extra metadata
(specification not defined yet) that can be left empty, and an expiration date
(must be no later than 6 months in the future).
Once the proposal has been created, people can start to vote on it via the vote action.
The vote action is called using the voter's account, proposal's name, vote's value (0 for
negative vote and 1 for a positive vote) and a JSON string for extra metadata
(specification not defined yet), which can be left empty.
A vote overwrites any previous value if present. That means that if you voted initially with a
vote value of 0 (negative vote) and you perform a second vote action on the same proposal
this time with a value of 1 (positive vote), your current vote for the proposal is now 1.
There is no decay once you have voted. Once you vote, it does not change, nor is it removed
until you either call unvote, or the proposal has been cleaned up by the clnproposal action.
Once a vote has been cast, a user can remove its vote via the unvote action.
The unvote action is called using the voter's account and proposal's name. An unvote
action completely removes your vote from the proposal and clears the RAM usage
associated to that vote.
While a proposal is still active a proposer can decide to manually expire it
by calling the expire action which receives as its only argument the proposal_name.
This amends the proposal's expires_at field to the current time instead of waiting for
its original expiration date to be reached.
Once a proposal is expired (be it manually or automatically if it passed its expiration date), the
proposal enters a 3 day freeze period. Within this freeze period, the proposal is locked
and no actions can be called on it (no vote changes, no vote removal (unvote) and no clean up).
This is to allow a period where multiple tools can query the results for cross-verification.
Once a proposal has ended its freeze period, it's now possible to clean it via the clnproposal action.
The clnproposal action receives the proposal_name and a max_count value. clnproposal is done in
batch, each batch removing an amount of votes on the proposal (received via the max_count variable).
Once all votes are removed on a proposal, the proposal itself is removed.
The clean proposal effectively reclaims all RAM consumed for votes and for the proposal itself. The RAM is thus given back to voters (for their votes) and to the proposer (for the proposal).
The clnproposal action can be called by anybody, there are no restrictions. There is no risk since
only expired proposals that have passed their freeze period can be cleaned. Thus, no issues can
arise by cleaning proposals.
Prerequisites:
We assume the docker binary is available in your PATH environment as well as eosc and
eos-bios. The eos-bios and eosc binaries are required to correctly boot the local
development & test node as well as running the automated test suite.
Simply call the build.sh script which launches a Docker container and
compiles the contract:
./build.sh
You can easily start a development node using the run.sh script, which uses
eos-bios and Docker to launch a
fully configured sandboxed nodeos development node:
./run.sh
This creates the following accounts:
eosforumrcppproposer1proposer2poster1poster2voter1voter2zzzzzzzzzzzz
All accounts created in the development node above use the following public/private key pair:
- Public:
EOS5MHPYyhjBjnQZejzZHqHewPWhGTfQWSVTWYEhDmJu4SXkzgweP - Private:
5JpjqdhVCQTegTjrLtCSXHce7c9M8w7EXYZS7xC13jVFF4Phcrx
You can pre-fill your environment with some proposals and votes easily by simply
calling the ./tests/data.sh script.
./tests/data.sh
Once you are done with the nodeos development node, simply call stop.sh
to stop the running instance:
./stop.sh
To easily interact with the development node via eosc on your terminal, simply export
the following environment variables:
export EOSC_GLOBAL_INSECURE_VAULT_PASSPHRASE="secure"
export EOSC_GLOBAL_API_URL="http://localhost:9898"
export EOSC_GLOBAL_VAULT_FILE="`pwd`/tests/eosc-vault.json"
The direnv tool can be used to automatically import those variables
when you cd in the project's root directory.
Running the full automatic test suite is easy as doing:
./tests.sh
This launches nodeos development node (via ./run.sh) and then
executes all the integration tests found in tests folder (see all.sh
for exact files picked up).
To correctly run the tests, you will need to switch the freeze period of a proposal for 2 seconds (waiting 3 days could be a bit too long!)
In the file include/forum.hpp, change the line:
constexpr static uint32_t FREEZE_PERIOD_IN_SECONDS = 3 * 24 * 60 * 60;
So it looks like this instead:
constexpr static uint32_t FREEZE_PERIOD_IN_SECONDS = 2; // NEVER MERGE LIKE THIS
The test.sh script refuses to run if the string is not found in the file.
Important Be 100% sure to revert back the changes, you would not like to push a freeze period of 2 seconds in the repository!
The latest version of this code lives on the eosforumrcpp account on
the EOS Mainnet and on the cancancan345 account on the EOS Kylin network.
Tools that initially integrated support for this contract are:
- eosc has a command-line interface implementation to submit posts and votes.
- EOS ToolKit Forum Post allows you to post content through this contract.
- MyEOSKit already has special casing for the
postactions. See this transaction for example.
Here is the list of possible actions:
Propose a new proposal to the community.
proposer(typeaccount_name) - The actual proposer's accountproposal_name(typename) - The proposal's name, its ID among all proposalstitle(typestring) - The proposal's title (must be less than 1024 characters)proposal_json(typestring) - The proposal's JSON metadata, no specification yet, see JSON Structure Guidelinesexpires_at(typetime_point_sec) - The expiration date of the proposal, must be no later than 6 months in the future, ISO 8601 string format (in UTC) without a timezone modifier.
- When missing signature of
proposer - When
proposal_namealready exists - When
titleis longer than 1024 characters - When
proposal_jsonJSON is invalid or too large (must be a JSON object and be less than 32768 characters) - When
expires_atdate is earlier than now or later than 6 months in the future
eosc tx create eosforumrcpp propose '{"proposer": "proposer1", "proposal_name": "example", "title": "The title, for list views", "proposal_json": "", "expires_at": "2019-01-30T17:03:20"}' -p proposer1@active
OR
eosc forum propose proposer1 example "The title, for list views" 2019-01-30T17:03:20 --json "[JSON object]"
Vote for a given proposal using your account.
voter(typeaccount_name) - The actual voter's accountproposal_name(typename) - The proposal's name to vote onvote(typeuint8) - Your vote on the proposal,0means a negative vote,1means a positive votevote_json(typestring) - The vote's JSON metadata, no specification yet, see JSON Structure Guidelines
- When missing signature of
voter - When
proposal_namedoes not exist - When
proposal_nameis already expired - When the
vote_jsonJSON is invalid or too large (must be a JSON object and be less than 8192 characters)
eosc tx create eosforumrcpp vote '{"voter": "voter1", "proposal_name": "example", "vote": 0, "vote_json": ""}' -p voter1@active
OR
eosc forum vote voter1 example 0
Remove your current active vote, effectively reclaiming the stored RAM of the vote. Of course, your vote will not count anymore (neither positively or negatively) on the current proposal's voting statistics.
It's not possible to unvote on a proposal that is expired but within its freeze period of 3 days.
If the proposal is expired and the freeze period has elapsed, it's possible to unvote on the proposal.
To be nice to the community however, you should call clnproposal until the proposal
is fully cleaned up so that every vote will be removed and RAM will be freed for all voters.
voter(typeaccount_name) - The actual voter's accountproposal_name(typename) - The proposal's name to remove your vote from
- When missing signature of
voter - When
proposal_namedoes not exist - When
proposal_nameis expired but within its freeze period of 3 days
eosc tx create eosforumrcpp unvote '{"voter": "voter1", "proposal_name": "example"}' -p voter1@active
OR
eosc forum unvote voter1 example
Immediately expires a currently active proposal. The proposal can only be expired by the original proposer that created it. It's not valid to expire an already expired proposal.
proposal_name(typename) - The proposal's name to expire
- When missing signatures of proposal's
proposer - When
proposal_namedoes not exist - When
proposal_nameis already expired
eosc tx create eosforumrcpp expire '{"proposal_name": "example"}' -p proposer1@active
OR
eosc forum expire proposer1 example
Note proposer1 must be the same as the one that created initially the example proposal.
Clean a proposal from all its votes and the proposal itself once there are no more associated votes. The action
works iteratively, receiving a max_count value. It removes as many as max_count votes. When there
are no more votes, the proposal itself is deleted.
This effectively clears all the RAM consumed for a proposal and all its votes. Call the action multiple times until all votes are removed.
It's possible to clean a proposal only if it has expired and if its freeze period of 3 days has fully
elapsed. Within the freeze period, the proposal is locked and no actions can be performed on it.
Since only expired proposals can be cleaned, anybody can invoke this action, no authorization is required.
Voters, proposers, or any community member is invited to call clnproposal to clean the RAM related to
a proposal.
Note Since a proposal can expire only by a manual action issued by the proposal's author or if it
has passed its expires_at value, it's safe to be called by anybody since the proposal has effectively
terminated its lifecycle.
cleaner_account(typename) - The account that CPU/NET will be charged toproposal_name(typename) - The proposal's name to cleanmax_count(typeuint64) - The amount of votes to clean out in this batch
- When
proposal_nameis not expired yet - When
proposal_nameis expired but within its freeze period of 3 days
Note Giving a max_count that is too big increases the probability that the transaction
fails due to excessive CPU usage. Find the sweet spot to avoid that.
eosc tx create eosforumrcpp clnproposal '{"proposal_name": "example", "max_count": 100}' -p voter1@active
OR
eosc forum clean-proposal [cleaner_account_name] example 100
poster(typeaccount_name) - The poster's accountpost_uuid(typestring) - The postUUID(for reply purposes)content(typestring) - The actual content of the postreply_to_poster(typeaccount_name) - The initial post's poster your post replies toreply_to_post_uuid(typestring) - The initial post'sUUIDyour post replies tocertify(typebool) - Reserved for future usejson_metadata(typestring) - The post's JSON metadata, no specification yet, see JSON Structure Guidelines
- When missing signature of
poster - When
contentis an empty string - When
contentis bigger than 10240 characters - When
post_uuidis an empty string - When
post_uuidis bigger than 128 characters - When
reply_to_posteris not set butreply_to_post_uuidis - When
reply_to_posteris not an existing account - When
reply_to_posteris set andreply_to_post_uuidis an empty string - When
reply_to_posteris set andreply_to_post_uuidis bigger than 128 characters - When
json_metadataJSON is invalid or too large (must be a JSON object and be less than 8192 characters)
eosc tx create eosforumrcpp post '{"poster": "poster1", "post_uuid":"examplepost_id", "content": "hello world", "reply_to_poster": "", "reply_to_post_uuid": "", "certify": false, "json_metadata": "{\"type\": \"chat\"}"}' -p poster1@active
OR
eosc forum post poster1 "hello world"
poster(typeaccount_name) - Remove a previous post you didpost_uuid(typestring) - TheUUIDof the post to remove
- When missing signature of
poster - When
post_uuidis an empty string - When
post_uuidis bigger than 128 characters
eosc tx create eosforumrcpp unpost '{"poster": "poster1", "post_uuid":"examplepost_id"}' -p poster1@active
OR
eosc forum unpost poster1 [UUID_of_example]
Record a status for the associated account. If the content is empty, the action will remove a
previous status. Otherwise, it will add a status entry for the account using the content received.
account(typeaccount_name) - The account to add a status tocontent(typestring) - The content associated to the status
- When missing signature of
account - When
post_uuidis bigger than 256 characters - When
contentis the empty string and no previousstatusexisted foraccount
Example (add status):
eosc tx create eosforumrcpp status '{"account": "voter2", "content":"status of something"}' -p voter2@active
Example (remove previous status):
eosc tx create eosforumrcpp status '{"account": "voter2", "content":""}' -p voter2@active
OR
eosc forum status voter2 "status of something"
Example (remove previous status):
eosc forum status voter2 ""
proposal_name(typename) - The proposal's name, its ID among all proposalsproposer(typeaccount_name) - The actual proposer's accounttitle(typestring) - The proposal's title, a brief description of the proposalproposal_json(typestring) - The proposal's JSON metadata, no specification yet, see JSON Structure Guidelinescreated_at(typetime_point_sec) - The date at which the proposal's was created, ISO 8601 string format (in UTC) without a timezone modifier.expires_at(typetime_point_sec) - The date at which the proposal's will expire, ISO 8601 string format (in UTC) without a timezone modifier.
- First (
1typename) - Index byproposal_namefield - Second (
2typename) - Index byproposer
eosc get table eosforumrcpp eosforumrcpp proposal
OR
eosc forum list
Caveats Right now, eosc does not support searching giving only a direct key. Instead, it really requires
a lower and upper bound. The upper bound being exclusive, to correctly get the upper bound, take the account name
and change the last character to the next one in the EOS name alphabet (order is a-z1-5.).
So, looking for all proposals proposed by testusertest, the lower bound key would be testusertest and
the upper bound key would be testusertesu (last character t bumped to next one u).
eosc get table eosforumrcpp eosforumrcpp proposal --index 2 --key-type name --lower-bound testusertest --upper-bound testusertesu
OR
eosc forum list --from-proposer testusertest
account(typeaccount_name) - The status' postercontent(typestring) - The content of the statusupdated_at(typetime_point_sec) - The date at which the status was last updated, ISO 8601 string format (in UTC) without a timezone modifier.
eosc get table eosforumrcpp eosforumrcpp status
id(typeuint64) - The unique ID of thevoter/proposal_namepairproposal_name(typename) - Theproposal_nameon which the vote appliesvoter(typeaccount_name) - Thevoterthat votedvote(typeuint8) - The vote value of thevoter(0means negative vote,1means a positive vote)vote_json(typestring) - The vote's JSON metadata, no specification yet, see JSON Structure Guidelinesupdated_at(typetime_point_sec) - The date at which the vote was last updated, ISO 8601 string format (in UTC) without a timezone modifier.
- First (
1typei64) - Index byidfield - Second (
2typei128input in hexadecimal little-endian format) - Index by proposal name, the key is composed in the high bytes using theproposal_nameand the low bytes are thevoter. - Third (
3typei128input in hexadecimal little-endian format) - Index by voter, the key is composed in the high bytes using thevoterand the low bytes are theproposal_name.
eosc get table eosforumrcpp eosforumrcpp vote
The idea is to turn the proposal_name into an integer, convert it to hexadecimal, and compute the lowest
possible key for voter (lower bound) as well as the highest possible key for voter (upper bound).
Note The hexadecimal values below are all in little-endian format, so high bytes are on the right side and low bytes on the left side.
Here are the steps to compute the lower/upper bounds for the table query:
- Convert
ramusetestEOS name to hexadecimal usingeosc tools name.
eosc tools names ramusetest
from \ to hex hex_be name uint64
--------- --- ------ ---- ------
name 0040c62a2baca5b9 b9a5ac2b2ac64000 ramusetest 13377287569575133184
-
Create the
lower_boundkey by prepending0000000000000000to thehexvalue shown above:0x00000000000000000040c62a2baca5b9 -
Create the
upper_boundkey by prependingffffffffffffffffto thehexvalue shown above:0xffffffffffffffff0040c62a2baca5b9.
Now that we have the lower and upper bound keys, simply perform your query:
eosc get table eosforumrcpp eosforumrcpp vote --index 2 --key-type i128 --lower-bound 0x00000000000000000040c62a2baca5b9 --upper-bound 0xffffffffffffffff0040c62a2baca5b9
You will see only the votes against the proposal ramusetest.
The idea is to turn the voter into an integer, convert it to hexadecimal, and compute the lowest
possible key for proposal_name (lower bound) as well as the highest possible key for proposal_name (upper bound).
Note The hexadecimal values below are all in little-endian format, so high bytes are on the right side and low bytes on the left side.
Here the steps to compute the lower/upper bounds for the table query:
- Convert
testusertestEOS name to hexadecimal usingeosc tools name.
eosc tools names testusertest
from \ to hex hex_be name uint64
--------- --- ------ ---- ------
name 90b1ca57619db1ca cab19d6157cab190 testusertest 14605628107949519248
-
Create the
lower_boundkey by prepending0000000000000000to thehexvalue shown above:0x000000000000000090b1ca57619db1ca -
Create the
upper_boundkey by prependingffffffffffffffffto thehexvalue shown above:0xffffffffffffffff90b1ca57619db1ca.
Now that we have the lower and upper bound keys, simply perform your query:
eosc get table eosforumrcpp eosforumrcpp vote --index 3 --key-type i128 --lower-bound 0x000000000000000090b1ca57619db1ca --upper-bound 0xffffffffffffffff90b1ca57619db1ca
You will see only the proposals that voter testusertest voted for.
You can use any vocabulary you want when creating posts, proposals and votes, there is no specification yet for the JSON. However, by following some simple guidelines, you can simplify your life and the life of those building UIs around these messages.
For all json prefixed or suffixed fields in propose, vote and
post, the type field should determine a higher order protocol, and
determines what other sibling fields will be required.
-
typeis a required field to distinguish protocol. See types in the section below. -
questionmeans the reference language question of a proposition. -
contentis a Markdown document, detailing everything there is to know about the proposal (tally methods, time frame, references, required etc..)
typeis optional. Defaults tosimpleif not present.
simpleis the same as notypeat all. The value of the vote is the booleanvotefield of the action.
typeis a required field to distinguish protocol. See below for sample types
The following fields attempt to standardize the meaning of certain
keys. If you specify your own type, you can define whatever you
want.
-
titleis a title that will be shown above a message, often used in clickable headlines. Similar to a Reddit post's title. -
tagsis a list of strings, prefixed or not with a#.
-
chat, which is a simple chat, pushing a message out. -
eos-bps-roll-call, this is used within EOS Block Producers calls to indicate they are present. -
eos-bps-emergency, once 3 block producers send a message of this type within an hour, all block producers can trigger a wake-up alarm within 1h. Do not abuse this message to avoid alert fatigue. ##### Example serious vulnerability requires mitigation, serious network issues, immediate action required, etc.. -
eos-bps-notify, once 7 block producers send a message of this type within an hour, other block producers can trigger a notification to get their attention in the next 24h. ##### Example new ECAF order requires attention. -
eos-arbitration-order, BPs can watch for known Arbitration forums accounts, and alert themselves of required action. Further fields could be defined like a link to the PDF format order; a reference to a ready-madeeosio.msigtransaction proposition; etc..
MIT See license file
Original code and inspiration: Daniel Larimer