Skip to content

feat: ETHR multiledger suport#174

Merged
Toktar merged 3 commits into
hyperledger-indy:mainfrom
flaviocgarcia78:multiledger
May 29, 2026
Merged

feat: ETHR multiledger suport#174
Toktar merged 3 commits into
hyperledger-indy:mainfrom
flaviocgarcia78:multiledger

Conversation

@flaviocgarcia78
Copy link
Copy Markdown
Contributor

Summary

This PR introduces support for multi-ledger configuration when resolving the did:ethr method in Indy-Besu.

With this change, the resolver can interact with multiple EVM-compatible ledgers instead of relying on a single configured network. This enables broader interoperability and flexibility when working with decentralized identity deployments across different Ethereum-based networks.

Motivation

Currently, the did:ethr integration assumes a single ledger configuration. However, real-world deployments often require interacting with multiple networks (e.g., mainnet, testnets, or permissioned EVM chains).

Supporting multiple ledgers allows implementers to:

  • Resolve DIDs across different Ethereum-based networks
  • Improve interoperability between deployments
  • Support environments where identity infrastructure spans multiple chains

Changes

  • Added multi-ledger configuration support for the did:ethr method
  • Updated resolver logic to select the appropriate ledger
  • Added configuration structure for defining multiple EVM networks
  • Refactored related components to support ledger selection

Testing

The following tests were performed:

  • Verified DID resolution against multiple configured ledgers
  • Ensured backward compatibility with single-ledger configurations
  • Confirmed correct behavior for existing DID resolution flows

Compatibility

This change is backward compatible. Existing configurations using a single ledger will continue to function without modification.

Related Work

Related to ongoing work around multi-network interoperability in DID resolution.

Contributor

Contribution by external collaborator.

@flaviocgarcia78
Copy link
Copy Markdown
Contributor Author

Hi @Artemkaaas and @Toktar,

I've just opened this PR adding multi-ledger support for the did:ethr method.
Could you please take a look and let me know if the approach aligns with the project expectations?

If there are any adjustments needed (design, configuration format, tests, etc.), I'm happy to update the implementation.

Thanks!

@Toktar
Copy link
Copy Markdown
Contributor

Toktar commented Apr 21, 2026

Hi @flaviocgarcia78, thank you for this contribution! It is a valuable addition. Appreciate the tests and the comprehensive demo script as well.

Before we can move forward with merging, I'd like to flag a few architectural and code quality concerns:

Architectural

No client caching. get_ledger_for_identifier() and ping_all() instantiate a new LedgerClient on every call. This is expensive (HTTP connections, potential TLS handshake). Looking at Arc I guess, caching was planned, could you add a client cache?

Fragile DID parsing. extract_network() takes parts[2] without validating the DID method. It makes sense to check if it's did:ethr and if there's a namespaces

VdrError::ClientInvalidResponse is used for configuration errors (missing network, wrong mode). These are semantically different. A dedicated variant like ConfigError or NetworkNotFound would be clearer.

Scope

This PR mixes several unrelated changes. I'd recommend splitting this into focused PRs, it's very hard to review as-is.

Internal infrastructure leak. The .gitlab-ci.yml references gitcorporativo.serpro (internal GitLab) with hardcoded project IDs and SSL certificate workarounds. This shouldn't land in a Hyperledger open-source repo.

Committed artifacts. The file vdr/uniffi/--out-dir is a CLI typo artifact, and the 5090-line Swift file should be generated in CI rather than committed.

What is the reason of monitoring stack removal from docker-compose? Could you please return it?

LedgerMode, LedgerResult, and LedgerClientConfig lack Debug, this makes debugging difficult.

And could you please fix linter errors and DCO?

Thank you!

@@ -0,0 +1,285 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mutex is imported but unused. Could you implement client caching please?

Comment thread .gitlab-ci.yml Outdated
RUSTUP_HOME: "/Users/serpro/.rustup"
PATH: "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/Users/serpro/.cargo/bin:$PATH"
before_script:
- rustup show # debug: garante que o runner enxerga o mesmo rust
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please use English language for comments?

Comment thread .gitlab-ci.yml Outdated
needs: ["generate-bindings"]
script:
- cd vdr/uniffi
# Substitui '/' por '-' para nomes de branch seguros
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please use English language for comments?

Comment thread .gitlab-ci.yml Outdated
- PACKAGE_VERSION=${CI_COMMIT_REF_NAME//\//-}
- TAR_NAME=libs-and-bindings-${PACKAGE_VERSION}.tar.gz
- tar czf $TAR_NAME target out $(test -d wrappers && echo wrappers)
# Upload direto no script garante que só tenta se o tar existir
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please use English language for comments?

Comment thread .gitlab-ci.yml Outdated
build_macos:
stage: build
tags:
- macos # garante que vai rodar só no runner com essa tag
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please use English language for comments?

Comment thread vdr/src/client/ledger_router.rs Outdated
Comment on lines +83 to +84
default_network: default_network.unwrap_or("default").to_string(),
mode: mode.unwrap_or(LedgerMode::ConfigOnly),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The magic strings "default" and the implicit ConfigOnly default appear in several places.

rpc_node: rpc_node.to_string(),
contract_configs: contract_configs.to_vec(),
network: network.map(|s| s.to_string()),
quorum_config: quorum_config.cloned(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

quorum_config from LedgerClientConfig is available but not passed to LedgerClient::new() — None is used instead. This looks like an oversight or I missed something

/// The extracted network name as String
pub fn extract_network(identifier: &str) -> VdrResult<String> {
let parts: Vec<&str> = identifier.split(':').collect();
if parts.len() < 3 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

DID can contain more then 3 prefixes.

network1 = 'ledger1'
network2 = 'ledger2'
network3 = 'ledger3'
project_root = f"{os.getcwd()}/../../.."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can be an issue if the script is called from a different working directory. os.path.dirname(os.path.abspath(file)) with relative navigation would be more robust.

print(' Revocation Status List:' + status_list.to_string())

if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(demo())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

deprecated since Python 3.10. asyncio.run(demo()) is the modern replacement.

Signed-off-by: Flavio Caetano Garcia <flavio.garcia@serpro.gov.br>
config.network.as_deref(),
None,
)?;
Ok(LedgerResult::Client(client))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should use HashMap<String, Arc<LedgerClient>> as a cache because now it does parsing of all contract ABIs and opens a connection on every call of get_ledger_for_identifier, which is called per operation.

pub network: Option<String>,
/// Optional quorum configuration, if applicable.
pub quorum_config: Option<QuorumConfig>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should it also contain LedgerMode as a property? I see LedgerMode::LedgerClient is hardcoded below, while the actual definition provides two options.

Copy link
Copy Markdown
Contributor Author

@flaviocgarcia78 flaviocgarcia78 Apr 29, 2026

Choose a reason for hiding this comment

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

Good point.
At the moment, I kept LedgerMode hardcoded to LedgerClient in the FFI layer to keep the API simpler and avoid introducing additional enum mappings in UniFFI.
Since the main goal of this PR is to introduce multi-ledger routing and caching, I suggest we keep this behavior for now and handle exposing LedgerMode as a follow-up improvement (it will require proper UniFFI enum support and bindings update).

configs: HashMap<String, LedgerClientConfig>,
default_network: String,
mode: LedgerMode,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like LedgerRouter is not integrated into methods like resolve_did, resolve_schema, and so on. They still accept client instances.

I think we should define a common trait for LedgerRouter and LedgerClient, so all existing resolution methods can accept any of the client implementations. At the FFI layer, we can provide duplicate methods if we face an issue.

LedgerConfiguration(chain_id=config["chainId"], node_address=config["nodeAddress"], contract_configs=contract_configs, network=network1, quorum_config=None),
LedgerConfiguration(chain_id=config["chainId"], node_address=config["nodeAddress"], contract_configs=contract_configs, network=network2, quorum_config=None),
LedgerConfiguration(chain_id=config["chainId"], node_address=config["nodeAddress"], contract_configs=contract_configs, network=network3, quorum_config=None)
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I do not really think that this demo actually verifies the multi-ledger behaviour correctness. All three configurations use identical chainId and nodeAddress. This is the same single ledger registered under three names.

Copy link
Copy Markdown
Contributor Author

@flaviocgarcia78 flaviocgarcia78 Apr 29, 2026

Choose a reason for hiding this comment

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

You're right — the current demo validates routing behavior rather than real multi-ledger connectivity.

Each network is registered under a distinct name, so the router logic (network resolution and client selection) is exercised, even though the underlying node is the same.

I will clarify this in the demo comments to avoid confusion, and we can extend it later with distinct endpoints if needed.

receipt = await client.get_receipt(txn_hash)
print(' Transaction receipt: ' + receipt)

print("3. Resolve DID Document:" +network1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Label says network1 but client resolves did defined via network2

Comment thread .gitlab-ci.yml Outdated
needs: ["generate-bindings"]
variables:
TWINE_REPOSITORY_URL: "https://gitcorporativo.serpro/api/v4/projects/21675/packages/pypi"
before_script:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems to refer to a private corporate GitLab instance (gitcorporativo.serpro) with a hardcoded project ID (21675). It has no relation to this project. This job needs to be deleted.

@Artemkaaas
Copy link
Copy Markdown
Contributor

The change added in the PR is definitely valuable - named-network routing for multi-ledger support fills a real gap.
Apart from several clean-up comments, I have only the two main technical comments before getting this PR ready to merge:

  1. Adding client caching
  2. Router integration into resolution methods

Signed-off-by: Flavio Caetano Garcia <flavio.garcia@serpro.gov.br>
@flaviocgarcia78
Copy link
Copy Markdown
Contributor Author

@Artemkaaas

**Artemkaaas **

First of all, thank you for the opportunity to contribute to the indy-besu project — it’s a great initiative and very interesting work.

Special thanks to @Artemkaaas and @Toktar for the thorough review and valuable feedback. It really helped to refine the approach and improve the implementation.

I’ve pushed updates addressing the main points discussed. Happy to iterate further if needed.
Addressing the main review point: Router integration into resolution methods

I took a deeper look into integrating the router directly into resolution flows (e.g. resolve_did, resolve_schema), and I agree this is a valuable direction.

However, I believe it is safer to keep the current approach for now, where the caller explicitly resolves the LedgerClient via the router:

client = router.get_ledger_for_identifier(identifier)

and then passes it to the corresponding operation.

The main reason is that not all flows have a reliable identifier to derive the target ledger. For example, in write operations such as schema creation:

SchemaRegistry.build_create_schema_endorsing_data(client, schema)

the schema may not yet have a fully qualified identifier (or network information), making automatic routing ambiguous or error-prone.

Because of that, introducing router-based delegation at this stage could lead to implicit behavior and edge cases that are harder to reason about.

For this PR, I focused on:

  • introducing named-network routing
  • adding client caching to avoid repeated instantiation
  • keeping routing explicit and predictable

I suggest handling router-based delegation (especially for read/resolve methods) as a follow-up improvement.

Happy to extend in that direction in a next PR if that sounds good 👍

— Flavio Garcia

@Artemkaaas
Copy link
Copy Markdown
Contributor

@flaviocgarcia78
Thank you for processing comments.

#174 (comment)
Sounds reasonable. Explicit routing at the call site is safer for now. A follow-up PR is a fair call.

Could you fix markdown linter issues and remove one unprocessed Portuguese comment - https://github.com/hyperledger-indy/indy-besu/pull/174/changes#diff-3739dfc2ecb3a79bfb86811a09028efd7279f2743ecec65bf4917cb2fa6ded08R78
And after that, we will merge the PR.

Signed-off-by: Flavio Caetano Garcia <flavio.garcia@serpro.gov.br>
@flaviocgarcia78
Copy link
Copy Markdown
Contributor Author

@flaviocgarcia78 Thank you for processing comments.

#174 (comment) Sounds reasonable. Explicit routing at the call site is safer for now. A follow-up PR is a fair call.

Could you fix markdown linter issues and remove one unprocessed Portuguese comment - https://github.com/hyperledger-indy/indy-besu/pull/174/changes#diff-3739dfc2ecb3a79bfb86811a09028efd7279f2743ecec65bf4917cb2fa6ded08R78 And after that, we will merge the PR.

@Artemkaaas,
Thank you for the review and guidance.

I have addressed the requested adjustments:

  • fixed the markdown linter issue by replacing the root-relative link with a relative path
  • removed the remaining Portuguese comment and replaced it with an English comment

The branch was updated with the latest changes.

@Toktar Toktar merged commit 543ec57 into hyperledger-indy:main May 29, 2026
7 checks passed
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.

4 participants