Skip to content

Commit e839073

Browse files
authored
Add Canton gRPC Service Clients to provider output (NONEVM-3770) (#737)
This updates the Canton chain providers to: - Directly return gRPC service clients for all available services - All these service clients are already authenticated, i.e. they have a oauth2 token provider setup to automatically add PerRPCCredentials to all outgoing calls - Remove the custom `JWTProvider` implementation and replace with an `authentication.Provider` that re-uses existing oauth2 implementations - Add UserID & PartyID to output, since those are required by clients in order to interact with Canton Follow-up for: #673
1 parent 5e4a025 commit e839073

File tree

13 files changed

+681
-141
lines changed

13 files changed

+681
-141
lines changed

.changeset/polite-loops-beg.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
feat(chain): update Canton support to support clients and authentication
6+
7+
- Add gRPC service clients to Canton chain output
8+
- Add UserID and PartyID to Canton chain output
9+
- Remove JWTProvider and replace with oauth2.TokenSource for authentication

chain/canton/canton_chain.go

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package canton
22

33
import (
4+
"golang.org/x/oauth2"
5+
"google.golang.org/grpc"
6+
7+
apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2"
8+
adminv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2/admin"
9+
participantv30 "github.com/digital-asset/dazl-client/v8/go/api/com/digitalasset/canton/admin/participant/v30"
10+
411
chaincommon "github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/common"
512
)
613

714
type ChainMetadata = chaincommon.ChainMetadata
815

9-
// Chain represents a Canton network instance used CLDF.
16+
// Chain represents a Canton network instance initialized by CLDF.
1017
// It contains chain metadata and augments it with Canton-specific information.
1118
// In particular, it tracks all known Canton participants and their connection
1219
// details so that callers can discover and interact with the network's APIs.
@@ -22,15 +29,26 @@ type Chain struct {
2229
// A participant hosts parties and their local ledger state, and mediates all
2330
// interactions with the Canton ledger for those parties via its exposed APIs
2431
// (ledger, admin, validator, etc.). It is identified by a human-readable
25-
// name, provides a set of API endpoints, and uses a JWT provider to issue
32+
// name, provides a set of API endpoints, and uses a TokenSource to issue
2633
// authentication tokens for secure access to those endpoints.
2734
type Participant struct {
2835
// A human-readable name for the participant
2936
Name string
3037
// The endpoints to interact with the participant's APIs
3138
Endpoints ParticipantEndpoints
32-
// A JWT provider instance to generate JWTs for authentication with the participant's APIs
33-
JWTProvider JWTProvider
39+
// The set of service clients to interact with the participant's Ledger API.
40+
// All clients are ready-to-use and are already configured with the correct authentication.
41+
LedgerServices LedgerServiceClients
42+
// (Optional) The set of service clients to interact with the participant's Admin API.
43+
// Will only be populated if the participant has been configured with an Admin API URL
44+
AdminServices *AdminServiceClients
45+
// An OAuth2 token source to obtain access tokens for authentication with the participant's APIs
46+
TokenSource oauth2.TokenSource
47+
// The UserID that will be used to interact with this participant.
48+
// The TokenSource will return access tokens containing this UserID as a subject claim.
49+
UserID string
50+
// The PartyID that will be used to interact with this participant.
51+
PartyID string
3452
}
3553

3654
// ParticipantEndpoints holds all available API endpoints for a Canton participant
@@ -46,5 +64,87 @@ type ParticipantEndpoints struct {
4664
AdminAPIURL string
4765
// (HTTP) The URL to access the participant's Validator API
4866
// https://docs.sync.global/app_dev/validator_api/index.html
67+
// This also serves the Scan Proxy API, which provides access to the Global Scan and Token Standard APIs:
68+
// https://docs.sync.global/app_dev/validator_api/index.html#validator-api-scan-proxy
4969
ValidatorAPIURL string
5070
}
71+
72+
// LedgerAdminServiceClients provides all available Ledger API admin gRPC service clients.
73+
type LedgerAdminServiceClients struct {
74+
CommandInspection adminv2.CommandInspectionServiceClient
75+
IdentityProviderConfig adminv2.IdentityProviderConfigServiceClient
76+
PackageManagement adminv2.PackageManagementServiceClient
77+
ParticipantPruning adminv2.ParticipantPruningServiceClient
78+
PartyManagement adminv2.PartyManagementServiceClient
79+
UserManagement adminv2.UserManagementServiceClient
80+
}
81+
82+
// LedgerServiceClients provides all available Ledger API gRPC service clients.
83+
type LedgerServiceClients struct {
84+
CommandCompletion apiv2.CommandCompletionServiceClient
85+
Command apiv2.CommandServiceClient
86+
CommandSubmission apiv2.CommandSubmissionServiceClient
87+
EventQuery apiv2.EventQueryServiceClient
88+
PackageService apiv2.PackageServiceClient
89+
State apiv2.StateServiceClient
90+
Update apiv2.UpdateServiceClient
91+
Version apiv2.VersionServiceClient
92+
// Ledger API admin clients
93+
// These endpoints can only be accessed if the user the participant has been configured with
94+
// has admin rights. Access with caution and only if you're certain that admin rights are available.
95+
Admin LedgerAdminServiceClients
96+
}
97+
98+
// CreateLedgerServiceClients creates all LedgerServiceClients given a gRPC client connection.
99+
func CreateLedgerServiceClients(conn grpc.ClientConnInterface) LedgerServiceClients {
100+
return LedgerServiceClients{
101+
Admin: LedgerAdminServiceClients{
102+
CommandInspection: adminv2.NewCommandInspectionServiceClient(conn),
103+
IdentityProviderConfig: adminv2.NewIdentityProviderConfigServiceClient(conn),
104+
PackageManagement: adminv2.NewPackageManagementServiceClient(conn),
105+
ParticipantPruning: adminv2.NewParticipantPruningServiceClient(conn),
106+
PartyManagement: adminv2.NewPartyManagementServiceClient(conn),
107+
UserManagement: adminv2.NewUserManagementServiceClient(conn),
108+
},
109+
CommandCompletion: apiv2.NewCommandCompletionServiceClient(conn),
110+
Command: apiv2.NewCommandServiceClient(conn),
111+
CommandSubmission: apiv2.NewCommandSubmissionServiceClient(conn),
112+
EventQuery: apiv2.NewEventQueryServiceClient(conn),
113+
PackageService: apiv2.NewPackageServiceClient(conn),
114+
State: apiv2.NewStateServiceClient(conn),
115+
Update: apiv2.NewUpdateServiceClient(conn),
116+
Version: apiv2.NewVersionServiceClient(conn),
117+
}
118+
}
119+
120+
// AdminServiceClients provides all available Admin API service clients.
121+
// These services can only be accessed if the user the participant has been configured with has
122+
// admin rights.
123+
type AdminServiceClients struct {
124+
Package participantv30.PackageServiceClient
125+
ParticipantInspection participantv30.ParticipantInspectionServiceClient
126+
ParticipantRepair participantv30.ParticipantRepairServiceClient
127+
ParticipantStatus participantv30.ParticipantStatusServiceClient
128+
PartyManagement participantv30.PartyManagementServiceClient
129+
Ping participantv30.PingServiceClient
130+
Pruning participantv30.PruningServiceClient
131+
ResourceManagement participantv30.ResourceManagementServiceClient
132+
SynchronizerConnectivity participantv30.SynchronizerConnectivityServiceClient
133+
TrafficControl participantv30.TrafficControlServiceClient
134+
}
135+
136+
// CreateAdminServiceClients creates all AdminServiceClients given a gRPC client connection.
137+
func CreateAdminServiceClients(conn grpc.ClientConnInterface) AdminServiceClients {
138+
return AdminServiceClients{
139+
Package: participantv30.NewPackageServiceClient(conn),
140+
ParticipantInspection: participantv30.NewParticipantInspectionServiceClient(conn),
141+
ParticipantRepair: participantv30.NewParticipantRepairServiceClient(conn),
142+
ParticipantStatus: participantv30.NewParticipantStatusServiceClient(conn),
143+
PartyManagement: participantv30.NewPartyManagementServiceClient(conn),
144+
Ping: participantv30.NewPingServiceClient(conn),
145+
Pruning: participantv30.NewPruningServiceClient(conn),
146+
ResourceManagement: participantv30.NewResourceManagementServiceClient(conn),
147+
SynchronizerConnectivity: participantv30.NewSynchronizerConnectivityServiceClient(conn),
148+
TrafficControl: participantv30.NewTrafficControlServiceClient(conn),
149+
}
150+
}

chain/canton/canton_chain_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package canton
22

33
import (
4+
"reflect"
5+
"strings"
46
"testing"
57

68
chainsel "github.com/smartcontractkit/chain-selectors"
79
"github.com/stretchr/testify/assert"
10+
"google.golang.org/grpc"
811
)
912

1013
func TestChain_ChainInfo(t *testing.T) {
@@ -41,3 +44,39 @@ func TestChain_ChainInfo(t *testing.T) {
4144
})
4245
}
4346
}
47+
48+
func TestCreateLedgerServiceClients(t *testing.T) {
49+
t.Parallel()
50+
51+
var conn grpc.ClientConnInterface
52+
ledgerServiceClients := CreateLedgerServiceClients(conn)
53+
assertNoFieldIsZero(t, ledgerServiceClients)
54+
assertNoFieldIsZero(t, ledgerServiceClients.Admin)
55+
}
56+
57+
func TestCreateAdminServiceClients(t *testing.T) {
58+
t.Parallel()
59+
60+
var conn grpc.ClientConnInterface
61+
adminServiceClients := CreateAdminServiceClients(conn)
62+
assertNoFieldIsZero(t, adminServiceClients)
63+
}
64+
65+
// assertNoFieldIsZero checks that all fields of a struct are non-zero. If any field is zero, it fails the test and reports which fields were zero.
66+
func assertNoFieldIsZero(t *testing.T, structValue any, msgAndArgs ...any) {
67+
t.Helper()
68+
69+
var emptyFields []string
70+
structT := reflect.TypeOf(structValue)
71+
structV := reflect.ValueOf(structValue)
72+
for i := range structT.NumField() {
73+
field := structT.Field(i)
74+
if structV.Field(i).IsZero() {
75+
emptyFields = append(emptyFields, field.Name)
76+
}
77+
}
78+
79+
if len(emptyFields) > 0 {
80+
assert.Fail(t, "Expected all fields to be set, but the following fields were zero: "+strings.Join(emptyFields, ", "), msgAndArgs...)
81+
}
82+
}

chain/canton/jwt_provider.go

Lines changed: 0 additions & 34 deletions
This file was deleted.

chain/canton/jwt_provider_test.go

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package authentication
2+
3+
import (
4+
"context"
5+
6+
"golang.org/x/oauth2"
7+
"google.golang.org/grpc/credentials"
8+
"google.golang.org/grpc/credentials/insecure"
9+
)
10+
11+
// Provider provides authentication credentials for connecting to a Canton participant's API endpoints.
12+
// The Provider acts as both a raw token-source for HTTP API authentication, and a gRPC credentials provider for gRPC endpoint authentication.
13+
//
14+
// Implementations of this interface can implement different means of fetching and refreshing authentication tokens,
15+
// as well as enforcing different levels of transport security. The specific implementation of the Provider
16+
// should be chosen based on the environment being connected to (e.g. LocalNet vs. production, i.e. CI/OIDC).
17+
type Provider interface {
18+
// TokenSource returns an oauth2.TokenSource that can be used to retrieve access tokens for authenticating with the participant's API endpoints.
19+
TokenSource() oauth2.TokenSource
20+
// TransportCredentials returns gRPC transport credentials to be used when connecting to the participant's RPC endpoints.
21+
TransportCredentials() credentials.TransportCredentials
22+
// PerRPCCredentials returns gRPC per-RPC credentials to be used when connecting to the participant's gRPC endpoints.
23+
PerRPCCredentials() credentials.PerRPCCredentials
24+
}
25+
26+
// InsecureStaticProvider is an insecure implementation of Provider that always
27+
// returns the same static access token and does not provide/enforce transport security.
28+
// This provider is only suitable for testing against LocalNet or other non-production environments.
29+
type InsecureStaticProvider struct {
30+
AccessToken string
31+
}
32+
33+
var _ Provider = InsecureStaticProvider{}
34+
35+
func NewInsecureStaticProvider(accessToken string) InsecureStaticProvider {
36+
return InsecureStaticProvider{
37+
AccessToken: accessToken,
38+
}
39+
}
40+
41+
func (i InsecureStaticProvider) TokenSource() oauth2.TokenSource {
42+
return oauth2.StaticTokenSource(&oauth2.Token{
43+
AccessToken: i.AccessToken,
44+
})
45+
}
46+
47+
func (i InsecureStaticProvider) TransportCredentials() credentials.TransportCredentials {
48+
return insecure.NewCredentials()
49+
}
50+
51+
func (i InsecureStaticProvider) PerRPCCredentials() credentials.PerRPCCredentials {
52+
return insecureTokenSource{
53+
TokenSource: i.TokenSource(),
54+
}
55+
}
56+
57+
// insecureTokenSource is an insecure OAuth2 PerRPCCredentials implementation that retrieves tokens from an underlying oauth2.TokenSource.
58+
// It does not enforce transport security, making it only suitable for testing against LocalNet.
59+
type insecureTokenSource struct {
60+
oauth2.TokenSource
61+
}
62+
63+
var _ credentials.PerRPCCredentials = insecureTokenSource{}
64+
65+
func (ts insecureTokenSource) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
66+
token, err := ts.Token()
67+
if err != nil {
68+
return nil, err
69+
}
70+
if token == nil {
71+
//nolint:nilnil // nothing to do here, just returning no metadata and no error
72+
return nil, nil
73+
}
74+
75+
return map[string]string{
76+
"authorization": "Bearer " + token.AccessToken,
77+
}, nil
78+
}
79+
80+
func (ts insecureTokenSource) RequireTransportSecurity() bool {
81+
return false
82+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package authentication
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
"google.golang.org/grpc/credentials/insecure"
9+
)
10+
11+
func TestInsecureStaticProvider(t *testing.T) {
12+
t.Parallel()
13+
14+
testToken := "test-token-123"
15+
provider := NewInsecureStaticProvider(testToken)
16+
17+
// Test that TokenSource returns the correct token
18+
token, err := provider.TokenSource().Token()
19+
require.NoError(t, err)
20+
assert.Equal(t, testToken, token.AccessToken)
21+
22+
// Test that the provider returns the correct transport credentials
23+
transportCredentials := provider.TransportCredentials()
24+
assert.Equal(t, insecure.NewCredentials(), transportCredentials)
25+
26+
// Test that the provider returns the correct per RPC credentials
27+
perRPCCredentials := provider.PerRPCCredentials()
28+
require.NotNil(t, perRPCCredentials)
29+
30+
// Test that the RPC credentials return the correct metadata
31+
metadata, err := perRPCCredentials.GetRequestMetadata(t.Context())
32+
require.NoError(t, err)
33+
header, ok := metadata["authorization"]
34+
require.True(t, ok, "PerRPCCredentials didn't return authorization header")
35+
assert.Equal(t, "Bearer "+testToken, header)
36+
37+
// Test that the RPC credentials do not require transport security
38+
requireTransportSecurity := perRPCCredentials.RequireTransportSecurity()
39+
assert.False(t, requireTransportSecurity, "PerRPCCredentials must not require transport security")
40+
}

0 commit comments

Comments
 (0)