Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace ballots with indexed map #114

Merged
merged 11 commits into from
Feb 23, 2022
Merged

Conversation

ueco-jb
Copy link

@ueco-jb ueco-jb commented Feb 22, 2022

closes #83

I expanded Ballot structure, so now it contains also address of voter and proposal id.
It got me thinking - now this is basically the same structure as VoteInfo link, BUT... it is using String instead of Addr type as in Ballot now.
Options:

  1. Leave it as it is
  2. Get rid of separate Ballot structure and use VoteInfo in voting-contract everywhere instead
    2a) but check for Addr correctness before each query.
    2b) replace String with Addr in VoteInfo structure

@ueco-jb ueco-jb self-assigned this Feb 22, 2022
@ueco-jb ueco-jb force-pushed the 83-replace-ballots-with-indexed-map branch from 330fc50 to e964d5c Compare February 22, 2022 13:20
@ueco-jb ueco-jb force-pushed the 83-replace-ballots-with-indexed-map branch from e964d5c to af211cd Compare February 22, 2022 13:21
@ueco-jb ueco-jb marked this pull request as ready for review February 22, 2022 13:29
@maurolacy
Copy link
Contributor

closes #83

I expanded Ballot structure, so now it contains also address of voter and proposal id.

These are in the primary key. I understand you need voter for indexing. proposal_id wouldn't be needed, as it can be extracted from the (deserialized) pk.

It got me thinking - now this is basically the same structure as VoteInfo link, BUT... it is using String instead of Addr type as in Ballot now. Options:

  1. Leave it as it is
  2. Get rid of separate Ballot structure and use VoteInfo in voting-contract everywhere instead
    2a) but check for Addr correctness before each query.
    2b) replace String with Addr in VoteInfo structure

VoteInfo is perhaps used for responses? I think it makes sense for voter to be String there.

@ueco-jb
Copy link
Author

ueco-jb commented Feb 22, 2022

proposal_id wouldn't be needed

That's right, I kept it mostly for consistency. Good point, removed.

I think it makes sense for voter to be String there.

Cool, that's why I asked. Anyway, I removed proposal_id so question is irrelevant now.

Copy link
Contributor

@maurolacy maurolacy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

This flagged a number of limitations with the current storage-plus muti-index impl, that we can address in follow-up issues.

.ballots
.idx
.voter
.prefix(voter_addr)
.range(deps.storage, start, None, Order::Ascending)
Copy link
Contributor

@maurolacy maurolacy Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use range_raw here, as you don't use / need the pk. That, or stick with range, and get proposal_id from the (deserialised) pk, below.

That way you can avoid the need for storing the proposal_id as part the value.

I'm now thinking that it would be good that the MultiIndex index function signature, instead of being fn(&T) -> IK, were fn(&PK, &T) -> IK instead. That way, the pk or parts of it can be used for indexing.

That will avoid having to store the voter address for the value as well. Will create an issue for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That way you can avoid the need for storing the proposal_id as part the value.

So this is already done.

Copy link
Contributor

@maurolacy maurolacy Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. After CosmWasm/cw-plus#670 (if we ever decide to do it), you can avoid the need for storing owner in the value also. As the index function will have that from the pk passed to it, and can take it from there.

This is just an idea at the moment. It will be a breaking change, but I think it may be worth it.


pub struct BallotIndexes<'a> {
// This PrimaryKey allows quering over all proposal ids for given voter address
pub voter: MultiIndex<'a, Addr, Ballot, (u64, Addr)>,
Copy link
Contributor

@maurolacy maurolacy Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That last type parameter is crucial, as it now serves not only for signalling the pk deserialization type, but also and more important, to define the type for typed bounds over the multi-index.

Let's create an issue for updating documentation about it. And, making that parameter mandatory, instead of optional.

Also, if we finish CosmWasm/cw-plus#531, that parameter could be tied to the IndexedMap pk definition directly, or automatically.

Which would be great, and will solve the issues with type-less bounds / ranges once and for all.

Copy link
Contributor

@maurolacy maurolacy Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impl<'a> Ballots<'a> {
pub fn new(storage_key: &'a str, release_subkey: &'a str) -> Self {
let indexes = BallotIndexes {
voter: MultiIndex::new(|ballot| ballot.voter.clone(), storage_key, release_subkey),
Copy link
Contributor

@maurolacy maurolacy Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, if we go ahead and change the index function signature for MultiIndex, we could instead have

Suggested change
voter: MultiIndex::new(|ballot| ballot.voter.clone(), storage_key, release_subkey),
voter: MultiIndex::new(|(pk, _ballot)| pk.1, storage_key, release_subkey),

and get rid of the need for adding extra fields to the values just for indexing.

Update: See CosmWasm/cw-plus#670.

Copy link
Contributor

@uint uint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for pulling that code out into the ballots.rs file. Awesome.

Now that I think about it, shouldn't we mark this as breaking-state?

.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|item| {
let (proposal_id, ballot) = item?;
let ((proposal_id, _), ballot) = item?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼


let votes: StdResult<Vec<_>> = BALLOTS_BY_VOTER
.prefix(&voter_addr)
// PrimaryKey of that IndexMap is (proposal_id, voter_address) -> (u64, Addr)
Copy link
Contributor

@maurolacy maurolacy Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, pk is (the last) part of a (multi-)index key. That's why, after fixing the voter addr with prefix(), you still need to range over the rest of the index key (which matches / corresponds to the pk, in this case).

let votes: StdResult<Vec<_>> = BALLOTS_BY_VOTER
.prefix(&voter_addr)
// PrimaryKey of that IndexMap is (proposal_id, voter_address) -> (u64, Addr)
let start = start_after.map(|m| Bound::exclusive((m, voter_addr.clone())));
Copy link
Contributor

@maurolacy maurolacy Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way to do this (just for fun. The current impl is better / clearer):

Suggested change
let start = start_after.map(|m| Bound::exclusive((m, voter_addr.clone())));
let start = start_after.map(|proposal_id| Bound::inclusive((proposal_id+1, Addr::unchecked("")));

An inclusive bound over proposal_id+1 is needed, because we currently don't have something like Addr::MAX (but we could).

This works basically because the (proposal_id, voter) tuple is unique (guaranteed, because it's the pk). So, this will not miss any keys (for any given voter, he could have voted (at most) only once on any given proposal).

@ueco-jb
Copy link
Author

ueco-jb commented Feb 22, 2022

@uint

Now that I think about it, shouldn't we mark this as breaking-state?

I actually removed "breaking" label, since all changes are done internally. And although technically state.rs file is changed, it doesn't even need any adaptation in contracts that already uses this new functionality (so tgrade-validator-voting and tgrade-community-pool).

@uint
Copy link
Contributor

uint commented Feb 22, 2022

@uint

Now that I think about it, shouldn't we mark this as breaking-state?

I actually removed "breaking" label, since all changes are done internally. And although technically state.rs file is changed, it doesn't even need any adaptation in contracts that already uses this new functionality (so tgrade-validator-voting and tgrade-community-pool).

Yeah, but if there are live voting contracts out there that have already stored some ballots on the blockchain, they will probably break when migrating since their stored data is structured differently now (Map -> IndexedMap).

@ueco-jb ueco-jb mentioned this pull request Feb 23, 2022
@ueco-jb ueco-jb merged commit 4784f46 into main Feb 23, 2022
@ueco-jb ueco-jb deleted the 83-replace-ballots-with-indexed-map branch February 23, 2022 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Refactor BALLOTS and BALLOTS_BY_VOTER in voting-contract
3 participants