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

Implement PowerTable as ordered map #32

Merged
merged 9 commits into from
Jan 18, 2024

Conversation

ranchalp
Copy link
Contributor

@ranchalp ranchalp commented Jan 10, 2024

EDIT: builds upon #34 , so merge that one first.

Implements PowerTable as an ordered map. To be used by explicit justification to create/verify bitsets (see here) and, even if using implicit justification, by the creation/verification of PoFs as part of the catch-up mechanism.

Copy link
Member

@anorth anorth left a comment

Choose a reason for hiding this comment

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

We really need two things here:

  1. A data structure that can be serialized as the canonical power table, giving a CID
  2. A runtime structure for looking up power values and keys

This is good for (2), but we won't want to serialise the lookup table. For now lets ignore that problem. We can chose later to add a new type for the serializable form, or manually implement serialisation here, including only the wanted data.

f3/granite.go Outdated
@@ -44,6 +45,24 @@ type GMessage struct {
Ticket Ticket
// Signature by the sender's public key over Instance || Round || Step || Value.
Signature []byte

Evidence AggEvidence
Copy link
Member

Choose a reason for hiding this comment

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

This is unused in this PR too - can we keep it out until it will be used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done by rebasing from #34

f3/powertable.go Outdated
// It includes an ActorID and its Weight
type PowerEntry struct {
ID ActorID
Power uint
Copy link
Member

Choose a reason for hiding this comment

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

In the main network, power is always represented as a big integer. It's tempting to argue that an int64 should be enough for one miner, but that only(!) 16 EiB or 170x the current largest miner. 170x can disappear pretty quickly given an expected annual doubling. So I think we need a bigint here.

In types.go add type StoragePower big.Int and use that (this mimics Lotus)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

f3/powertable.go Show resolved Hide resolved
f3/powertable.go Outdated
type PowerTable struct {
Entries map[ActorID]uint
Entries []PowerEntry // Slice to maintain the order
Copy link
Member

Choose a reason for hiding this comment

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

Please document that this is to be maintained in order by (ID descending, Power ascending).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's actually in order by (Power descending, ID ascending), but done otherwise.

f3/powertable.go Outdated
}
}

// NewPowerTableFromMap creates a new PowerTable from a map of ActorID to weight.
// It is more efficient than Add, as it only needs to sort the entries once.
func NewPowerTableFromMap(entriesMap map[ActorID]uint) PowerTable {
Copy link
Member

Choose a reason for hiding this comment

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

Are there any callers of this? I don't think this is how Lotus will call it to construct the table - it doesn't have a map like this.

I think the canonical constructor will be NewPowerTable(entries []Entry) *PowerTable, which should sort the entries and construct the lookup table and total.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed it to []PowerEntry.

f3/powertable.go Outdated
Comment on lines 57 to 59
if _, ok := p.Lookup[id]; ok {
panic("duplicate power entry")
}
Copy link
Member

Choose a reason for hiding this comment

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

You can remove this, because you can check for duplication after doing the sort.Search.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

f3/powertable.go Outdated
type PowerTable struct {
Entries map[ActorID]uint
Entries []PowerEntry // Slice to maintain the order
Lookup map[ActorID]int // Map for quick index lookups
Copy link
Member

Choose a reason for hiding this comment

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

Please be more clear about what this is indexing. "Maps actor ID to the index of the associated entry in Entries"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

f3/powertable.go Outdated Show resolved Hide resolved
f3/powertable.go Outdated
p.Total += power
}

func (p *PowerTable) GetPower(id ActorID) uint {
Copy link
Member

Choose a reason for hiding this comment

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

Suggest making this Get(id ActorID) (Power, []byte) and return both the power and private key, when you add that. And maybe even add the ok bool return here too (otherwise add a Has() method and panic here if !ok)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@ranchalp
Copy link
Contributor Author

Thanks! addressed the comments :)

f3/granite.go Outdated
weakThreshold := q.powerTable.Total * 1 / 3
if candidate.power > weakThreshold {
one := NewStoragePower(1)
weakThreshold := new(StoragePower).Mul(q.powerTable.Total, one)
Copy link
Member

Choose a reason for hiding this comment

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

No need to multiply by one

Copy link
Contributor Author

@ranchalp ranchalp Jan 17, 2024

Choose a reason for hiding this comment

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

Oops, AI help was too obvious here 😅 . Done.

f3/granite.go Outdated
Comment on lines 625 to 631
two := NewStoragePower(2)
three := NewStoragePower(3)

threshold := new(StoragePower).Mul(q.powerTable.Total, two)
threshold.Div(threshold, three)

return q.sendersTotalPower.Cmp(threshold) > 0
Copy link
Member

Choose a reason for hiding this comment

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

Can you factor out a hasStrongQuorum(part, total *StoragePower) bool pure function to use here and above? And maybe do a hasWeakQuorum too for symmetry.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

Copy link
Member

Choose a reason for hiding this comment

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

Remember to replace the code here with a call to your new method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Done (upcoming commit)

f3/granite.go Outdated
@@ -608,21 +520,21 @@ type quorumState struct {
// The power supporting each chain so far.
chainPower map[TipSetID]chainPower
// Total power of all distinct senders from which some chain has been received so far.
sendersTotalPower uint
sendersTotalPower StoragePower
Copy link
Member

Choose a reason for hiding this comment

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

It's tricky to know how to reference Go big.Ints properly. Is there a good reason this one is not a pointer but the others are? Using a pointer seems to be canonical so I'd suggest doing that here too, unless there's a good reason not to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are absolutely right. I started with no pointers but missed this one when switching to pointers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Total: 0,
// NewPowerTable creates a new PowerTable from a slice of PowerEntry .
// It is more efficient than Add, as it only needs to sort the entries once.
func NewPowerTable(entries []PowerEntry) *PowerTable {
Copy link
Member

Choose a reason for hiding this comment

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

Please note the function takes ownership of the slice - it must not be modified afterwards.

Copy link
Contributor Author

@ranchalp ranchalp Jan 17, 2024

Choose a reason for hiding this comment

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

Correct, but not yet an issue. I reckon you are noticing this but suggesting we leave it as is?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, by note I meant "write a comment"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

f3/powertable.go Outdated Show resolved Hide resolved
f3/powertable.go Outdated
})

// Check for duplication at the found index or adjacent entries
if index < len(p.Entries) && (p.Entries[index].ID == id || (index > 0 && p.Entries[index-1].ID == id)) {
Copy link
Member

Choose a reason for hiding this comment

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

You only need to check the exact entry pointed to.

Search uses binary search to find and return the smallest index i in [0, n) at which f(i) is true

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

f3/powertable.go Show resolved Hide resolved
f3/powertable.go Outdated
p.Total.Add(p.Total, power)
}

func (p *PowerTable) Get(id ActorID) (*StoragePower, []byte, bool) {
Copy link
Member

Choose a reason for hiding this comment

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

Having seen this in use (especially ignoring the bool return), I have a further suggestion.

The existing code used the zero value map lookup to silently accept messages from participants with no power. Now is the time to change that. I recommend:

  • Add a Has(id) method here, and call it in isValid to reject messages from participants absent or with zero power
  • Remove the bool return here and just panic if not found. All the call sites should be able to rely on the prior filtering of valid messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

f3/types.go Outdated
"strings"
)

type ActorID uint64

type StoragePower big.Int
Copy link
Member

Choose a reason for hiding this comment

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

Make this a type alias and then you can remove all the wrapper methods.

type StoragePower = big.Int

This is slightly less "safe" in that it doesn't distinguish between different things a big.Int might be representing, but I think it's fine here (and Lotus does too).

Copy link
Contributor Author

@ranchalp ranchalp Jan 17, 2024

Choose a reason for hiding this comment

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

Ah ok, I thought about this too but I did not do it precisely to distinguish between different things. But agreed that helps remove boilerplate. And specially for consistency with lotus it makes sense. Done.

sim/sim.go Outdated
participants := make([]*f3.Participant, simConfig.HonestCount)
for i := 0; i < len(participants); i++ {
participants[i] = f3.NewParticipant(f3.ActorID(i), graniteConfig, ntwk, vrf)
ntwk.AddParticipant(participants[i])
genesisPower.Add(participants[i].ID(), 1)
genesisPower.Add(participants[i].ID(), f3.NewStoragePower(1), make([]byte, 0))
Copy link
Member

Choose a reason for hiding this comment

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

Let's put a non-empty key in here so that the fake signature verification at least checks it's the right signature. Serialize the participant ID into a byte array, and maybe add some magic bytes to distinguish it from other serialized integers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@ranchalp
Copy link
Contributor Author

ranchalp commented Jan 17, 2024

Just addressed the latest comments! Thanks!!

type PowerTable struct {
Entries map[ActorID]uint
Total uint
Entries []PowerEntry // Slice to maintain the order. Meant to be maintained in order in order by (Power descending, ID ascending)
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: Why do we build the power table sorted power?
Usage-wise, we will almost always query it by ActorID; events SPs appearing and disappearing are also rarer than an order of the power changing (especially in the long tail).

In general, it seems to me that having the power table have a different representation for granite format and different in the wire format (finality certificates) makes sense. Especially since we will need to optimize the wire format for usage in smart contracts.

Copy link
Contributor

@Kubuxu Kubuxu Jan 17, 2024

Choose a reason for hiding this comment

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

I saw it was also part of granite as well. It needs to communicate which participants signed each step.
One option could be communicating that in the form of RLE+ bitfield of ActorIDs, keeping the power table format specifics out of the granite itself.
https://github.com/filecoin-project/go-bitfield/

Copy link
Member

Choose a reason for hiding this comment

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

I see what you're saying. Note that the TipSet structure here will have a PowerTable CID added to it, which must match the serialized format used in certificates. However that CID will come from outside, attached to the tipsets pushed in.

The signers will certainly be an RLE+ bitfield (it's present in subsequent PRs). A bitfield over powertable indices will be more dense than over ActorIDs, and I think the density does matter here, as bytes in the messages (of which there are quadratic in the participant count). The sorting by power is also a heuristic to increase density.

These are good questions, lets defer to an issue if you still think it's worth consideration. We can move on here with this, and come back to revise this structure if warranted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have not gotten there yet (See TODOs in #37's description), but having them sorted by power makes it easy for the creator of a message to optimize message size when creating a justification (i.e. the creator needs to just iterate until the strong quorum is reached when creating the justification). Not visible at the moment even in #37 because all received values that justify the same message are added to the justification (so in many cases this will be more added signatures than required for a strong quorum).

Also the cost of having a sorted power table is negligible compared to running Granite.

Copy link
Contributor

Choose a reason for hiding this comment

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

but having them sorted by power makes it easy for the creator of a message to optimize message size when creating a justification

sure, but this happens on the granite side, which can easily just form a temporary datastructure for the sort, I'm thinking about the 3rd party users who will have to replicate the semantics of whatever power table format we decide on using

Copy link
Contributor

Choose a reason for hiding this comment

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

Not delaying this PR due to this, as @anorth let's defer this discussion to when we get to finality certificate

Copy link
Member

@anorth anorth left a comment

Choose a reason for hiding this comment

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

LGTM after nits

f3/granite.go Outdated
Comment on lines 625 to 631
two := NewStoragePower(2)
three := NewStoragePower(3)

threshold := new(StoragePower).Mul(q.powerTable.Total, two)
threshold.Div(threshold, three)

return q.sendersTotalPower.Cmp(threshold) > 0
Copy link
Member

Choose a reason for hiding this comment

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

Remember to replace the code here with a call to your new method

type PowerTable struct {
Entries map[ActorID]uint
Total uint
Entries []PowerEntry // Slice to maintain the order. Meant to be maintained in order in order by (Power descending, ID ascending)
Copy link
Member

Choose a reason for hiding this comment

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

I see what you're saying. Note that the TipSet structure here will have a PowerTable CID added to it, which must match the serialized format used in certificates. However that CID will come from outside, attached to the tipsets pushed in.

The signers will certainly be an RLE+ bitfield (it's present in subsequent PRs). A bitfield over powertable indices will be more dense than over ActorIDs, and I think the density does matter here, as bytes in the messages (of which there are quadratic in the participant count). The sorting by power is also a heuristic to increase density.

These are good questions, lets defer to an issue if you still think it's worth consideration. We can move on here with this, and come back to revise this structure if warranted.

Total: 0,
// NewPowerTable creates a new PowerTable from a slice of PowerEntry .
// It is more efficient than Add, as it only needs to sort the entries once.
func NewPowerTable(entries []PowerEntry) *PowerTable {
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, by note I meant "write a comment"

go.mod Outdated
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-multibase v0.0.1 // indirect
github.com/multiformats/go-multihash v0.0.13 // indirect
github.com/multiformats/go-varint v0.0.5 // indirect
Copy link
Member

Choose a reason for hiding this comment

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

It looks like there's some noise crept in here, please normalise before merging.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@ranchalp
Copy link
Contributor Author

Addressed comments from today :)

@ranchalp ranchalp merged commit 2efdddf into filecoin-project:main Jan 18, 2024
2 checks passed
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.

3 participants