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

feat: implement circuit v2 #1533

Merged
merged 60 commits into from
Mar 2, 2023
Merged

Conversation

ckousik
Copy link
Contributor

@ckousik ckousik commented Jan 5, 2023

Reopen the earlier circuit v2 implementation by @mpetrunic with a few fixes to ensure it is up to date with libp2p.

@mpetrunic looks mostly okay, but could you do a rough pass to make sure I haven't missed anything?

Todo:

@p-shahi p-shahi mentioned this pull request Jan 5, 2023
1 task
@p-shahi p-shahi linked an issue Jan 5, 2023 that may be closed by this pull request
active?: boolean
}

export interface AutoRelayConfig {
Copy link
Member

Choose a reason for hiding this comment

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

Do we still use this? We should either remove or rename 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.

We use it to automatically initiate a reservation on newly connected peers or if the circuit relay protocol is added to an existing peer.

@p-shahi
Copy link
Member

p-shahi commented Jan 17, 2023

Is there anything blocking the PR review?

@mpetrunic
Copy link
Member

Is there anything blocking the PR review?

I think todo's are:

  • revisit configs
  • add go interop tests

But I think this shouldn't block reviews^^

Copy link
Member

@achingbrain achingbrain left a comment

Choose a reason for hiding this comment

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

First pass of reviewing

  • The RelayConfig, HopConfig RelayAdvertiseConfig and AutoRelayConfig interface fields all need jsdoc comments to explain what they do
  • The doc/CONFIGURATION.md sections detailing relay config needs updating

src/circuit/client.ts Outdated Show resolved Hide resolved
.gitignore Outdated Show resolved Hide resolved
tsconfig.json Outdated Show resolved Hide resolved
src/circuit/relay.ts Outdated Show resolved Hide resolved
src/circuit/v2/stop.ts Outdated Show resolved Hide resolved
src/circuit/client.ts Outdated Show resolved Hide resolved
src/circuit/client.ts Outdated Show resolved Hide resolved
src/circuit/client.ts Outdated Show resolved Hide resolved
src/circuit/client.ts Outdated Show resolved Hide resolved
src/circuit/v1/protocol/index.proto Outdated Show resolved Hide resolved
achingbrain added a commit to ipfs/interop that referenced this pull request Jan 18, 2023
Circuit Relay v2 is being implemented in libp2p/js-libp2p#1533
so enable the tests
@2color
Copy link
Contributor

2color commented Jan 19, 2023

I'm curious how this PR relates to AutoNAT (#1005).

Isn't AutoNAT generally a prerequisite prior to doing NAT hole punching?

@p-shahi
Copy link
Member

p-shahi commented Jan 19, 2023

I'm curious how this PR relates to AutoNAT (#1005).

Isn't AutoNAT generally a prerequisite prior to doing NAT hole punching?

@2color Yes AutoNAT is a requirement. You can see our roadmap issue for what's all needed: #1461
RelayV2 is required for WebRTC browser-to-browser as well which is why we're doing this right now.

@p-shahi p-shahi added this to the WebRTC browser-to-brow milestone Jan 19, 2023
src/circuit/client.ts Outdated Show resolved Hide resolved
package.json Outdated
@@ -91,7 +92,8 @@
"test:firefox": "aegir test -t browser -f \"./dist/test/**/*.spec.js\" -- --browser firefox",
"test:firefox-webworker": "aegir test -t webworker -f \"./dist/test/**/*.spec.js\" -- --browser firefox",
"test:examples": "cd examples && npm run test:all",
"test:interop": "aegir test -t node -f dist/test/interop.js"
"test:interop": "aegir test -t node -f dist/test/interop.js",
"test:relay": "aegir test -t node -f \"./dist/test/**/relay.{node,spec}.js\""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: Remove this before merging. This is in place to ease testing.

const stream = await connection.newStream('/libp2p/circuit/relay/0.1.0')

/* eslint-disable-next-line no-console */
console.log('>>>>>> connection established')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NOTE: If the connection is established, connection.newStream fails because of a reset.

return this.components.connectionManager.getConnections(dstPeer)[0] ?? undefined
}

async _onProtocolV1 (data: IncomingStreamData) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mpetrunic Should we disable circuit v1 relaying entirely? There seems to be no reason to register this handler as it will not relay.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, sure! I was just copying logic that go-libp2p had but I'm pretty sure nowadays, there is no need for that!

addressSorter?: AddressSorter
export interface HopConfig {
enabled?: boolean
active?: boolean
Copy link
Contributor Author

Choose a reason for hiding this comment

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

How are active and enabled different? Also, we only use enabled in Relay to decide whether or not to advertise the node as a relay.

src/circuit/index.ts Show resolved Hide resolved
ttl?: number
}

export interface HopConfig {
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 is duplicated.


await this.components.registrar.handle(RELAY_CODEC, (data) => {
void this._onProtocol(data).catch(err => {
await this.components.registrar.handle(RELAY_V2_HOP_CODEC, (data) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should adding this listener be conditional on HopConfig.enabled | HopConfig.active === true ?

Copy link
Member

Choose a reason for hiding this comment

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

Since circuit v2 kinda changed interaction, maybe it makes sense to make up new configs and names that are more dev friendly

* @param limit - maximum number of reservations to store
* @param reservationClearInterval - interval to check for expired reservations in millisecons
*/
constructor (private readonly limit = 15, private readonly reservationClearInterval = 300 * 1000) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Move these values to constants.ts.

src/circuit/client.ts Outdated Show resolved Hide resolved
@achingbrain
Copy link
Member

achingbrain commented Feb 28, 2023

For data limits, the Go code discards any data read (silently) after the limit is exceeded, while for the duration limit, it fails with a ErrDeadlineExceeded once the duration is completed. In the js case, the current implementation closes the source/sink silently once either limit is exceeded. Is this the required behaviour?

Having talked with @Jorropo and @mxinden this isn't the correct behaviour - I've opened libp2p/specs#526 to document what should happen - if everyone agrees with it then the stream should be reset.

src/circuit/v2/util.ts Outdated Show resolved Hide resolved
src/circuit/v2/util.ts Outdated Show resolved Hide resolved
private readonly autoRelay?: AutoRelay
private timeout?: any
private started: boolean
enabled?: boolean
Copy link
Member

Choose a reason for hiding this comment

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

Under what circumstances would a user want to disable the reservation manager?

Are there tests for when it's disabled to ensure libp2p can run without 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.

We would want to disable the reservation manager when we do not want to automatically listen on relays. It replaces the old name AutoRelay. #1533 (comment).

The relay tests disable this by default https://github.com/libp2p/js-libp2p/pull/1533/files#diff-81971332c1841fd8324647bd37da55f446e332af9e07d4f28632d5768302dac9R27

src/circuit/v2/util.ts Outdated Show resolved Hide resolved
async _onProtocol (data: IncomingStreamData) {
const { connection, stream } = data
async onHop ({ connection, stream }: IncomingStreamData) {
log('received circuit v2 hop protocol stream from %s', connection.remotePeer)
const controller = new TimeoutController(this._init.hop.timeout)
Copy link
Member

Choose a reason for hiding this comment

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

What's the benefit of having a hop timeout separate from the underlying transport timeout?

Abortable streams are expensive because they race each buffer against the abort controller so if it's not necessary this could be simplified.

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 is the time in which the hop protocol must be completed. The hop timeout is much lower than the transport timeout limit, and is only used while 1) a new reservation is being created, or 2) a client is attempting to connect over the relay. Once the operations are complete, the timeout is cleared.

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 think I can avoid the abortableDuplex though.

@ckousik ckousik requested a review from achingbrain March 1, 2023 13:38
const dataLimitSource = (source: Source<Uint8ArrayList>, abort: (err: Error) => void, limit: bigint): Source<Uint8ArrayList> => {
if (limit === BigInt(0)) {
return source
const dataLimitSource = (stream: Stream, limit: bigint): Stream => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this safe? While the implementation of it-pb-stream returns the underlying stream, the return type only guarantees returning a Duplex. The stream functions could be erased from the returned value in a future release without changing the API, which would cause this to break.

Copy link
Member

Choose a reason for hiding this comment

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

3.0.1 changes the unwrap return type to be the type of the wrapped stream, so it should be safe.

Copy link
Member

@achingbrain achingbrain left a comment

Choose a reason for hiding this comment

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

Nearly there, comments inline.

Comment on lines 498 to 507
// source to dest, write 4 bytes
const sender = pushable()
void pipe(sender, sourceStream)
sender.push(uint8arrayFromString('01234'))
// source to dest, exceed stream limit
sender.push(uint8arrayFromString('extra'))
const data = await all(destStream.source)
expect(data).to.have.length(1)
expect(data[0]).to.have.length(5)
expect(srcServerAbort.callCount).to.equal(1)
Copy link
Member

Choose a reason for hiding this comment

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

This sends five bytes, not four. When four bytes are sent the test fails.

The dialler should send 0123 then extra, then the relay should relay five bytes before reset the stream.

Also, the test needs to assert that both streams were reset - from the dialler to the relay and from the relay to the listener.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. I was resetting the stream if the sender attempted to send more than the limit.

Also, the test needs to assert that both streams were reset - from the dialler to the relay and from the relay to the listener.

The relay enforces the limit, so the abort is called from the relay to the dialler and from the relay to the listener. This is checked.

const dataLimitSource = (source: Source<Uint8ArrayList>, abort: (err: Error) => void, limit: bigint): Source<Uint8ArrayList> => {
if (limit === BigInt(0)) {
return source
const dataLimitSource = (stream: Stream, limit: bigint): Stream => {
Copy link
Member

Choose a reason for hiding this comment

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

3.0.1 changes the unwrap return type to be the type of the wrapped stream, so it should be safe.

test/circuit/v2/hop.spec.ts Outdated Show resolved Hide resolved
src/circuit/v2/hop.ts Outdated Show resolved Hide resolved
@ckousik ckousik requested a review from achingbrain March 2, 2023 10:01
src/circuit/v2/util.ts Outdated Show resolved Hide resolved
@ckousik ckousik requested a review from achingbrain March 2, 2023 10:56
@@ -38,8 +46,10 @@ export class ReservationStore implements IReservationStore, Startable {
this.init = {
maxReservations: options?.maxReservations ?? 15,
reservationClearInterval: options?.reservationClearInterval ?? 300 * 1000,
applyDefaultLimit: options?.applyDefaultLimit === false,
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 fun, it would have turned every js-libp2p node with the relay server enabled into an unlimited relay 😱

Copy link
Member

@achingbrain achingbrain left a comment

Choose a reason for hiding this comment

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

I am going to merge this which will publish an RC to unblock other efforts as it is functional though it still needs some work in a follow-up before being released.

  • Split the code into two parts - the transport and the relay server
    • At the moment the transport handles some of the server responsibilities
    • The hop/stop implementations accept a load of context as arguments that might be easier to reason about as a class
  • Configure the transport and relay server separately - at the moment the server is partially configured under the config.relay.hop key and config.relay.advertise - the fact that the docs say "Does not make you a relay" in several places mean it's really not obvious
  • Maybe even have a separate CircuitRelay transport that you can configure
  • More tests

@achingbrain achingbrain merged commit d605cbe into libp2p:master Mar 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Status: Done
Development

Successfully merging this pull request may close these issues.

Implement Circuit Relay v2 in JS
6 participants