Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion clients/remote/src/ledger/party-allocation-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ const MockTopologyWriteService: jest.MockedClass<any> = jest
generateTransactions: jest.fn<AsyncFn>().mockResolvedValue({
generatedTransactions: [],
}),
generateTopology: jest.fn<AsyncFn>().mockResolvedValue({
partyId: 'party2::mypublickey',
publicKeyFingerprint: 'mypublickey',
topologyTransactions: ['tx1'],
multiHash: 'combinedHash',
}),
addTransactions: jest.fn<AsyncFn>(),
authorizePartyToParticipant: jest.fn<AsyncFn>(),
submitExternalPartyTopology: jest.fn<AsyncFn>(),
allocateExternalParty: jest
.fn<AsyncFn>()
.mockResolvedValue({ partyId: 'party2::mypublickey' }),
}))

// Add static method to the mock class
Expand Down
44 changes: 19 additions & 25 deletions clients/remote/src/ledger/party-allocation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,35 +110,29 @@ export class PartyAllocationService {
): Promise<AllocatedParty> {
const namespace =
TopologyWriteService.createFingerprintFromKey(publicKey)
const partyId = `${hint}::${namespace}`

const transactions = await this.topologyClient
.generateTransactions(publicKey, partyId)
.then((resp) => resp.generatedTransactions)

const txHashes = transactions.map((tx) =>
Buffer.from(tx.transactionHash)
)

const combinedHash = TopologyWriteService.combineHashes(txHashes)

const signature = await signingCallback(combinedHash)

const signedTopologyTxs = transactions.map((transaction) =>
TopologyWriteService.toSignedTopologyTransaction(
txHashes,
transaction.serializedTransaction,
signature,
namespace
)
const transactions = await this.topologyClient.generateTopology(
publicKey,
hint
)

await this.topologyClient.submitExternalPartyTopology(
signedTopologyTxs,
partyId
const signature = await signingCallback(transactions.multiHash)

const res = await this.topologyClient.allocateExternalParty(
transactions.topologyTransactions!.map((transaction) => ({
transaction,
})),
[
{
format: 'SIGNATURE_FORMAT_CONCAT',
signature: signature,
signedBy: namespace,
signingAlgorithmSpec: 'SIGNING_ALGORITHM_SPEC_ED25519',
},
]
)
await this.ledgerClient.grantUserRights(userId, partyId)

return { hint, partyId, namespace }
await this.ledgerClient.grantUserRights(userId, res.partyId)
return { hint, partyId: res.partyId, namespace }
}
}
4 changes: 1 addition & 3 deletions core/ledger-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@
"@canton-network/core-splice-client": "workspace:^",
"@canton-network/core-token-standard": "workspace:^",
"@canton-network/core-types": "workspace:^",
"@grpc/grpc-js": "^1.13.4",
"@grpc/grpc-js": "^1.14.0",
"@protobuf-ts/grpc-transport": "^2.11.1",
"@protobuf-ts/runtime": "^2.11.1",
"@protobuf-ts/runtime-rpc": "^2.11.1",
"bignumber.js": "^9.2.1",
"dayjs": "^1.11.13",
"openapi-fetch": "^0.14.0",
Expand Down
83 changes: 83 additions & 0 deletions core/ledger-client/src/ledger-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ export type GetResponse<Path extends GetEndpoint> = paths[Path] extends {
? Res
: never

// Explicitly use the 3.3 schema here, as there has not been a 3.4 snapshot containing these yet
export type GenerateTransactionResponse =
v3_3.components['schemas']['GenerateExternalPartyTopologyResponse']

export type AllocateExternalPartyResponse =
v3_3.components['schemas']['AllocateExternalPartyResponse']

export type OnboardingTransactions = NonNullable<
v3_3.components['schemas']['AllocateExternalPartyRequest']['onboardingTransactions']
>

export type MultiHashSignatures = NonNullable<
v3_3.components['schemas']['AllocateExternalPartyRequest']['multiHashSignatures']
>

export class LedgerClient {
// privately manage the active connected version and associated client codegen
private readonly clients: Record<SupportedVersions, Client<paths>>
Expand Down Expand Up @@ -192,6 +207,74 @@ export class LedgerClient {
return
}

/** TODO: simplify once 3.4 snapshot contains this endpoint */
public async allocateExternalParty(
synchronizerId: string,
onboardingTransactions: OnboardingTransactions,
multiHashSignatures: MultiHashSignatures
): Promise<AllocateExternalPartyResponse> {
await this.init()

if (this.clientVersion !== '3.3') {
throw new Error(
'allocateExternalParty is only supported on 3.3 clients'
)
}

const client: Client<v3_3.paths> = this.clients['3.3']

const resp = await client.POST('/v2/parties/external/allocate', {
body: {
synchronizer: synchronizerId,
identityProviderId: '',
onboardingTransactions,
multiHashSignatures,
},
})

return this.valueOrError(resp)
}

/** TODO: simplify once 3.4 snapshot contains this endpoint */
public async generateTopology(
synchronizerId: string,
publicKey: string,
partyHint?: PartyId,
localParticipantObservationOnly: boolean = false,
confirmationThreshold: number = 1,
otherConfirmingParticipantUids: string[] = []
): Promise<GenerateTransactionResponse> {
await this.init()

if (this.clientVersion !== '3.3') {
throw new Error(
'allocateExternalParty is only supported on 3.3 clients'
)
}

const client: Client<v3_3.paths> = this.clients['3.3']

const resp = await client.POST(
'/v2/parties/external/generate-topology',
{
body: {
synchronizer: synchronizerId,
partyHint: partyHint || publicKey.slice(0, 5),
publicKey: {
format: 'CRYPTO_KEY_FORMAT_RAW',
keyData: publicKey,
keySpec: 'SIGNING_KEY_SPEC_EC_CURVE25519',
},
localParticipantObservationOnly,
confirmationThreshold,
otherConfirmingParticipantUids,
},
}
)

return this.valueOrError(resp)
}

public async post<Path extends PostEndpoint>(
path: Path,
body: PostRequest<Path>,
Expand Down
91 changes: 70 additions & 21 deletions core/ledger-client/src/topology-write-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { LedgerClient } from './ledger-client.js'
import {
AllocateExternalPartyResponse,
GenerateTransactionResponse,
LedgerClient,
MultiHashSignatures,
OnboardingTransactions,
} from './ledger-client.js'
import { createHash } from 'node:crypto'
import { PartyId } from '@canton-network/core-types'
import {
CryptoKeyFormat,
SigningKeyScheme,
Expand Down Expand Up @@ -36,8 +44,6 @@ import {
} from '@canton-network/core-ledger-proto'
import { GrpcTransport } from '@protobuf-ts/grpc-transport'
import { ChannelCredentials } from '@grpc/grpc-js'
import { createHash } from 'node:crypto'
import { PartyId } from '@canton-network/core-types'

function prefixedInt(value: number, bytes: Buffer | Uint8Array): Buffer {
const buffer = Buffer.alloc(4 + bytes.length)
Expand All @@ -46,6 +52,9 @@ function prefixedInt(value: number, bytes: Buffer | Uint8Array): Buffer {
return buffer
}

/**
* @deprecated used only to convert public key into grpc protobuf representation
*/
function signingPublicKeyFromEd25519(publicKey: string): SigningPublicKey {
return {
format: CryptoKeyFormat.RAW,
Expand All @@ -65,16 +74,23 @@ function computeSha256CantonHash(purpose: number, bytes: Uint8Array): string {
return Buffer.concat([multiprefix, hash]).toString('hex')
}

// TODO(#180): remove or rewrite after grpc is gone
export class TopologyWriteService {
private topologyClient: TopologyManagerWriteServiceClient
private topologyReadService: TopologyManagerReadServiceClient
private ledgerClient: LedgerClient

private store: StoreId
private storeId = () =>
StoreId.create({
store: {
oneofKind: 'synchronizer',
synchronizer: StoreId_Synchronizer.create({
id: this.synchronizerId,
}),
},
})

constructor(
synchronizerId: string,
private synchronizerId: string,
userAdminUrl: string,
private userAdminToken: string,
ledgerClient: LedgerClient
Expand All @@ -88,16 +104,7 @@ export class TopologyWriteService {
this.topologyReadService = new TopologyManagerReadServiceClient(
transport
)

this.ledgerClient = ledgerClient
this.store = StoreId.create({
store: {
oneofKind: 'synchronizer',
synchronizer: StoreId_Synchronizer.create({
id: synchronizerId,
}),
},
})
}

static combineHashes(hashes: Buffer[]): string {
Expand Down Expand Up @@ -132,9 +139,16 @@ export class TopologyWriteService {
return Buffer.from(predefineHashPurpose, 'hex').toString('base64')
}

static createFingerprintFromKey = (
static createFingerprintFromKey(publicKey: string): string
/** @deprecated using the protobuf publickey is no longer supported -- use the string parameter instead */
static createFingerprintFromKey(publicKey: SigningPublicKey): string
/** @deprecated using the protobuf publickey is no longer supported -- use the string parameter instead */
static createFingerprintFromKey(
publicKey: SigningPublicKey | string
): string
static createFingerprintFromKey(
publicKey: SigningPublicKey | string
): string => {
): string {
let key: SigningPublicKey

if (typeof publicKey === 'string') {
Expand All @@ -151,6 +165,7 @@ export class TopologyWriteService {
return computeSha256CantonHash(hashPurpose, key.publicKey)
}

/** @deprecated only for grpc/protobuf implementation */
static toSignedTopologyTransaction(
txHashes: Buffer<ArrayBuffer>[],
serializedTransaction: Uint8Array<ArrayBufferLike>,
Expand All @@ -177,6 +192,7 @@ export class TopologyWriteService {
})
}

/** @deprecated */
private generateTransactionsRequest(
namespace: string,
partyId: PartyId,
Expand Down Expand Up @@ -238,13 +254,14 @@ export class TopologyWriteService {
GenerateTransactionsRequest_Proposal.create({
mapping,
serial: 1,
store: this.store,
store: this.storeId(),
operation: Enums_TopologyChangeOp.ADD_REPLACE,
})
),
})
}

/** @deprecated use allocateExternalParty() instead */
async submitExternalPartyTopology(
signedTopologyTxs: SignedTopologyTransaction[],
partyId: PartyId
Expand All @@ -253,6 +270,35 @@ export class TopologyWriteService {
await this.authorizePartyToParticipant(partyId)
}

async allocateExternalParty(
onboardingTransactions: OnboardingTransactions,
multiHashSignatures: MultiHashSignatures
): Promise<AllocateExternalPartyResponse> {
return this.ledgerClient.allocateExternalParty(
this.synchronizerId,
onboardingTransactions,
multiHashSignatures
)
}

async generateTopology(
publicKey: string,
partyHint?: PartyId,
localParticipantObservationOnly: boolean = false,
confirmationThreshold: number = 1,
otherConfirmingParticipantUids: string[] = []
): Promise<GenerateTransactionResponse> {
return this.ledgerClient.generateTopology(
this.synchronizerId,
publicKey,
partyHint,
localParticipantObservationOnly,
confirmationThreshold,
otherConfirmingParticipantUids
)
}

/** @deprecated use generateTopology() */
async generateTransactions(
publicKey: string,
partyId: PartyId,
Expand Down Expand Up @@ -292,13 +338,14 @@ export class TopologyWriteService {
}).response
}

/** @deprecated */
private async addTransactions(
signedTopologyTxs: SignedTopologyTransaction[]
): Promise<AddTransactionsResponse> {
const request = AddTransactionsRequest.create({
transactions: signedTopologyTxs,
forceChanges: [],
store: this.store,
store: this.storeId(),
})

return this.topologyClient.addTransactions(request, {
Expand All @@ -308,6 +355,7 @@ export class TopologyWriteService {
}).response
}

/** @deprecated */
async waitForPartyToParticipantProposal(
partyId: PartyId
): Promise<Uint8Array | undefined> {
Expand All @@ -319,7 +367,7 @@ export class TopologyWriteService {
await this.topologyReadService.listPartyToParticipant(
ListPartyToParticipantRequest.create({
baseQuery: BaseQuery.create({
store: this.store,
store: this.storeId(),
proposals: true,
timeQuery: {
oneofKind: 'headState',
Expand All @@ -344,6 +392,7 @@ export class TopologyWriteService {
})
}

/** @deprecated */
async authorizePartyToParticipant(
partyId: PartyId
): Promise<AuthorizeResponse> {
Expand All @@ -359,7 +408,7 @@ export class TopologyWriteService {
transactionHash: Buffer.from(hash).toString('hex'),
},
mustFullyAuthorize: false,
store: this.store,
store: this.storeId(),
})

return this.topologyClient.authorize(request, {
Expand Down
Loading
Loading