Skip to content

Conversation

Antrikshgwal
Copy link

@Antrikshgwal Antrikshgwal commented Aug 16, 2025

What was wrong?

As tracked by #791, the Relay V2 protocol needs to support Signed Peer Record (SPR) transfer to establish secure and verifiable connection between peers relayed through intermediaries. Currently, Circuit Relay V2 only handles HOP and STOP requests without propagating authenticated peer information.

Issue #847

How was it fixed?

This WIP PR aims to:

  • Extend the Circuit Relay V2 protobuf definitions to include an optional signed_peer_record field in HOP and STOP messages.

  • Implement handlers in the Relay V2 module to:

  • Send the local peer’s SPR along with HOP and STOP requests.

  • Receive and verify incoming SPRs against the peer’s PeerId.

  • Validate that relayed connections are only established when valid SPRs are exchanged.

This design follows the integration strategy used in KAD-DHT and Gossipsub, ensuring consistency across protocols.

The goal is to improve the trustworthiness of peer discovery and relay connections by embedding verifiable identity metadata directly in Relay V2 message flows.

To-Do

  • Clean up commit history
  • Add or update documentation related to these changes
  • Add entry to the release notes

Cute Animal Picture

image

@Antrikshgwal Antrikshgwal marked this pull request as draft August 16, 2025 14:44
@lla-dane
Copy link
Contributor

lla-dane commented Aug 17, 2025

@Antrikshgwal
When you updated the .proto file and recompiled it, the circui_pb2.pyi file should also have been changed, which has not. Make sure you are using this command:

protoc --proto_path=. --python_out=. --pyi_out=. `libp2p/relay/circuit_v2/pb/circuit.proto`

Now further, you should set up the following utilities in the curcuit_v2 module:

  1. maybe_consume_signed_record: For consuming the received records in requests/responses
  2. env_to_send_in_RPC and issue_and_cache_local_record: For issuing a signed-record which broadcasting a message.

For reference, look at libp2p/kad_dht/utils.py file in #815

After that you should look for code-snippets where a HopMessage or StopMessage is received like these:

response_bytes = await stream.read()
if not response_bytes:
  logger.error("No response received from relay %s", peer_id)
  return False

# Parse response
response = HopMessage()
response.ParseFromString(response_bytes)

# Check if reservation was successful
if response.type == HopMessage.RESERVE and response.HasField(
  "status"
):

and add the maybe_consume_signed_record in the way to process the sent record like this:

# Consume the source signed_peer_record if sent
if not maybe_consume_signed_record(message, self.host):
    logger.error(
        "Received an invalid-signed-record, dropping the stream"
    )
    await stream.close()
    return

And in the same way, when we are braodcasting messages, you need to send a singned-record via the env_to_send_in_RPC function.

Now an example of how I did it in dht:

 # Add sender's signed-peer-record
envelope_bytes, bool = env_to_send_in_RPC(self.host)
response.senderRecord = envelope_bytes

You have to add these while we are sending you requests and responses both.
So basically when we receive a message, you need to maybe_consume_peer_record and when we broadcast message, you need to attach a signed-record with the message

CCing @seetadev

@seetadev
Copy link
Contributor

@Antrikshgwal and @lla-dane : This is excellent work — really glad to see steady progress on bringing Signed Peer Record (SPR) support into Circuit Relay V2. Completely agree with the design: without SPR propagation, relayed peers don’t have a verifiable identity layer, and that leaves gaps in the trust model.

Extending the protocol to carry signed_peer_record in HOP/STOP flows is exactly the right direction, and it aligns well with the precedent we already have in KAD-DHT and Gossipsub. That consistency across protocols will make adoption and reasoning about peer identity much smoother.

Appreciate the concrete steps outlined in the WIP PR:

Updating the protobuf definitions with optional signed_peer_record.

Implementing both sending and verifying SPRs in HOP/STOP handlers.

Enforcing that relayed connections only succeed when records are valid.

This design ensures that relay connections carry the same authenticity guarantees as direct connections, which is a big step forward for overall security.

@lla-dane’s feedback on implementation steps is indeed very important — especially around integrating maybe_consume_signed_record, env_to_send_in_RPC, and issue_and_cache_local_record.

These utilities have already proven reliable in DHT, so reusing the same patterns here is a smart move. Adding these hooks in the message receive/send paths for HOP and STOP will give us robust, testable behavior.

Looking forward to the rebased PR with the protobuf updates, SPR utilities wired in, and commit history tidied up. Great momentum here — keep it up!

@seetadev
Copy link
Contributor

@lla-dane and @Antrikshgwal : Ran the CI/CD pipeline. Please take your time in resolving the issues.

@Antrikshgwal
Copy link
Author

Hey @seetadev! I have a doubt, according to the proto file I pushed, we are sending records in the hop messages and receiving records in the stop messages. Then, according to the diagram, Peer B can't receive records of Peer A through the relay circuit. I am not sure if it is the intended behaviour.
I guessed that maybe after establishing a switched connection, B can get the records from A, so it is not important to send through the relay. Please clarify.

@Antrikshgwal Antrikshgwal marked this pull request as ready for review August 21, 2025 18:41
Comment on lines 53 to 78
def issue_and_cache_local_record(host: IHost) -> bytes:
env = create_signed_peer_record(
host.get_id(),
host.get_addrs(),
host.get_private_key(),
)
# Cache it for next time use
host.get_peerstore().set_local_record(env)
return env.marshal_envelope()


def env_to_send_in_RPC(host: IHost) -> tuple[bytes, bool]:
listen_addrs_set = {addr for addr in host.get_addrs()}
local_env = host.get_peerstore().get_local_record()

if local_env is None:
# No cached SPR yet -> create one
return issue_and_cache_local_record(host), True
else:
record_addrs_set = local_env._env_addrs_set()
if record_addrs_set == listen_addrs_set:
# Perfect match -> reuse cached envelope
return local_env.marshal_envelope(), False
else:
# Addresses changed -> issue a new SPR and cache it
return issue_and_cache_local_record(host), True No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

Move these 2 functions to peer/peerstore.py file, as done in #835 and #815, and add docstrings for this function too.

Comment on lines 16 to 50
def maybe_consume_signed_record(msg: HopMessage | StopMessage, host: IHost) -> bool:
if isinstance(msg, StopMessage):
if msg.HasField("senderRecord"):
try:
# Convert the signed-peer-record(Envelope) from
# protobuf bytes
envelope, _ = consume_envelope(
msg.senderRecord,
"libp2p-peer-record",
)
# Use the default TTL of 2 hours (7200 seconds)
if not host.get_peerstore().consume_peer_record(envelope, 7200):
logger.error("Updating the certified-addr-book was unsuccessful")
except Exception as e:
logger.error("Error updating the certified addr book for peer: %s", e)
return False
elif isinstance(msg,HopMessage):
if msg.HasField("signedRecord"):
try:
# Convert the signed-peer-record(Envelope) from
# protobuf bytes
envelope, _ = consume_envelope(
msg.signedRecord,
"libp2p-peer-record",
)
# Use the default TTL of 2 hours (7200 seconds)
if not host.get_peerstore().consume_peer_record(envelope, 7200):
logger.error("Failed to update the Certified-Addr-Book")
except Exception as e:
logger.error(
"Error updating the certified-addr-book: %s",
e,
)
return False
return True
Copy link
Contributor

Choose a reason for hiding this comment

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

Add docstrings for this function. Add the checker for verifying if the peer-id who sent the record is same as the senderRecord's peer id. Refer the corresponding function in #815.

@seetadev
Copy link
Contributor

@Antrikshgwal : Hi Antriksh. Thank you for submitting the PR and making improvements. Appreciate it.

Please resolve the CI/CD issues. CCing @lla-dane and @sumanjeet0012, who will help you arrive at a good conclusion on the issue.

Also, please add the test suite like the one @lla-dane added for adding signed peer record transfer support in kad-dht and pub-sub/gossipsub. Please consult with @lla-dane in detail as you make improvements.

@sumanjeet0012
Copy link
Contributor

The CI/CD pipeline currently has a couple of issues:

1. Failing Test Cases

Currently 4 circuit relay tests are failing

  • you can test locally before pushing changes using:
pytest <path/to/file or directory>

2. Linting Issues

There are several linting issues:

  • Run the following command locally to automatically fix resolvable issues:
pre-commit run --all-files
  • Manually address any remaining issues that cannot be automatically resolved

3. Documentation Issue

Status: Module import error during documentation generation

Error Message:

WARNING: autodoc: failed to import module 'relay' from module 'libp2p'; the following exception was raised:

Root Cause: The relay module is not properly available in libp2p

Solution:
Create __init__.py files in the following directories:

  • libp2p/relay/
  • libp2p/relay/pb/

@Antrikshgwal Let me know if you need any help.

@Antrikshgwal
Copy link
Author

Antrikshgwal commented Aug 30, 2025

Create __init__.py files in the following directories:

  • libp2p/relay/
  • libp2p/relay/pb/

Hi @sumanjeet0012
These directories already have __init__.py files.

"shift create_signed_records in peerstore"
@seetadev
Copy link
Contributor

@Antrikshgwal : Hi Antriksh. Re-ran the CI/CD pipeline.

Please resolve the CI/CD issues. CCing @lla-dane and @sumanjeet0012, who will help you arrive at a good conclusion on the issue.

Also, please add the test suite like the one @lla-dane added for adding signed peer record transfer support in kad-dht and pub-sub/gossipsub. Please consult with @lla-dane in detail as you make improvements.

@Antrikshgwal
Copy link
Author

@lla-dane

@lla-dane
Copy link
Contributor

lla-dane commented Sep 1, 2025

@Antrikshgwal: Fixed the linter issues in this commit: lla-dane@bcb58ba

@seetadev
Copy link
Contributor

seetadev commented Sep 1, 2025

@Antrikshgwal : Thank you for the improvements.

Also, thank you @lla-dane for fixing the lint issue.

@lla-dane : Re-ran the CI/CD pipeline. Looks like your changes are yet to be merged.

Looking forward to arriving at a good conclusion on the issue.

@Antrikshgwal : Please add the test suite like the one @lla-dane added for adding signed peer record transfer support in kad-dht and pub-sub/gossipsub. Please consult with @lla-dane in detail as you make improvements. Also, wish to ask if you have added newsfragment file.

@lla-dane
Copy link
Contributor

lla-dane commented Sep 11, 2025

Like both them are just envelope wrapped peer-records only right? When the client contacts the relay, it uses HopMessages and sends its signed peer record in the signedRecord parameter. And when there is communication between relay and destination-peer, the sender includes their signed-record in the senderRecord component. Right ?

I would suggest, at both the places (HopMessage and StopMessage schema), stick to a single name only for the signed-peer-records, preferably senderRecord, for less confusion.

@Antrikshgwal
Copy link
Author

Like both them are just envelope wrapped peer-records only right? When the client contacts the relay, it uses HopMessages and sends its signed peer record in the signedRecord parameter. And when there is communication between relay and destination-peer, the sender includes their signed-record in the senderRecord component. Right ?

Yeah, makes sense.

@Antrikshgwal
Copy link
Author

Like both them are just envelope wrapped peer-records only right? When the client contacts the relay, it uses HopMessages and sends its signed peer record in the signedRecord parameter. And when there is communication between relay and destination-peer, the sender includes their signed-record in the senderRecord component. Right ?

I would suggest, at both the places (HopMessage and StopMessage schema), stick to a single name only for the signed-peer-records, preferably senderRecord, for less confusion.

Will do. Thanks for the suggestion.

@Antrikshgwal
Copy link
Author

In this function:

async def _send_status(
    self,
    stream: ReadWriteCloser,
    code: int,
    message: str,
    envelope: Envelope | None = None,
) -> None:
    """Send a status message."""
    try:
		..
		..
            status_msg = HopMessage(
                type=HopMessage.STATUS,
                status=pb_status,
                signedRecord=envelope,
            )
		..
		..

Envelope parameter is optional, but at all the places where this function is called like this:

await self._send_status(
    stream,
    StatusCode.MALFORMED_MESSAGE,
    f"Invalid message type: {hop_msg.type}",
)

Envelope is not passed, I would suggest passing the envelope param at all the places, since a HopMessage is being sent out via this function.

Added the Envelope parameter in _send_status and _stop_send_status calls with appropiate envelopes

@Antrikshgwal
Copy link
Author

@Antrikshgwal: There are many places where HopMessages are sent out, but the senderRecord from the sender's side is not included, make sure to include them at all the places, and handle them properly at the receiving end.

added SPR in every HOP and STOP message.

@Antrikshgwal
Copy link
Author

@Antrikshgwal: There are many places where HopMessages are sent out, but the senderRecord from the sender's side is not included, make sure to include them at all the places, and handle them properly at the receiving end.

Also modified the protobuf schema. Now the record containing field in both HOP and STOP schemas is named senderRecord

@Antrikshgwal
Copy link
Author

@lla-dane Please let me know if further changes has to be made.

@seetadev
Copy link
Contributor

@Antrikshgwal : Great, thank you so much for addressing the feedback by @lla-dane. Appreciate it.

Please add a test suite and newsfragment too.

@seetadev
Copy link
Contributor

seetadev commented Sep 21, 2025

@lla-dane and @Antrikshgwal : Please try and complete this important PR before the maintainer's call this week. We plan to do a py-libp2p release once websockets support is added and the corresponding PR merged. Would like your opened PRs to be reviewed, merged and landed in the new release too.

@Antrikshgwal
Copy link
Author

@lla-dane and @Antrikshgwal : Please try and complete this important PR before the maintainer's call this week. We plan to do a py-libp2p release once websockets support is added and the corresponding PR merged. Would like your opened PRs to be reviewed, merged and landed in the new release too.

Sure, I’m waiting on @lla-dane ’s review before proceeding

@lla-dane
Copy link
Contributor

@seetadev @Antrikshgwal: Apologize for the delay on this PR, was engaged in a few other things, will surely post a feedback on this PR today.

Comment on lines 894 to 895
peer=client_host.get_id().to_bytes(),
senderRecord=corrupted_env.to_bytes(), # Invalid SPR
Copy link
Contributor

Choose a reason for hiding this comment

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

Change this line:

senderRecord=corrupted_env.to_bytes()

to this:

senderRecord=corrupted_env.marshal_envelope()

@Antrikshgwal, make this change the failing test will be resolved.

Copy link
Author

Choose a reason for hiding this comment

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

Changed corrupted_env.to_bytes() to corrupted_env.marshal_envelope().

@lla-dane
Copy link
Contributor

@seetadev @Antrikshgwal: Looked through the repo, it is nicely done in all the message transfer interfaces. Make a change comment on one of the failing tests, after that the CI runs will pass.

It would be great if @pacrob could also give it a quick look to see if anything is missing.

@seetadev
Copy link
Contributor

@Antrikshgwal : Wish to share that I re-ran the CI/CD pipeline and there were multiple issues.

Wish if you could add @Winter-Soren as a collaborator. He will help you resolve the CI/CD issues that are happening and are yet to be resolved.

@lla-dane will definitely share feedback on the PR once CI/CD issues have been resolved.

@lla-dane : @pacrob , @acul71 & I will be spending time this week on the upcoming release of Py-libp2p. Wish to share that we have tested the py-multiaddr release in multiple PRs in py-libp2p. The new release of both py-cid & py-multiaddr has landed well.

Comment on lines +56 to +57
if not (isinstance(peer_id, ID) and record.peer_id == peer_id):
return False
Copy link
Member

Choose a reason for hiding this comment

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

Here you reject unless peer_id is included, but none of the calls to this function in circuit_v2/protocol.py include an argument for peer_id, causing them all to always return False.

Copy link
Author

Choose a reason for hiding this comment

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

@pacrob Apologies, I added peer id support in the function after using it here and somehow forgot to update these calls. Now the maybe_consume_signed_records calls also send relevant peer ids.

docs/conf.py Outdated
# Prevent autodoc from trying to import module from tests.factories
autodoc_mock_imports = ["tests.factories"]
autodoc_mock_imports = ["tests.factories"
"libp2p.relay.circuit_v2.lib"
Copy link
Member

Choose a reason for hiding this comment

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

Why is this needed?

Copy link
Author

Choose a reason for hiding this comment

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

Actually, the Sphinx autodoc was trying to import libp2p.relay.circuit_v2.lib during the docs build, but the module isn’t present in the repo. I have added a comment stating the issue.

if resp.HasField("senderRecord"):
if not maybe_consume_signed_record(resp, self.host, relay_peer_id):
logger.error(
"Received an invalid-signed-record, dropping the stream"
Copy link
Member

Choose a reason for hiding this comment

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

Nit - writing it as "invalid-signed-record" implies there is a variable of that name. Clarify that it's an "invalid senderRecord" or "invalid Signed Peer Record" or something similar. 3 instances in this file and one in discovery.py.

Copy link
Author

@Antrikshgwal Antrikshgwal Sep 23, 2025

Choose a reason for hiding this comment

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

@pacrob Changed invalid-signed-record to invalid senderRecord.

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.

5 participants