You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When nim-libp2p (dialer) runs identify against a py-libp2p listener (e.g. transport-interop: WebSocket + Noise + Yamux), the Nim side can fail with:
Failed to finish outgoing upgrade in internalConnect: Incorrect message received!
That string comes from nim-libp2p’s identify decoder when decodeMsg does not return a valid IdentifyInfo—not from multistream or the WebSocket upgrade.
This discussion collects what we found, how Go and JavaScript handle the same signed peer record (SPR) field, and a practical workaround used in libp2p interop tests.
What Nim does (decodeMsg)
Nim parses the identify protobuf with a strict pipeline: failures while decoding individual fields can invalidate the entire message.
Relevant excerpt from nim-libp2p (libp2p/protocols/identify.nim):
Field 8 is the signed peer record (optional bytes signedPeerRecord = 8 in implementation protos). If present, Nim tries to decode it as its SignedPeerRecord type; wire bytes that do not decode cleanly can contribute to decode failure (depending on error propagation in the ? / toOpt path).
Repeated field 2 (listenAddrs): Nim’s multiaddr-specific getRepeatedField requires that at least one entry parses as a MultiAddress. If every entry fails (e.g. multiaddrs Nim’s parser rejects), the repeated-field decode can return IncorrectBlob, and the whole identify decode fails.
What py-libp2p sends (identify + SPR)
Default identify builds a protobuf that includes signedPeerRecord from env_to_send_in_RPC (libp2p/identity/identify/identify.py):
Go uses PeerRecordEnvelopeDomain = "libp2p-peer-record" (core/peer/record.go), matching py-libp2p’s ENVELOPE_DOMAIN.
Sending: SPR is attached when not disabled and a certified peer record exists (getSignedRecord); hosts can opt out via DisableSignedPeerRecord.
js-libp2p (@libp2p/protocol-identify)
Sending (packages/protocol-identify/src/identify.ts): uses peerData.peerRecordEnvelope or seals a new PeerRecord with RecordEnvelope.seal and passes signedPeerRecord: envelope.marshal() (plus listenAddrs from addresses decapsulated from /p2p/).
Several fields are optional; identify/push explicitly says missing fields may be ignored for partial updates. That does not force every implementation to tolerate malformed optional fields on the pull side, but it supports sending minimal identify when interop requires it.
Workaround used in libp2p test-plans (Python interop image)
For nim × python transport-interop, the Python ping harness monkeypatches_mk_identify_protobuf to clear fields that were triggering Nim’s strict decoder on WS interop: listen_addrs, observed_addr, protocols, and signedPeerRecord, leaving pubkey + agent/protocol version strings.
That is a test-harness shim, not a claim that py-libp2p is “wrong”—it documents where byte-level interop with nim-libp2p still diverges.
Interop tests: optional CI matrix nim-libp2p dialer × py-libp2p listener (or a small integration test) to catch identify regressions.
py-libp2p: consider whether identify should omit or normalize SPR / listen addrs when targeting strict peers, or document known limitations with nim-libp2p.
nim-libp2p: consider per-field decode resilience (skip bad optional fields) vs strict fail-fast—tradeoffs are real.
Signed envelopes / peer records: libp2p RFC 0002 (signed envelopes)
If useful, this can be turned into a tracking issue for code changes; opening as a Discussion first to align on whether the fix belongs in py-libp2p, nim-libp2p, or both.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
When nim-libp2p (dialer) runs identify against a py-libp2p listener (e.g. transport-interop: WebSocket + Noise + Yamux), the Nim side can fail with:
Failed to finish outgoing upgrade in internalConnect: Incorrect message received!That string comes from nim-libp2p’s identify decoder when
decodeMsgdoes not return a validIdentifyInfo—not from multistream or the WebSocket upgrade.This discussion collects what we found, how Go and JavaScript handle the same signed peer record (SPR) field, and a practical workaround used in libp2p interop tests.
What Nim does (
decodeMsg)Nim parses the identify protobuf with a strict pipeline: failures while decoding individual fields can invalidate the entire message.
Relevant excerpt from nim-libp2p (
libp2p/protocols/identify.nim):Field 8 is the signed peer record (
optional bytes signedPeerRecord = 8in implementation protos). If present, Nim tries to decode it as itsSignedPeerRecordtype; wire bytes that do not decode cleanly can contribute to decode failure (depending on error propagation in the?/toOptpath).Repeated field 2 (
listenAddrs): Nim’s multiaddr-specificgetRepeatedFieldrequires that at least one entry parses as aMultiAddress. If every entry fails (e.g. multiaddrs Nim’s parser rejects), the repeated-field decode can returnIncorrectBlob, and the whole identify decode fails.What py-libp2p sends (identify + SPR)
Default identify builds a protobuf that includes
signedPeerRecordfromenv_to_send_in_RPC(libp2p/identity/identify/identify.py):env_to_send_in_RPC(libp2p/peer/peerstore.py) creates or reuses a signed envelope with domain aligned to the rest of the ecosystem:So intended format is the standard signed envelope + peer record (RFC-style), same domain string as Go.
go-libp2p (reference)
Proto (
p2p/protocol/identify/pb/identify.proto):Parsing (
p2p/protocol/identify/id.go):Go uses
PeerRecordEnvelopeDomain="libp2p-peer-record"(core/peer/record.go), matching py-libp2p’sENVELOPE_DOMAIN.Sending: SPR is attached when not disabled and a certified peer record exists (
getSignedRecord); hosts can opt out viaDisableSignedPeerRecord.js-libp2p (
@libp2p/protocol-identify)Sending (
packages/protocol-identify/src/identify.ts): usespeerData.peerRecordEnvelopeor seals a newPeerRecordwithRecordEnvelope.sealand passessignedPeerRecord: envelope.marshal()(pluslistenAddrsfrom addresses decapsulated from/p2p/).Consuming (
packages/protocol-identify/src/utils.ts):Same conceptual pipeline: envelope bytes + peer-record domain + inner PeerRecord.
libp2p identify spec (v1.0.0 README)
The published identify doc lists the core message without field 8 (extensions added in implementations):
Several fields are optional;
identify/pushexplicitly says missing fields may be ignored for partial updates. That does not force every implementation to tolerate malformed optional fields on the pull side, but it supports sending minimal identify when interop requires it.Workaround used in libp2p
test-plans(Python interop image)For nim × python transport-interop, the Python ping harness monkeypatches
_mk_identify_protobufto clear fields that were triggering Nim’s strict decoder on WS interop:listen_addrs,observed_addr,protocols, andsignedPeerRecord, leaving pubkey + agent/protocol version strings.That is a test-harness shim, not a claim that py-libp2p is “wrong”—it documents where byte-level interop with nim-libp2p still diverges.
Suggested follow-ups (for maintainers / community)
References
decodeMsg,identify()readingreadLp/decodeMsglibp2p/identity/identify/identify.py,libp2p/peer/peerstore.py,libp2p/peer/envelope.pyp2p/protocol/identify/id.go,p2p/protocol/identify/pb/identify.protopackages/protocol-identify/src/identify.ts,packages/protocol-identify/src/utils.tsspecs/identify/README.md(identify v1.0.0)If useful, this can be turned into a tracking issue for code changes; opening as a Discussion first to align on whether the fix belongs in py-libp2p, nim-libp2p, or both.
Beta Was this translation helpful? Give feedback.
All reactions