Skip to content

Commit

Permalink
fix!: close streams gracefully
Browse files Browse the repository at this point in the history
- Refactors `.close`, `closeRead` and `.closeWrite` methods on the `Stream` interface to be async
- The `Connection` interface now has `.close` and `.abort` methods
- `.close` on `Stream`s and `Connection`s wait for the internal message queues to empty before closing
- `.abort` on `Stream`s and `Connection`s close the underlying stream immediately and discards any unsent data
- `@chainsafe/libp2p-yamux` now uses the `AbstractStream` class from `@libp2p/interface` the same as `@libp2p/mplex` and
`@libp2p/webrtc`

Follow-up PRs will be necessary to `@chainsafe/libp2p-yamux`, `@chainsafe/libp2p-gossipsub` and `@chainsafe/libp2p-noise`
though they will not block the release as their code is temporarily added to this repo to let CI run.

Fixes #1793
Fixes #656

BREAKING CHANGE: the `.close`, `closeRead` and `closeWrite` methods on the `Stream` interface are now asynchronous
  • Loading branch information
achingbrain committed Jul 20, 2023
1 parent b19c08f commit a4d77b5
Show file tree
Hide file tree
Showing 132 changed files with 4,336 additions and 2,207 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ jobs:
- uses: actions/checkout@v3
- uses: ipfs/aegir/actions/cache-node-modules@master
- name: Build images
run: (cd interop && make)
run: (cd interop && make -j 4)
- name: Save package-lock.json as artifact
uses: actions/upload-artifact@v2
with:
Expand All @@ -197,6 +197,7 @@ jobs:
- uses: libp2p/test-plans/.github/actions/run-interop-ping-test@master
with:
test-filter: js-libp2p-head
test-ignore: nim
extra-versions: ${{ github.workspace }}/interop/node-version.json ${{ github.workspace }}/interop/chromium-version.json ${{ github.workspace }}/interop/firefox-version.json
s3-cache-bucket: ${{ vars.S3_LIBP2P_BUILD_CACHE_BUCKET_NAME }}
s3-access-key-id: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_ACCESS_KEY_ID }}
Expand Down
2 changes: 1 addition & 1 deletion doc/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const node = await createLibp2p({
To define component metrics first get a reference to the metrics object:

```ts
import type { Metrics } from '@libp2p/interface-metrics'
import type { Metrics } from '@libp2p/interface/metrics'

interface MyClassComponents {
metrics: Metrics
Expand Down
2 changes: 2 additions & 0 deletions interop/BrowserDockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ WORKDIR /app/interop
RUN npx playwright install
ARG BROWSER=chromium # Options: chromium, firefox, webkit
ENV BROWSER=$BROWSER
# disable colored output and CLI animation from test runners
ENV CI true

ENTRYPOINT npm run test:interop:multidim -- --build false --types false -t browser -- --browser $BROWSER
3 changes: 3 additions & 0 deletions interop/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ COPY ./interop ./interop

WORKDIR /app/interop

# disable colored output and CLI animation from test runners
ENV CI true

ENTRYPOINT [ "npm", "run", "test:interop:multidim", "--", "--build", "false", "--types", "false", "-t", "node" ]
79 changes: 76 additions & 3 deletions interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,87 @@
## Table of contents <!-- omit in toc -->

- [Install](#install)
- [Usage](#usage)
- [Build js-libp2p](#build-js-libp2p)
- [node.js](#nodejs)
- [Browsers](#browsers)
- [Build another libp2p implementation](#build-another-libp2p-implementation)
- [Running Redis](#running-redis)
- [Start libp2p](#start-libp2p)
- [Start another libp2p implementation](#start-another-libp2p-implementation)
- [License](#license)
- [Contribution](#contribution)

## Install
## Usage

The multidim interop tests use random high ports for listeners. Since you need to know which port will be listened on ahead of time to `EXPOSE` a port in a Docker image to the host machine, this means everything has to be run in Docker.

### Build js-libp2p

This must be repeated every time you make a change to the js-libp2p source code.

#### node.js

```console
$ npm run build
$ docker build . -f ./interop/Dockerfile -t js-libp2p-node
```

#### Browsers

```console
$ npm run build
$ docker build . -f ./interop/BrowserDockerfile -t js-libp2p-browsers
```

### Build another libp2p implementation

1. Clone the test-plans repo somewhere
```console
$ git clone https://github.com/libp2p/test-plans.git
```
2. (Optional) If you are running an M1 Mac you may need to override the build platform.
- Edit `/multidim-interop/dockerBuildWrapper.sh`
- Add `--platform linux/arm64/v8` to the `docker buildx build` command
```
docker buildx build \
--platform linux/arm64/v8 \ <-- add this line
--load \
-t $IMAGE_NAME $CACHING_OPTIONS "$@"
```
3. (Optional) Enable some sort of debug output
- nim-libp2p
- edit `/multidim-interop/impl/nim/$VERSION/Dockerfile`
- Change `-d:chronicles_log_level=WARN` to `-d:chronicles_log_level=DEBUG`
- rust-libp2p
- When starting the docker container add `-e RUST_LOG=debug`
- go-libp2p
- When starting the docker container add `-e GOLOG_LOG_LEVEL=debug`
4. Build the version you want to test against
```console
$ cd impl/$IMPL/$VERSION
$ make
...
```

### Running Redis

Redis is used to allow inter-container communication, exchanging listen addresses etc. It must be started as a Docker container:

```console
$ docker run --name redis --rm -p 6379:6379 redis:7-alpine
```

### Start libp2p

```console
$ docker run -e transport=tcp -e muxer=yamux -e security=noise -e is_dialer=true -e redis_addr=redis:6379 --link redis:redis js-libp2p-node
```

### Start another libp2p implementation

```console
$ npm i multidim-interop
$ docker run -e transport=tcp -e muxer=yamux -e security=noise -e is_dialer=false -e redis_addr=redis:6379 --link redis:redis nim-v1.0
```

## License
Expand Down
1 change: 1 addition & 0 deletions interop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"scripts": {
"start": "node index.js",
"build": "aegir build",
"lint": "aegir lint",
"test:interop:multidim": "aegir test"
},
"dependencies": {
Expand Down
4 changes: 2 additions & 2 deletions interop/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
console.log("Everything is defined in the test folder")
// Everything is defined in the test folder

export { }
export { }
18 changes: 14 additions & 4 deletions interop/test/ping.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ let node: Libp2p<{ ping: PingService, identify: IdentifyService }>
const isDialer: boolean = process.env.is_dialer === 'true'
const timeoutSecs: string = process.env.test_timeout_secs ?? '180'

describe('ping test', () => {
describe('ping test', function () {
// make the default timeout longer than the listener timeout
this.timeout((parseInt(timeoutSecs) * 1000) + 30000)

// eslint-disable-next-line complexity
beforeEach(async () => {
// Setup libp2p node
Expand All @@ -39,6 +42,9 @@ describe('ping test', () => {

const options: Libp2pOptions<{ ping: PingService, identify: IdentifyService }> = {
start: true,
connectionManager: {
minConnections: 0
},
connectionGater: {
denyDialMultiaddr: async () => false
},
Expand Down Expand Up @@ -208,14 +214,18 @@ describe('ping test', () => {
// eslint-disable-next-line complexity
(isDialer ? it : it.skip)('should dial and ping', async () => {
try {
let otherMa: string = (await redisProxy(['BLPOP', 'listenerAddr', timeoutSecs]).catch(err => { throw new Error(`Failed to wait for listener: ${err}`) }))[1]
let otherMaStr: string = (await redisProxy(['BLPOP', 'listenerAddr', timeoutSecs]).catch(err => { throw new Error(`Failed to wait for listener: ${err}`) }))[1]
// Hack until these are merged:
// - https://github.com/multiformats/js-multiaddr-to-uri/pull/120
otherMa = otherMa.replace('/tls/ws', '/wss')
otherMaStr = otherMaStr.replace('/tls/ws', '/wss')

const otherMa = multiaddr(otherMaStr)

console.error(`node ${node.peerId.toString()} pings: ${otherMa}`)
const handshakeStartInstant = Date.now()
await node.dial(multiaddr(otherMa))

await node.dial(otherMa)

const pingRTT = await node.services.ping.ping(multiaddr(otherMa))
const handshakePlusOneRTT = Date.now() - handshakeStartInstant
console.log(JSON.stringify({
Expand Down
99 changes: 99 additions & 0 deletions packages/connection-encryption-noise/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"name": "@chainsafe/libp2p-noise",
"version": "12.0.1",
"author": "ChainSafe <info@chainsafe.io>",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ChainSafe/js-libp2p-noise#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ChainSafe/js-libp2p-noise.git"
},
"bugs": {
"url": "https://github.com/ChainSafe/js-libp2p-noise/issues"
},
"keywords": [
"crypto",
"libp2p",
"noise"
],
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
},
"type": "module",
"types": "./dist/src/index.d.ts",
"files": [
"src",
"dist",
"!dist/test",
"!**/*.tsbuildinfo"
],
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js"
}
},
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/strict-boolean-expressions": "off"
},
"ignorePatterns": [
"src/proto/payload.js",
"src/proto/payload.d.ts",
"test/fixtures/node-globals.js"
]
},
"scripts": {
"bench": "node benchmarks/benchmark.js",
"clean": "aegir clean",
"dep-check": "aegir dep-check",
"build": "aegir build",
"lint": "aegir lint",
"lint:fix": "aegir lint --fix",
"test": "aegir test",
"test:node": "aegir test -t node",
"test:browser": "aegir test -t browser -t webworker",
"test:electron-main": "aegir test -t electron-main",
"docs": "aegir docs",
"proto:gen": "protons ./src/proto/payload.proto",
"prepublish": "npm run build"
},
"dependencies": {
"@libp2p/crypto": "^1.0.11",
"@libp2p/interface": "~0.0.1",
"@libp2p/logger": "^2.1.1",
"@libp2p/peer-id": "^2.0.0",
"@stablelib/chacha20poly1305": "^1.0.1",
"@noble/hashes": "^1.3.0",
"@stablelib/x25519": "^1.0.3",
"it-length-prefixed": "^9.0.1",
"it-length-prefixed-stream": "^1.0.0",
"it-byte-stream": "^1.0.0",
"it-pair": "^2.0.2",
"it-pipe": "^3.0.1",
"it-stream-types": "^2.0.1",
"protons-runtime": "^5.0.0",
"uint8arraylist": "^2.3.2",
"uint8arrays": "^4.0.2"
},
"devDependencies": {
"@libp2p/interface-compliance-tests": "^3.0.0",
"@libp2p/peer-id-factory": "^2.0.0",
"@types/sinon": "^10.0.14",
"aegir": "^39.0.5",
"iso-random-stream": "^2.0.2",
"protons": "^7.0.0",
"sinon": "^15.0.0"
},
"browser": {
"./dist/src/alloc-unsafe.js": "./dist/src/alloc-unsafe-browser.js",
"util": false
}
}
5 changes: 5 additions & 0 deletions packages/connection-encryption-noise/src/@types/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type bytes = Uint8Array
export type bytes32 = Uint8Array
export type bytes16 = Uint8Array

export type uint64 = number
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { bytes } from './basic.js'
import type { NoiseSession } from './handshake.js'
import type { NoiseExtensions } from '../proto/payload.js'
import type { PeerId } from '@libp2p/interface/peer-id'

export interface IHandshake {
session: NoiseSession
remotePeer: PeerId
remoteExtensions: NoiseExtensions
encrypt: (plaintext: bytes, session: NoiseSession) => bytes
decrypt: (ciphertext: bytes, session: NoiseSession, dst?: Uint8Array) => { plaintext: bytes, valid: boolean }
}
48 changes: 48 additions & 0 deletions packages/connection-encryption-noise/src/@types/handshake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { bytes, bytes32, uint64 } from './basic.js'
import type { KeyPair } from './libp2p.js'
import type { Nonce } from '../nonce.js'

export type Hkdf = [bytes, bytes, bytes]

export interface MessageBuffer {
ne: bytes32
ns: bytes
ciphertext: bytes
}

export interface CipherState {
k: bytes32
// For performance reasons, the nonce is represented as a Nonce object
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce
}

export interface SymmetricState {
cs: CipherState
ck: bytes32 // chaining key
h: bytes32 // handshake hash
}

export interface HandshakeState {
ss: SymmetricState
s: KeyPair
e?: KeyPair
rs: bytes32
re: bytes32
psk: bytes32
}

export interface NoiseSession {
hs: HandshakeState
h?: bytes32
cs1?: CipherState
cs2?: CipherState
mc: uint64
i: boolean
}

export interface INoisePayload {
identityKey: bytes
identitySig: bytes
data: bytes
}
10 changes: 10 additions & 0 deletions packages/connection-encryption-noise/src/@types/libp2p.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { bytes32 } from './basic.js'
import type { NoiseExtensions } from '../proto/payload.js'
import type { ConnectionEncrypter } from '@libp2p/interface/connection-encrypter'

export interface KeyPair {
publicKey: bytes32
privateKey: bytes32
}

export interface INoiseConnection extends ConnectionEncrypter<NoiseExtensions> {}
4 changes: 4 additions & 0 deletions packages/connection-encryption-noise/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const NOISE_MSG_MAX_LENGTH_BYTES = 65535
export const NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG = NOISE_MSG_MAX_LENGTH_BYTES - 16

export const DUMP_SESSION_KEYS = Boolean(globalThis.process?.env?.DUMP_SESSION_KEYS)
16 changes: 16 additions & 0 deletions packages/connection-encryption-noise/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { bytes32, bytes } from './@types/basic.js'
import type { Hkdf } from './@types/handshake.js'
import type { KeyPair } from './@types/libp2p.js'

export interface ICryptoInterface {
hashSHA256: (data: Uint8Array) => Uint8Array

getHKDF: (ck: bytes32, ikm: Uint8Array) => Hkdf

generateX25519KeyPair: () => KeyPair
generateX25519KeyPairFromSeed: (seed: Uint8Array) => KeyPair
generateX25519SharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array

chaCha20Poly1305Encrypt: (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32) => bytes
chaCha20Poly1305Decrypt: (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array) => bytes | null
}
Loading

0 comments on commit a4d77b5

Please sign in to comment.