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: Support Selectable Write Ledger #2339

Merged
merged 10 commits into from
Aug 2, 2023
55 changes: 48 additions & 7 deletions Multiledger.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Multi-ledger in ACA-Py <!-- omit in toc -->

Ability to use multiple Indy ledgers (both IndySdk and IndyVdr) for resolving a `DID` by the ACA-Py agent. For read requests, checking of multiple ledgers in parallel is done dynamically according to logic detailed in [Read Requests Ledger Selection](#read-requests). For write requests, dynamic allocation of `write_ledger` is not supported. Write ledger can be assigned using `is_write` in the [configuration](#config-properties) or using any of the `--genesis-url`, `--genesis-file`, and `--genesis-transactions` startup (ACA-Py) arguments. If no write ledger is assigned then a `ConfigError` is raised.
Ability to use multiple Indy ledgers (both IndySdk and IndyVdr) for resolving a `DID` by the ACA-Py agent. For read requests, checking of multiple ledgers in parallel is done dynamically according to logic detailed in [Read Requests Ledger Selection](#read-requests). For write requests, dynamic allocation of `write_ledger` is supported. Configurable write ledgers can be assigned using `is_write` in the [configuration](#config-properties) or using any of the `--genesis-url`, `--genesis-file`, and `--genesis-transactions` startup (ACA-Py) arguments. If no write ledger is assigned then a `ConfigError` is raised.

More background information including problem statement, design (algorithm) and more can be found [here](https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA).

Expand Down Expand Up @@ -30,12 +30,45 @@ If `--genesis-transactions-list` is specified, then `--genesis-url, --genesis-fi
- id: localVON
is_production: false
genesis_url: 'http://host.docker.internal:9000/genesis'
- id: bcorvinTest
- id: bcovrinTest
is_production: true
is_write: true
genesis_url: 'http://test.bcovrin.vonx.io/genesis'
```

```
- id: localVON
is_production: false
genesis_url: 'http://host.docker.internal:9000/genesis'
- id: bcovrinTest
is_production: true
is_write: true
genesis_url: 'http://test.bcovrin.vonx.io/genesis'
endorser_did: '9QPa6tHvBHttLg6U4xvviv'
endorser_alias: 'endorser_test'
- id: greenlightDev
is_production: true
is_write: true
genesis_url: 'http://dev.greenlight.bcovrin.vonx.io/genesis'
```

Note: `is_write` property means that the ledger is write configurable. With reference to the above config example, both `bcovrinTest` and `greenlightDev` ledgers are write configurable. By default, on startup `bcovrinTest` will be the write ledger as it is the topmost write configurable production ledger, [more details](#write-requests) regarding the selection rule. Using `PUT /ledger/{ledger_id}/set-write-ledger` endpoint, either `greenlightDev` and `bcovrinTest` can be set as the write ledger.

```
- id: localVON
is_production: false
is_write: true
genesis_url: 'http://host.docker.internal:9000/genesis'
- id: bcovrinTest
is_production: true
genesis_url: 'http://test.bcovrin.vonx.io/genesis'
- id: greenlightDev
is_production: true
genesis_url: 'http://dev.greenlight.bcovrin.vonx.io/genesis'
```

Note: For instance with regards to example config above, `localVON` will be the write ledger, as there are no production ledgers which are configurable it will choose the topmost write configurable non production ledger.

### Config properties
For each ledger, the required properties are as following:

Expand All @@ -52,17 +85,25 @@ Optional properties:
- `pool_name`: name of the indy pool to be opened
- `keepalive`: how many seconds to keep the ledger open
- `socks_proxy`
- `is_write`: Whether the ledger is the write ledger. Only one ledger can be assigned, otherwise a `ConfigError` is raised.
- `is_write`: Whether this ledger is writable. It requires atleast one write ledger specified. Multiple write ledgers can be specified in config.
- `endorser_did`: Endorser public DID registered on the ledger, needed for supporting Endorser protocol at multi-ledger level.
- `endorser_alias`: Endorser alias for this ledger, needed for supporting Endorser protocol at multi-ledger level.

Note: Both `endorser_did` and `endorser_alias` are part of the endorser info. Whenever a write ledger is selected using `PUT /ledger/{ledger_id}/set-write-ledger`, the endorser info associated with that ledger in the config updates the `endorser.endorser_public_did` and `endorser.endorser_alias` profile setting respectively.


## Multi-ledger Admin API

Multi-ledger related actions are grouped under the `ledger` topic in the SwaggerUI or under `/ledger/multiple` path.
Multi-ledger related actions are grouped under the `ledger` topic in the SwaggerUI.

- `/ledger/multiple/config`:
- GET `/ledger/config`:
Returns the multiple ledger configuration currently in use
- `/ledger/multiple/get-write-ledger`:
- GET `/ledger/get-write-ledger`:
Returns the current active/set `write_ledger's` `ledger_id`
- GET `/ledger/get-write-ledgers`:
Returns list of available `write_ledger's` `ledger_id`
- PUT `/ledger/{ledger_id}/set-write-ledger`:
Set active `write_ledger's` `ledger_id`

## Ledger Selection

Expand Down Expand Up @@ -103,7 +144,7 @@ If multiple ledgers are configured then `IndyLedgerRequestsExecutor` service ext

### Write Requests

On startup, the first configured applicable ledger is assigned as the `write_ledger` [`BaseLedger`], the selection is dependant on the order (top-down) and whether it is `production` or `non_production`. For instance, considering this [example configuration](#example-config-file), ledger `bcorvinTest` will be set as `write_ledger` as it is the topmost `production` ledger. If no `production` ledgers are included in configuration then the topmost `non_production` ledger is selected.
On startup, the first configured applicable ledger is assigned as the `write_ledger` [`BaseLedger`], the selection is dependant on the order (top-down) and whether it is `production` or `non_production`. For instance, considering this [example configuration](#example-config-file), ledger `bcovrinTest` will be set as `write_ledger` as it is the topmost `production` ledger. If no `production` ledgers are included in configuration then the topmost `non_production` ledger is selected.

## A Special Warning for TAA Acceptance

Expand Down
41 changes: 39 additions & 2 deletions aries_cloudagent/askar/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ..ledger.indy_vdr import IndyVdrLedger, IndyVdrLedgerPool
from ..storage.base import BaseStorage, BaseStorageSearch
from ..storage.vc_holder.base import VCHolder
from ..utils.multi_ledger import get_write_ledger_config_for_profile
from ..wallet.base import BaseWallet
from ..wallet.crypto import validate_seed

Expand Down Expand Up @@ -120,8 +121,44 @@ def bind_providers(self):
ref(self),
),
)

if self.ledger_pool:
if (
self.settings.get("ledger.ledger_config_list")
and len(self.settings.get("ledger.ledger_config_list")) >= 1
):
write_ledger_config = get_write_ledger_config_for_profile(
settings=self.settings
)
cache = self.context.injector.inject_or(BaseCache)
injector.bind_provider(
BaseLedger,
ClassProvider(
IndyVdrLedger,
IndyVdrLedgerPool(
write_ledger_config.get("pool_name")
or write_ledger_config.get("id"),
keepalive=write_ledger_config.get("keepalive"),
cache=cache,
genesis_transactions=write_ledger_config.get(
"genesis_transactions"
),
read_only=write_ledger_config.get("read_only"),
socks_proxy=write_ledger_config.get("socks_proxy"),
),
ref(self),
),
)
self.settings["ledger.write_ledger"] = write_ledger_config.get("id")
if (
"endorser_alias" in write_ledger_config
and "endorser_did" in write_ledger_config
):
self.settings["endorser.endorser_alias"] = write_ledger_config.get(
"endorser_alias"
)
self.settings["endorser.endorser_public_did"] = write_ledger_config.get(
"endorser_did"
)
elif self.ledger_pool:
injector.bind_provider(
BaseLedger, ClassProvider(IndyVdrLedger, self.ledger_pool, ref(self))
)
Expand Down
36 changes: 36 additions & 0 deletions aries_cloudagent/askar/tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ...askar.profile import AskarProfile
from ...config.injection_context import InjectionContext
from ...ledger.base import BaseLedger

from .. import profile as test_module

Expand All @@ -24,6 +25,41 @@ async def test_init_success(open_store):
assert askar_profile.opened == open_store


@pytest.mark.asyncio
async def test_init_multi_ledger(open_store):
context = InjectionContext(
settings={
"ledger.ledger_config_list": [
{
"id": "BCovrinDev",
"is_production": True,
"is_write": True,
"endorser_did": "9QPa6tHvBHttLg6U4xvviv",
"endorser_alias": "endorser_dev",
"genesis_transactions": mock.MagicMock(),
},
{
"id": "SovrinStagingNet",
"is_production": False,
"genesis_transactions": mock.MagicMock(),
},
]
}
)
askar_profile = AskarProfile(
open_store,
context=context,
)

assert askar_profile.opened == open_store
assert askar_profile.settings["endorser.endorser_alias"] == "endorser_dev"
assert (
askar_profile.settings["endorser.endorser_public_did"]
== "9QPa6tHvBHttLg6U4xvviv"
)
assert (askar_profile.inject_or(BaseLedger)).pool_name == "BCovrinDev"


@pytest.mark.asyncio
async def test_remove_success(open_store):
openStore = open_store
Expand Down
38 changes: 18 additions & 20 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ def get_settings(self, args: Namespace) -> dict:
single_configured = False
multi_configured = False
update_pool_name = False
write_ledger_specified = False
if args.genesis_url:
settings["ledger.genesis_url"] = args.genesis_url
single_configured = True
Expand All @@ -930,27 +931,24 @@ def get_settings(self, args: Namespace) -> dict:
txn_config_list = yaml.safe_load(stream)
ledger_config_list = []
for txn_config in txn_config_list:
ledger_config_list.append(txn_config)
if "is_write" in txn_config and txn_config["is_write"]:
if "genesis_url" in txn_config:
settings["ledger.genesis_url"] = txn_config[
"genesis_url"
]
elif "genesis_file" in txn_config:
settings["ledger.genesis_file"] = txn_config[
"genesis_file"
]
elif "genesis_transactions" in txn_config:
settings["ledger.genesis_transactions"] = txn_config[
"genesis_transactions"
]
else:
raise ArgsParseError(
"No genesis information provided for write ledger"
)
if "id" in txn_config:
settings["ledger.pool_name"] = txn_config["id"]
update_pool_name = True
write_ledger_specified = True
if (
"genesis_url" not in txn_config
and "genesis_file" not in txn_config
and "genesis_transactions" not in txn_config
):
raise ArgsParseError(
"No genesis information provided for write ledger"
)
if "id" in txn_config and "pool_name" not in txn_config:
txn_config["pool_name"] = txn_config["id"]
update_pool_name = True
ledger_config_list.append(txn_config)
if not write_ledger_specified:
raise ArgsParseError(
"No write ledger genesis provided in multi-ledger config"
)
settings["ledger.ledger_config_list"] = ledger_config_list
multi_configured = True
if not (single_configured or multi_configured):
Expand Down
39 changes: 20 additions & 19 deletions aries_cloudagent/config/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,26 +89,27 @@ async def load_multiple_genesis_transactions_from_config(settings: Settings):
False if config.get("is_write") is None else config.get("is_write")
)
ledger_id = config.get("id") or str(uuid.uuid4())
if is_write_ledger and write_ledger_set:
raise ConfigError("Only a single ledger can be is_write")
elif is_write_ledger:
if is_write_ledger:
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change going to cause issues with single-tenant agents being able to declare more than one write ledger in the config file? I believe currently the assumption is that there only ever is one "write" ledger at any given time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The meaning of is_write has changed with this PR. For example:

- id: localVON
  is_production: false
  genesis_url: 'http://host.docker.internal:9000/genesis'
- id: bcorvinTest
  is_production: true
  is_write: true
  genesis_url: 'http://test.bcovrin.vonx.io/genesis'
- id: greenlightDev
  is_production: true
  is_write: true
  genesis_url: 'http://dev.greenlight.bcovrin.vonx.io/genesis'

is_write property means that the ledger is write configurable. With reference to the above config example, both bcorvinTest and greenlightDev ledgers are write configurable. By default, on startup bcorvinTest will be the write ledger as it is the topmost write configurable production ledger. Using PUT /ledger/{ledger_id}/set-write-ledger endpoint, either greenlightDev and bcorvinTest can be set as the write ledger.

write_ledger_set = True
ledger_txns_list.append(
{
"id": ledger_id,
"is_production": (
True
if config.get("is_production") is None
else config.get("is_production")
),
"is_write": is_write_ledger,
"genesis_transactions": txns,
"keepalive": int(config.get("keepalive", 5)),
"read_only": bool(config.get("read_only", False)),
"socks_proxy": config.get("socks_proxy"),
"pool_name": config.get("pool_name", ledger_id),
}
)
config_item = {
"id": ledger_id,
"is_production": (
True
if config.get("is_production") is None
else config.get("is_production")
),
"is_write": is_write_ledger,
"genesis_transactions": txns,
"keepalive": int(config.get("keepalive", 5)),
"read_only": bool(config.get("read_only", False)),
"socks_proxy": config.get("socks_proxy"),
"pool_name": config.get("pool_name", ledger_id),
}
if "endorser_alias" in config:
config_item["endorser_alias"] = config.get("endorser_alias")
if "endorser_did" in config:
config_item["endorser_did"] = config.get("endorser_did")
ledger_txns_list.append(config_item)
if not write_ledger_set and not (
settings.get("ledger.genesis_transactions")
or settings.get("ledger.genesis_file")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- id: sovrinMain
is_production: true
is_write: true
- id: sovrinStaging
is_production: true
- id: sovrinTest
is_production: false
32 changes: 32 additions & 0 deletions aries_cloudagent/config/tests/test-ledger-args-no-write.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
- id: sovrinMain
is_production: true
genesis_transactions:
reqSignature: {}
txn:
data:
data:
alias: Node1
blskey: >-
4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba
blskey_pop: >-
RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1
client_ip: 192.168.65.3
client_port: 9702
node_ip: 192.168.65.3
node_port: 9701
services:
- VALIDATOR
dest: Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv
metadata:
from: Th7MpTaRZVRYnPiabds81Y
type: '0'
txnMetadata:
seqNo: 1
txnId: fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62
ver: '1'
- id: sovrinStaging
is_production: true
genesis_file: /home/indy/ledger/sandbox/pool_transactions_genesis
- id: sovrinTest
is_production: false
genesis_url: 'http://localhost:9000/genesis'
2 changes: 2 additions & 0 deletions aries_cloudagent/config/tests/test-ledger-args.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
- id: sovrinMain
is_production: true
is_write: true
genesis_transactions:
reqSignature: {}
txn:
Expand All @@ -26,6 +27,7 @@
ver: '1'
- id: sovrinStaging
is_production: true
is_write: true
genesis_file: /home/indy/ledger/sandbox/pool_transactions_genesis
- id: sovrinTest
is_production: false
Expand Down
Loading