Skip to content

Commit

Permalink
feat(core): support multiple indy ledgers (#474)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Oct 20, 2021
1 parent ed9db11 commit 47149bc
Show file tree
Hide file tree
Showing 31 changed files with 1,221 additions and 266 deletions.
26 changes: 19 additions & 7 deletions docs/getting-started/0-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,14 @@ const BCOVRIN_TEST_GENESIS = `{"reqSignature":{},"txn":{"data":{"data":{"alias":
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"138.197.138.255","client_port":9708,"node_ip":"138.197.138.255","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}`

const agentConfig = {
poolName: 'BCovrin Test'
genesisTransactions: BCOVRIN_TEST_GENESIS
indyLedgers: [
{
id: 'BCovrin Test',
genesisTransactions: BCOVRIN_TEST_GENESIS,
isProduction: false,
},
],
}


```

Note: You do not need the genesis file if you are creating a connection between your Agent and another Agent for exchanging simple messages.
Expand Down Expand Up @@ -127,8 +130,13 @@ const agentConfig: InitConfig = {
autoAcceptConnections: true,
autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
autoAcceptProofs: AutoAcceptProof.ContentApproved,
poolName: 'BCovrin Test',
genesisTransactions: BCOVRIN_TEST_GENESIS,
indyLedgers: [
{
id: 'BCovrin Test',
genesisTransactions: BCOVRIN_TEST_GENESIS,
isProduction: false,
},
],
logger: new ConsoleLogger(LogLevel.debug),
}

Expand Down Expand Up @@ -220,7 +228,11 @@ The agent currently supports the following configuration options. Fields marked
- `publicDidSeed`: The seed to use for initializing the public did of the agent. This does not register the DID on the ledger.
- `genesisPath`: The path to the genesis file to use for connecting to an Indy ledger.
- `genesisTransactions`: String of genesis transactions to use for connecting to an Indy ledger.
- `poolName`: The name of the pool to use for the specified `genesisPath`. Default `default-pool`
- `indyLedgers`: The indy ledgers to connect to. This is an array of objects with the following properties. Either `genesisPath` or `genesisTransactions` must be set, but not both. See [4. Ledger](./4-ledger.md) for more information.
- `id`\*: The id (or name) of the ledger, also used as the pool name
- `isProduction`\*: Whether the ledger is a production ledger. This is used by the pool selector algorithm to know which ledger to use for certain interactions (i.e. prefer production ledgers over non-production ledgers)
- `genesisPath`: The path to the genesis file to use for connecting to an Indy ledger.
- `genesisTransactions`: String of genesis transactions to use for connecting to an Indy ledger.
- `logger`: The logger instance to use. Must implement `Logger` interface
- `didCommMimeType`: The mime-type to use for sending and receiving messages.
- `DidCommMimeType.V0`: "application/ssi-agent-wire"
Expand Down
88 changes: 41 additions & 47 deletions docs/getting-started/4-ledger.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,43 @@
# Ledger

> TODO
>
> - Context, some explanations
> - Why use public networks or local development networks
> - Whats the difference between the different public networks
- [Using Public Test Networks](#using-public-test-networks)
- [Using Your Own Development Network](#using-your-own-development-network)
- [VON Network](#von-network)
- [Preparing for Credential Issuance](#preparing-for-credential-issuance)
- [DID](#did)
- [Schema](#schema)
- [Credential Definition](#credential-definition)

## Using Public Test Networks

> TODO
>
> - For development you can use one of the public test networks
> - Sovrin BuilderNet: https://selfserve.sovrin.org/
> - BCGov Test Network: http://dev.greenlight.bcovrin.vonx.io/
> - Indicio Test Network: https://selfserve.indiciotech.io/
## Using Your Own Development Network

### VON Network

> TODO:
>
> - [VON Network](https://github.com/bcgov/von-network) install steps
> - con: only works if the framework runs in a docker container
## Preparing for Credential Issuance

> TODO
### DID

> TODO
### Schema

> TODO
### Credential Definition

> TODO
- [Configuration](#configuration)
- [Pool Selector Algorithm](#pool-selector-algorithm)

## Configuration

Ledgers to be used by the agent can be specified in the agent configuration using the `indyLedgers` config. Only indy ledgers are supported at the moment. The `indyLedgers` property is an array of objects with the following properties. Either `genesisPath` or `genesisTransactions` must be set, but not both:

- `id`\*: The id (or name) of the ledger, also used as the pool name
- `isProduction`\*: Whether the ledger is a production ledger. This is used by the pool selector algorithm to know which ledger to use for certain interactions (i.e. prefer production ledgers over non-production ledgers)
- `genesisPath`: The path to the genesis file to use for connecting to an Indy ledger.
- `genesisTransactions`: String of genesis transactions to use for connecting to an Indy ledger.

```ts
const agentConfig: InitConfig = {
indyLedgers: [
{
id: 'sovrin-main',
isProduction: true,
genesisPath: './genesis/sovrin-main.txn',
},
{
id: 'bcovrin-test',
isProduction: false,
genesisTransactions: 'XXXX',
},
],
}
```

### Pool Selector Algorithm

The pool selector algorithm automatically determines which pool (network/ledger) to use for a certain operation. For **write operations**, the first pool is always used. For **read operations** the process is a bit more complicated and mostly based on [this](https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA) google doc.

The order of the ledgers in the `indyLedgers` configuration object matters. The pool selection algorithm works as follows:

- When the DID is anchored on only one of the configured ledgers, use that ledger
- When the DID is anchored on multiple of the configured ledgers
- Use the first ledger (order of `indyLedgers`) with a self certified DID
- If none of the DIDs are self certified use the first production ledger (order of `indyLedgers` with `isProduction: true`)
- If none of the DIDs are self certified or come from production ledgers, use the first non production ledger (order of `indyLedgers` with `isProduction: false`)
- When the DID is not anchored on any of the configured ledgers, a `LedgerNotFoundError` will be thrown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"eslint-plugin-prettier": "^3.4.0",
"express": "^4.17.1",
"husky": "^7.0.1",
"indy-sdk": "^1.16.0-dev-1636",
"jest": "^27.0.4",
"lerna": "^4.0.0",
"prettier": "^2.3.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@multiformats/base-x": "^4.0.1",
"@types/indy-sdk": "^1.16.6",
"@types/node-fetch": "^2.5.10",
"@types/ws": "^7.4.4",
Expand All @@ -33,6 +34,7 @@
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"js-sha256": "^0.9.0",
"lru_map": "^0.4.1",
"luxon": "^1.27.0",
"make-error": "^1.3.6",
"multibase": "^4.0.4",
Expand Down
12 changes: 2 additions & 10 deletions packages/core/src/agent/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,8 @@ export class AgentConfig {
return this.initConfig.publicDidSeed
}

public get poolName() {
return this.initConfig.poolName ?? 'default-pool'
}

public get genesisPath() {
return this.initConfig.genesisPath
}

public get genesisTransactions() {
return this.initConfig.genesisTransactions
public get indyLedgers() {
return this.initConfig.indyLedgers ?? []
}

public get walletConfig() {
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/cache/CacheRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { RecordTags, TagsBase } from '../storage/BaseRecord'

import { BaseRecord } from '../storage/BaseRecord'
import { uuid } from '../utils/uuid'

export type CustomCacheTags = TagsBase
export type DefaultCacheTags = TagsBase

export type CacheTags = RecordTags<CacheRecord>

export interface CacheStorageProps {
id?: string
createdAt?: Date
tags?: CustomCacheTags

entries: Array<{ key: string; value: unknown }>
}

export class CacheRecord extends BaseRecord<DefaultCacheTags, CustomCacheTags> {
public entries!: Array<{ key: string; value: unknown }>

public static readonly type = 'CacheRecord'
public readonly type = CacheRecord.type

public constructor(props: CacheStorageProps) {
super()

if (props) {
this.id = props.id ?? uuid()
this.createdAt = props.createdAt ?? new Date()
this.entries = props.entries
this._tags = props.tags ?? {}
}
}

public getTags() {
return {
...this._tags,
}
}
}
14 changes: 14 additions & 0 deletions packages/core/src/cache/CacheRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { inject, scoped, Lifecycle } from 'tsyringe'

import { InjectionSymbols } from '../constants'
import { Repository } from '../storage/Repository'
import { StorageService } from '../storage/StorageService'

import { CacheRecord } from './CacheRecord'

@scoped(Lifecycle.ContainerScoped)
export class CacheRepository extends Repository<CacheRecord> {
public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService<CacheRecord>) {
super(CacheRecord, storageService)
}
}
73 changes: 73 additions & 0 deletions packages/core/src/cache/PersistedLruCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { CacheRepository } from './CacheRepository'

import { LRUMap } from 'lru_map'

import { CacheRecord } from './CacheRecord'

export class PersistedLruCache<CacheValue> {
private cacheId: string
private limit: number
private _cache?: LRUMap<string, CacheValue>
private cacheRepository: CacheRepository

public constructor(cacheId: string, limit: number, cacheRepository: CacheRepository) {
this.cacheId = cacheId
this.limit = limit
this.cacheRepository = cacheRepository
}

public async get(key: string) {
const cache = await this.getCache()

return cache.get(key)
}

public async set(key: string, value: CacheValue) {
const cache = await this.getCache()

cache.set(key, value)
await this.persistCache()
}

private async getCache() {
if (!this._cache) {
const cacheRecord = await this.fetchCacheRecord()
this._cache = this.lruFromRecord(cacheRecord)
}

return this._cache
}

private lruFromRecord(cacheRecord: CacheRecord) {
return new LRUMap<string, CacheValue>(
this.limit,
cacheRecord.entries.map((e) => [e.key, e.value as CacheValue])
)
}

private async fetchCacheRecord() {
let cacheRecord = await this.cacheRepository.findById(this.cacheId)

if (!cacheRecord) {
cacheRecord = new CacheRecord({
id: this.cacheId,
entries: [],
})

await this.cacheRepository.save(cacheRecord)
}

return cacheRecord
}

private async persistCache() {
const cache = await this.getCache()

await this.cacheRepository.update(
new CacheRecord({
entries: cache.toJSON(),
id: this.cacheId,
})
)
}
}
71 changes: 71 additions & 0 deletions packages/core/src/cache/__tests__/PersistedLruCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { mockFunction } from '../../../tests/helpers'
import { CacheRecord } from '../CacheRecord'
import { CacheRepository } from '../CacheRepository'
import { PersistedLruCache } from '../PersistedLruCache'

jest.mock('../CacheRepository')
const CacheRepositoryMock = CacheRepository as jest.Mock<CacheRepository>

describe('PersistedLruCache', () => {
let cacheRepository: CacheRepository
let cache: PersistedLruCache<string>

beforeEach(() => {
cacheRepository = new CacheRepositoryMock()
mockFunction(cacheRepository.findById).mockResolvedValue(null)

cache = new PersistedLruCache('cacheId', 2, cacheRepository)
})

it('should return the value from the persisted record', async () => {
const findMock = mockFunction(cacheRepository.findById).mockResolvedValue(
new CacheRecord({
id: 'cacheId',
entries: [
{
key: 'test',
value: 'somevalue',
},
],
})
)

expect(await cache.get('doesnotexist')).toBeUndefined()
expect(await cache.get('test')).toBe('somevalue')
expect(findMock).toHaveBeenCalledWith('cacheId')
})

it('should set the value in the persisted record', async () => {
const updateMock = mockFunction(cacheRepository.update).mockResolvedValue()

await cache.set('test', 'somevalue')
const [[cacheRecord]] = updateMock.mock.calls

expect(cacheRecord.entries.length).toBe(1)
expect(cacheRecord.entries[0].key).toBe('test')
expect(cacheRecord.entries[0].value).toBe('somevalue')

expect(await cache.get('test')).toBe('somevalue')
})

it('should remove least recently used entries if entries are added that exceed the limit', async () => {
// Set first value in cache, resolves fine
await cache.set('one', 'valueone')
expect(await cache.get('one')).toBe('valueone')

// Set two more entries in the cache. Third item
// exceeds limit, so first item gets removed
await cache.set('two', 'valuetwo')
await cache.set('three', 'valuethree')
expect(await cache.get('one')).toBeUndefined()
expect(await cache.get('two')).toBe('valuetwo')
expect(await cache.get('three')).toBe('valuethree')

// Get two from the cache, meaning three will be removed first now
// because it is not recently used
await cache.get('two')
await cache.set('four', 'valuefour')
expect(await cache.get('three')).toBeUndefined()
expect(await cache.get('two')).toBe('valuetwo')
})
})
3 changes: 3 additions & 0 deletions packages/core/src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './PersistedLruCache'
export * from './CacheRecord'
export * from './CacheRepository'
Loading

0 comments on commit 47149bc

Please sign in to comment.