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

Delegcns #2

Closed
wants to merge 55 commits into from
Closed

Delegcns #2

wants to merge 55 commits into from

Conversation

aakoshh
Copy link
Owner

@aakoshh aakoshh commented Jul 15, 2022

Summary of changes
Changes introduced in this pull request:

  • Adds deleg_cns for Delegated Consensus which is a simplistic demo consensus where only a single miner can ever propose blocks.
  • I moved some parts of state_api::miner_create_block to chain::persist_block_messages, namely the storing of messages as CID and the aggregation of BLS signatures. This looks like a common thing that will be required by everything that wants to produce a block, and the chain crate was already full of helper method to read things that were persisted this way. Only the BLS may be questionable there, but it seemed like a natural place for it to be in, and the crate already references the forest_crypto stuff.
  • Added a select_messages_for_block method to the MessagePool to pick the messages which should go in the next block.

TODO:

  • Generate a BLS key pair and set it as the worker and owner address of the nominated miner in a preseal.json file
  • Create a template genesis.json file using forest genesis new-template CLI command and add the keys to it with forest genesis add-miner
  • Find a way to turn the genesis.json template file into a genesis.car that daemon.rs expects to load
  • Start a mining loop that produces a block at regular intervals with the right timestamps
  • Select transactions from the mempool that can go into the block, observing the rules about fees and nonces
  • Figure out how to add the miner private key to the wallet

Genesis file generation
It looks like I will have to rely on Lotus to generate the genesis file because:

The rabbit hole appears too deep for me to start implementing it, and I expect this might be on the roadmap of the Forest core team. Since all we need is a CAR file with the delegated miner address in it, using Lotus seems like an acceptable compromise until Forest has the capability to do this on its own.

Added a README which documents the process of producing a working genesis.car file.

The idea for starting nodes is that one of them will have a wallet that actually contains the private key of the miner, while the others don't. If the key of the chosen miner can be found in the wallet, then mining will commence, otherwise the node is treated as a delegator.

Other information and links

I'm not sure that the select_messages_for_block mechanism will work in the future as it only selects messages to be included in the next immediate block. It would not be suitable for building up a whole pipeline of unconfirmed messages, to be included in the next several blocks.

For example in the Tendermint example of a subnet consensus we can send transactions to Tendermint which comes up with a total ordering of them that is final, so a pre-requisite to work with Filecoin execution rules is for these to be ordered by the account sequence numbers, which the feeder is trying to do by inspecting the messages first. In theory we can have dozens of Tendermint blocks without creating Filecoin blocks from them immediately, as long as when we do, we put the messages in the same order. This would require a different method being available in the MessagePoolApi that can project the next messages indefinitely, maybe by getting a Map<Address, Sequence> to resume from, to which it can add anything new that would be suitable in a new block. It can still do the ordering, just chop off the prefix of certain Chains.

@aakoshh aakoshh changed the base branch from main to consensus-weight July 15, 2022 19:14
@aakoshh aakoshh changed the base branch from consensus-weight to main July 15, 2022 19:14

### Generate signing keys

According to the [spec](https://spec.filecoin.io/#section-systems.filecoin_nodes.repository.key_store) we need to generate a BLS key for signing blocks, a.k.a. the "worker" key.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The more I think about it, the more I feel we'll need to decouple the use of miner actors for HC validators in non Filecoin EC consensus. Even if we want to still use the concepts of worker and owner addresses, it'd be good to decouple into a new actor without all the storage-specific functions.

For future iteration, it may be a good idea to just use account actors for validators in non-Filecoin EC consensus (I am aware this may require some changes to the validation logic, unfortunately).

Copy link
Owner Author

Choose a reason for hiding this comment

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

You are right, the Genesis block will have lots of actors that have nothing to do with the consensus we run on a subnet. You did well in Eudico to take control there. Maybe when can talk to the Forest team before they replicate such functionality to do it in a more modular way. Tendermint and Cosmos provide a nice model here, but it was made to be that way. We can't rock the boat too much if we want to keep in sync with the core Forest changes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can't rock the boat too much if we want to keep in sync with the core Forest changes.

Definitely. I don't want to suffer again what we will potentially suffer when we push Eudico's changes to Lotus :) I feel that as the Forest team is smaller they will be happy with us contributing as long as we respect the compatibility with mainnet. I would definitely propose all the changes that we need (as long as they're non-breaking) and make Forest also "our thing". (Honestly, right now I think you are one of the top people with deep knowledge of the base code --which are few--)

@@ -0,0 +1,225 @@
# Delegated Consensus
Copy link
Collaborator

Choose a reason for hiding this comment

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

I love this README, amazing job!! 🙏

Copy link
Owner Author

Choose a reason for hiding this comment

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

Great! I added a version of the script that you can use as well, without Docker.

///
/// Historically this has been hardcoded to `t0100`,
/// which is the ID of the first actor created by the system.
chosen_one: Address,
Copy link
Collaborator

Choose a reason for hiding this comment

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

the variable name is very appropriate :)

Copy link
Collaborator

@adlrocha adlrocha left a comment

Choose a reason for hiding this comment

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

LGTM. I love the Proposer approach for validators. I think it'll give us a lot of flexibility in the future. As you are using cfg to determine the type of consensus to spawn, we could maybe try to upstream this change also to Forest. WDYT?

/// consensus variants.
async fn run<DB, MP>(
self,
// NOTE: We will need access to the `ChainStore` as well, or, ideally
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, this is the approach we follow in eudico. We expose the state manager to the mining process to access the required state.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Right, yes, I figured that Forest mimics Lotus in this regard.

It's just sort of an anti-pattern in Object Oriented programming to expose dependencies and chain together lots of accessors, like it.a().b().c().do_something(x) instead of it.do_abc(x), and manager.chain().database().put(...) completely obfuscates any ideas about what someone might want to do with manager, it's just a hold-all to allow you to do almost anything, because the ChainStore is super permissive.

Ideally we could give something like the MessagePool gets via the Provider interface, or limited views over individual stuff like ChainStoreReader. I left it like this because we're already passing the StateManager to the validate_block function, but it's worth a rethink at some point.


We could use the [lotus-keygen](https://github.com/filecoin-project/lotus/tree/v1.17.0-rc3/cmd/lotus-keygen) command to generate Secp256k1 or BLS keys, but unfortunately this command is not copied into the Docker image we built. However, that's not a problem in this case becase the `lotus-seed` command will generate a key for us, if it's not given an existing one.

### Generate the pre-seal file
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just a quick note: there's no need for pre-sealed data to start delegated consensus (we don't need to on-board power to mine the chain).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see, you need to have some pre-sealed data in advance because there's no command in lotus to add a miner in genesis without pre-sealed data (just realized while reading the script).


### All together now

The [scripts/generate-genesis-files.sh](scripts/generate-genesis-files.sh) file is a convenience script that runs all the above commands assuming that `lotus-seed` is on the `PATH`, which may be the case if we already have Lotus or Eudico installed. If so, we can run the script directly, giving it the output directory (which must exist) to write the genesis files to.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is really helpful. We may find a slight blocker here, though, once we start spawning new subnets, as we won't be able to generate genesis files "on-the-fly" as we do in Eudico. We may need to make a slight de-tour to introduce the ability to write CAR files natively in Rust.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes, spinning up a subnet will be more of a manual process of aligning all the ducks than what it sounds like to be in Eudico. OTOH if we took a page out of the Cosmos handbook, it's only the subnet consensus code that really knows what it wants to put into its own Genesis parameters, so if we want to keep it flexible, it has to be like that to some extent.

That said I'd be happy to look into CAR generation to understand the formats better.

cargo build --release --features deleg_cns
```

## Add the private key to a wallet
Copy link
Collaborator

@adlrocha adlrocha Jul 22, 2022

Choose a reason for hiding this comment

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

You may have come across this method already and it may not work as expected, but the analogous to this function is what we use in Eudico to import keys into the node's keystore/wallet: https://github.com/ChainSafe/forest/blob/5fc22340af3164d36bef40ecd71864c8c0b7bd10/forest/src/cli/wallet_cmd.rs#L126-L138

From reading the implementation it looks like it imports a single key in json format, not the whole keystore, but I may be wrong.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thanks! I didn't look close enough and thought it might do the whole thing; hopefully this is it!

// These will be two different `Actor` instances created for the Miner.
//
// In Eudico they use the _Account ID_ directly and not create a _Miner Actor_, but in
// Forest we go through the common machinery, and validation will call [get_miner_work_addr],
Copy link
Collaborator

Choose a reason for hiding this comment

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

As discussed, we may need to decouple Filecoin EC block validation from the rest of the consensus protocols so we can use account actors directly instead of having to go through all of the identity dance of the miner actor.

DB: BlockStore,
{
let header = ts.blocks().first().expect("Tipset is never empty.");
// We don't have a height, only epoch, which is not exactly the same as there can be "null" epochs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this true? At least in the delegated consensus implementation of Eudico we propose a new block every epoch. There can't be "null" epochs.

Copy link
Owner Author

Choose a reason for hiding this comment

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

It is true that the proposer doesn't leave empty epochs, however, the validation allows nulls, so it would not be against the rules if it did.

In any case it didn't seem like there was a distinction between epoch and height in Lotus. I'm more biased here by my notion of height in all other blockchains I have come across.

DB: BlockStore + Sync + Send + 'static,
MP: MessagePoolApi + Send + Sync + 'static,
{
// TODO: Ideally these should not be coming through the `StateManager`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not? Do you think we should have a more restrictive API from the proposer?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes, I think we should only give it limited capabilities. The MessagePool has too many public mutable fields, so that was a non-starter to share (anyone with a direct reference to the message pool could insert/remove messages from its caches at will). The StateManager itself is not too bad, but the ChainStore itself has public methods like set_genesis and set_heaviest_tipset that we should not be able to call without going through the proper channels.

The blockstore is another one that would be best if it was passed directly, if access is needed. Currently you can reach it via ChainStore.db, ChainStore.blockstore(), ChainStore.blockstore_cloned(), StateManager.blockstore(), StateManger.blockstore_cloned(), StateManager.chain_store().blockstore(), etc.

The Provider that the MessagePool gets is better at limiting access to what the mempool can use, but in return exposed via the public api field, rather than kept for itself, so again it's not perfect. Also, at some point interfaces like this tend to become annoying as well: they collect all dependencies a component needs into one mockable package, but they also make a mess of individual concepts, like having a state manager, a chain store and a block store as things. It's tempting to put too much logic into the methods of the provider, rather than what it provides to.

It is a balancing act. Perhaps separating interfaces to a reader/writer part is a good start.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's a great point. I think is definitely worth a refactor as we keep building the protocol.

@adlrocha
Copy link
Collaborator

The latest version of of the fvm_ipld_car library can only read CAR files, not write them.

Regarding this, we should maybe check with the FVM team if they are planning to implement CAR writing (it would make our lives significantly easier).

Something we'll also need to figure out is how to perform integration tests for HC and new consensus implementations, in Eudico we have itests:

@aakoshh
Copy link
Owner Author

aakoshh commented Jul 22, 2022

in Eudico we have itests:

Thanks for the links! Definitely these sort of tests that set up multiple nodes are where we want to be. I think the primary way of integration testing Forest at the moment is to try syncing with calibnet. For delegcns I didn't see that we'd gain huge benefits from automated tests in this PR, but it might be a good idea to follow up with a PR that where we set up some nodes in memory with real or mocked networking and let it run for a while. It may require some changes to the daemon.

Definitely more tests are needed when actual hierarchical consensus machinery is added to the mix, and when we implement new consensus protocols like Narwhal, since they won't have any reference like Lotus to be tested against.

@aakoshh
Copy link
Owner Author

aakoshh commented Jul 22, 2022

As you are using cfg to determine the type of consensus to spawn, we could maybe try to upstream this change also to Forest. WDYT?

Agreed, it would be good to get their feedback on this approach! I wasn't too happy about the conditional setup be in the middle of the daemon code, but unfortunately I couldn't find a better tradeoff in the amount of boilerplate to look at when I tried to move those parts to be outside the main flow. At least it would not burden their baseline usecase with dynamic dispatch.

I will figure out the wallet and see if the thing starts up at all, then close this PR and open another one against the upstream branch (I can't find a way to switch the base of the PR back to the other repo, only the branch can be changed 😕 )

@aakoshh
Copy link
Owner Author

aakoshh commented Jul 26, 2022

Closing as ChainSafe#1714 got merged.

@aakoshh aakoshh closed this Jul 26, 2022
@aakoshh aakoshh deleted the delegcns branch July 26, 2022 08:31
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.

6 participants