Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Implement bitswap in the network behaviour using libp2p_bitswap. #6795

Closed
wants to merge 34 commits into from

Conversation

expenses
Copy link
Contributor

@expenses expenses commented Aug 3, 2020

I believe this would be a good start to implementing #6075. As libp2p-bitswap still needs some work (see #6075 (comment)), this PR probably needs a lot of work in order to be merged. A few points:

* Ideally the Bitswap behaviour would be wrapped in a Toggle so that it's only activated when a chain requests it, but we can't do that until libp2p/rust-libp2p#1684 gets merged.
* I think we should be inserting the bitswap blocks into the DHT as well, so that nodes we're not directly connected to will be able to access them.
* As the idea is to allow the fetching of preimages by an IPFS client, I'm not sure if we should be messing about with IPLD as well, or if this is too low level for that.

polkadot companion: paritytech/polkadot#1550

@expenses expenses added A0-please_review Pull request needs code review. B0-silent Changes should not be mentioned in any release notes C1-low PR touches the given topic and has a low impact on builders. labels Aug 3, 2020
@expenses expenses requested a review from tomaka August 3, 2020 08:38
tomaka
tomaka previously requested changes Aug 3, 2020
client/network/src/service.rs Outdated Show resolved Hide resolved
client/network/src/service.rs Outdated Show resolved Hide resolved
client/network/src/behaviour.rs Outdated Show resolved Hide resolved
client/network/src/behaviour.rs Outdated Show resolved Hide resolved
client/network/src/behaviour.rs Outdated Show resolved Hide resolved
client/network/src/behaviour.rs Outdated Show resolved Hide resolved
client/network/src/protocol/event.rs Outdated Show resolved Hide resolved
@expenses expenses requested a review from tomaka August 3, 2020 11:58
@dvc94ch
Copy link
Contributor

dvc94ch commented Aug 3, 2020

One problem is that ipfs data will leak, as there isn't a way of disposing of it. And since it's content addressed you'll need to ref count the blocks as the data is deduplicated.

To get "ipld" support you just need to implement the ipld store traits, you can easily do that, and then you get all the ipld codecs and ipld collections for free.

@dvc94ch
Copy link
Contributor

dvc94ch commented Aug 4, 2020

I was planning on reimplementing bitswap as a light client notification protocol, but it doesn't seem there is a way to perform the get-providers dht query from the NetworkService. This would be an alternative that would also work for us. I'm going to make the entire network implementation of ipfs-embed replaceable so that I can drop in the light client. The db is already shared between ipfs-embed and the light-client.

@tomaka do you think exposing get-provider and start-providing in NetworkService could be a faster way forward?

@expenses expenses marked this pull request as ready for review August 6, 2020 09:12
@tomaka
Copy link
Contributor

tomaka commented Aug 6, 2020

This PR doesn't do anything related to the DHT yet as far as I can tell, but one thing to be aware of is that we're customizing the name of the Kademlia protocol to include the id of the chain, as a way to prevent nodes from different chains from connecting to each other.

I suppose that it would fundamentally be better to also connected to a shared DHT, or to IPFS itself, rather than to only the DHT of a specific chain. However, this would be problematic as we are already now suffering from problems caused by the high number of TCP sockets.

a light client notification protocol

Request-response protocols, added in #6634, would be more appropriate I think. They are similar to RPC queries, whereas if you try to emulate request-reponses on top of notifications you will suffer from all sorts of issues.

@tomaka do you think exposing get-provider and start-providing in NetworkService could be a faster way forward?

That'd be totally reasonable.

Copy link
Contributor

@dvc94ch dvc94ch left a comment

Choose a reason for hiding this comment

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

So I reviewed the code again. There are a couple of issues with the current implementation. The changes aren't atomic so if there is a crash half way through inserting a batch for example, the store will be in an inconsistent state. Also I noticed that the light client keeps the blocks in memory and they aren't persisted to disk.

Maybe substrate shouldn't implement the store, and instead just expose dht and bitswap operations via the Network thing.

@expenses
Copy link
Contributor Author

@dvc94ch This is ready for a re-review (the button has stopped working for me 🤷‍♀️ )

@@ -768,11 +768,40 @@ pub struct BuildNetworkParams<'a, TBl: BlockT, TExPool, TImpQu, TCl> {
pub finality_proof_request_builder: Option<BoxFinalityProofRequestBuilder<TBl>>,
/// An optional, shared finality proof request provider.
pub finality_proof_provider: Option<Arc<dyn FinalityProofProvider<TBl>>>,
/// The builder for an IPLD store.f
Copy link
Contributor

Choose a reason for hiding this comment

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

typo

/// Something that builds an IPLD store. You generally want `()`, which always returns `None`.
pub trait IpldStoreBuilder {
/// The type of store to potentially build.
type Store: libipld::store::Store<Params=libipld::DefaultStoreParams>+
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this not be fixed to DefaultStoreParams?

) -> Result<(), libipld::error::Error> {
match event {
sc_network::BitswapEvent::ReceivedBlock(_, cid, data) => {
storage.insert(libipld::Block::new(cid.clone(), data.to_vec())?).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

ipfs-embed moved away from get making any changes to the stored blocks. in ipfs-embed this will actually not insert anything at all as the block isn't pinned. I see the need for recording the timestamps and having a mixture of pins, refcounts and timestamps determine if a block is live or not. That way the store can be used as a cache without leading to storage leakage.

Ok(())
},
sc_network::BitswapEvent::ReceivedWant(peer_id, cid, _) => {
let block = storage.get(&cid).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

get will potentially try to fetch the block from the network. I think you only want to respond if you have the block locally. If the block isn't available locally or found on the network it will return a BlockNotFound. In general there is a problem that the semantics of go-ipfs, js-ipfs are not clear (or outright insufficient), and while formulating and implementing precise semantics for ipfs-embed in terms of atomicity/durability/garbage collection, they are still in flux as more experience is gained building applications on top. That's why I'd recommend completely removing any storage logic for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So should I just remove handle_bitswap_event and IpldStoreBuilder entirely?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure how you intend to use it. While exchanging blocks with full nodes and validators could be interesting if light clients can pay them to store blocks for them, for now I'm mainly interested in using it for light client to light client data exchange. So I think removing those entirely would be fine for now.

Copy link
Contributor

@dvc94ch dvc94ch left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks.

@gnunicorn
Copy link
Contributor

as this is mainly in the network, I think @tomaka should have something to say about this...

Comment on lines 64 to 65
libp2p-bitswap = "0.7.0"
tiny-cid = "0.2.5"
Copy link
Contributor

Choose a reason for hiding this comment

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

Alphabetical ordering, please!

@@ -54,6 +54,8 @@ pub struct Behaviour<B: BlockT, H: ExHashT> {
peer_info: peer_info::PeerInfoBehaviour,
/// Discovers nodes of the network.
discovery: DiscoveryBehaviour,
/// Exchanges blocks of data with other nodes.
pub(crate) bitswap: libp2p_bitswap::Bitswap,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer if you added methods to Behaviour rather than exposing implementation details.

@@ -24,7 +24,7 @@ use std::iter::Iterator;
/// In-memory storage for offchain workers.
#[derive(Debug, Clone, Default)]
pub struct InMemOffchainStorage {
storage: HashMap<Vec<u8>, Vec<u8>>,
pub(crate) storage: HashMap<Vec<u8>, Vec<u8>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't my code, but similarly I don't think it's great software design.

Comment on lines 601 to 604
/// Get the number of bitswap peers we are connected to.
pub fn bitswap_num_peers(&self) -> usize {
self.network_service.bitswap.peers().count()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What's a "bitswap peer" and what's a "non-bitswap peer"?

Copy link
Contributor

Choose a reason for hiding this comment

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

So these are open connections to other peers. It will send want requests to any peer that connects that speaks bitswap. For each open connection it maintains a list of blocks the other peer wants. If in the future the block is available it sends it to the peer. This architecture is because when I first wrote bitswap it seemed like you could have a strategy that gets a block from a friendly peer to get a block from a new peer. Now I think it should probably just ignore the want requests of blocks it doesn't have.

@@ -21,6 +21,7 @@ use bytes::Bytes;
use libp2p::core::PeerId;
use libp2p::kad::record::Key;
use sp_runtime::ConsensusEngineId;
use crate::behaviour::BitswapEvent;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
use crate::behaviour::BitswapEvent;
pub use crate::behaviour::BitswapEvent;

) -> (Self, impl Future<Output = ()> + Send + 'static)
) -> (Self, impl Future<Output = ()> + 'static)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change needed for this PR?
It's not a negative change, but please don't introduce changes that are unrelated to the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -87,6 +87,11 @@ impl TestPersistentOffchainDB {
}
}
}

/// Get whether the DB is empty.
pub fn is_empty(&self) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really see how this is relevant for the rest of the PR.

Comment on lines 1046 to 1072
/// Send a bitswap block to a peer.
pub fn bitswap_send_block(&self, peer_id: PeerId, cid: tiny_cid::Cid, data: Box<[u8]>) {
let _ = self
.to_worker
.unbounded_send(ServiceToWorkerMsg::BitswapSendBlock(peer_id, cid, data));
}

/// Send a bitswap block to all peers that have the block in their wantlist.
pub fn bitswap_send_block_all(&self, cid: tiny_cid::Cid, data: Box<[u8]>) {
let _ = self
.to_worker
.unbounded_send(ServiceToWorkerMsg::BitswapSendBlockAll(cid, data));
}

/// Send a bitswap WANT request to all peers for a block.
pub fn bitswap_want_block(&self, cid: tiny_cid::Cid, priority: i32) {
let _ = self
.to_worker
.unbounded_send(ServiceToWorkerMsg::BitswapWantBlock(cid, priority));
}

/// Cancel a bitswap WANT request.
pub fn bitswap_cancel_block(&self, cid: tiny_cid::Cid) {
let _ = self
.to_worker
.unbounded_send(ServiceToWorkerMsg::BitswapCancelBlock(cid));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be documented.
I don't know how bitswap works, and that's great because it should in my opinion be possible for someone who doesn't know how bitswap works to understand this API.

What's the general workflow of the thing? What happens if send_block is called on a peer that doesn't want this block?

Copy link
Contributor

Choose a reason for hiding this comment

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

bitswap_send_block is usually in response to a want event. the block is added to a queue and if a cancel request is received before the message is sent it will be removed from the queue. reading the code, if the cancel request comes in before the want request was processed, the cancel request will be ignored. I guess it could use some work, but it's not "terrible"

Comment on lines 185 to 186
/// A block was received.
ReceivedBlock(PeerId, tiny_cid::Cid, Box<[u8]>),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be called multiple times with the same block? What's the relationship between the Cid and the data? Is it guaranteed to be in response to a WANT request?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes

Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be called multiple times with the same block?

no, unless the events weren't polled until the block arrived and a new want request was submitted.

What's the relationship between the Cid and the data

the cid hash matches the data, the cid is the one submitted in the want request

Is it guaranteed to be in response to a WANT request?

it's guaranteed to be in response to a want request.

///
/// There are no guarantees about the recieved block:
/// * The CID and block contents might not match.
/// * We might have receieved a block without having sent out a want request first.
Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds like an attack vector.
@dvc94ch Can a node just connect and send, say, 2 GiB of data without us having asked for it, and we will simply buffer it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should probably be combined with the reputation system right?

Copy link
Contributor

@dvc94ch dvc94ch Sep 22, 2020

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dvc94ch right, but that 2 GiB of data is still held in memory for a period of time, right?

Copy link
Contributor

@dvc94ch dvc94ch Sep 23, 2020

Choose a reason for hiding this comment

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

Well that depends on the transport. A message is processed from the queue, and dropped if it wasn't requested. We don't have a sophisticated mechanism for disconnecting from peers, I thought that substrate handles bandwidth limiting, telemetry and reputation, and instead of handling this stuff myself in bitswap/ipfs-embed, I thought we can leverage that. I'm glad to redesign bitswap if there is some input on how it should work

Copy link
Contributor

Choose a reason for hiding this comment

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

A message is processed from the queue, and dropped if it wasn't requested. We don't have a sophisticated mechanism for disconnecting from peers, I thought that substrate handles bandwidth limiting, telemetry and reputation, and instead of handling this stuff myself in bitswap/ipfs-embed, I thought we can leverage that. I'm glad to redesign bitswap if there is some input on how it should work

I'm not familiar with the bitswap protocol or the internals of bitswap, so I'm just naively asking.
The problem I have in mind is: someone can just connect, open 1000 substreams (or whatever the maximum is), negotiate bitswap, send a length prefix of N, and then send N - 1 bytes but never the last byte. Do this multiple times simultaneously and you can probably make the node run out of memory.
This is entirely specific to the bitswap protocol, and not something Substrate can handle.

What we do for other protocols is enforce a limit of one substream per protocol per connection, and make sure that the remote can never send a large amount of data without having explicitly requested it first.

Copy link
Contributor

@dvc94ch dvc94ch Sep 28, 2020

Choose a reason for hiding this comment

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

What we do for other protocols is enforce a limit of one substream per protocol per connection

ipfs-rust/libp2p-bitswap#7, can you point me to a specific protocol that handles this, so I can look how it's done?

make sure that the remote can never send a large amount of data without having explicitly requested it first.

I think this is already handled. Does this satisfy the requirement? https://github.com/ipfs-rust/libp2p-bitswap/blob/master/src/behaviour.rs#L216 Or is more work needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

can you point me to a specific protocol that handles this, so I can look how it's done?

I'm not familiar with this code unfortunately, but libp2p's request-response should do that

I think this is already handled. Does this satisfy the requirement? https://github.com/ipfs-rust/libp2p-bitswap/blob/master/src/behaviour.rs#L216 Or is more work needed?

As far as I understand, you're still entirely buffering the message, and then only discard it, so it really depends on the maximum size of the message. If it's 512 kiB it's ok, if it's 2 GiB it's not.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think these issues are handled much better now ipfs-rust/libp2p-bitswap#8.

@tomaka how concerned are you with interopability with other implementations? In particular now we avoid packing multiple requests responses in the same message, and reuse the substream to send multiple messages. Also even if we add that supporting bitswap 1.1.0 and 1.0.0 is not something I'm particularly interested in working on, and the api is really focused around bitswap 1.2.0 which works completely different from previous versions.

Copy link
Contributor

Choose a reason for hiding this comment

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

how concerned are you with interopability with other implementations?

If this feature is used specifically for large files storage using Substrate, I guess we don't really care.

@dvc94ch
Copy link
Contributor

dvc94ch commented Sep 23, 2020

So where do we go from here? I guess I could pr libp2p-bitswap to rust-libp2p, then we could review that first before moving on to the substrate specifics?

@dvc94ch
Copy link
Contributor

dvc94ch commented Sep 26, 2020

@expenses it seems the docs are still incorrect. If you're busy with other stuff, feel free to close the pr and I'll merge the changes that make sense into mine.

@expenses
Copy link
Contributor Author

@dvc94ch ok, that makes sense 👍

@expenses expenses closed this Sep 28, 2020
@tomaka
Copy link
Contributor

tomaka commented Sep 28, 2020

Link for note keeping: #7133

@expenses expenses deleted the ashley-bitswap branch December 10, 2020 14:34
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A0-please_review Pull request needs code review. B0-silent Changes should not be mentioned in any release notes C1-low PR touches the given topic and has a low impact on builders.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants