From db27c847633f571eae32e6163359ed7065909c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:08:26 +0100 Subject: [PATCH 1/5] Add Authentication for Canton LocalNet --- framework/components/blockchain/canton.go | 39 +++++++++++++++++++ .../components/blockchain/canton/nginx.go | 3 -- framework/examples/myproject/go.mod | 2 +- .../examples/myproject/smoke_canton_test.go | 21 +++------- framework/go.mod | 2 +- 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/framework/components/blockchain/canton.go b/framework/components/blockchain/canton.go index 212283797..d159fc6e9 100644 --- a/framework/components/blockchain/canton.go +++ b/framework/components/blockchain/canton.go @@ -3,12 +3,19 @@ package blockchain import ( "context" "fmt" + "time" + "github.com/golang-jwt/jwt/v5" "github.com/testcontainers/testcontainers-go" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain/canton" ) +const ( + DefaultCantonPort = "8080" + TokenExpiry = time.Hour * 24 +) + type CantonEndpoints struct { ScanAPIURL string // https://docs.sync.global/app_dev/scan_api/index.html RegistryAPIURL string // https://docs.sync.global/app_dev/token_standard/index.html#api-references @@ -27,6 +34,8 @@ type CantonParticipantEndpoints struct { HTTPHealthCheckURL string // responds on GET /health GRPCHealthCheckURL string // grpc.health.v1.Health/Check + + JWT string // JWT for this participant } // newCanton sets up a Canton blockchain network with the specified number of validators. @@ -54,6 +63,9 @@ func newCanton(ctx context.Context, in *Input) (*Output, error) { if in.NumberOfCantonValidators >= 100 { return nil, fmt.Errorf("number of validators too high: %d, valid range is 0-99", in.NumberOfCantonValidators) } + if in.Port == "" { + in.Port = DefaultCantonPort + } // Set up Postgres container postgresReq := canton.PostgresContainerRequest(in.NumberOfCantonValidators) @@ -100,6 +112,18 @@ func newCanton(ctx context.Context, in *Input) (*Output, error) { return nil, err } + svToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + Issuer: "", + Subject: "user-sv", + Audience: []string{canton.AuthProviderAudience}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpiry)), + NotBefore: jwt.NewNumericDate(time.Now()), + IssuedAt: jwt.NewNumericDate(time.Now()), + ID: "", + }).SignedString([]byte(canton.AuthProviderSecret)) + if err != nil { + return nil, fmt.Errorf("failed to create token for sv: %w", err) + } endpoints := &CantonEndpoints{ ScanAPIURL: fmt.Sprintf("http://scan.%s:%s/api/scan", host, in.Port), RegistryAPIURL: fmt.Sprintf("http://scan.%s:%s/registry", host, in.Port), @@ -110,10 +134,23 @@ func newCanton(ctx context.Context, in *Input) (*Output, error) { ValidatorAPIURL: fmt.Sprintf("http://sv.validator-api.%s:%s/api/validator", host, in.Port), HTTPHealthCheckURL: fmt.Sprintf("http://sv.http-health-check.%s:%s", host, in.Port), GRPCHealthCheckURL: fmt.Sprintf("sv.grpc-health-check.%s:%s", host, in.Port), + JWT: svToken, }, Participants: nil, } for i := 1; i <= in.NumberOfCantonValidators; i++ { + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + Issuer: "", + Subject: fmt.Sprintf("user-participant%v", i), + Audience: []string{canton.AuthProviderAudience}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpiry)), + NotBefore: jwt.NewNumericDate(time.Now()), + IssuedAt: jwt.NewNumericDate(time.Now()), + ID: "", + }).SignedString([]byte(canton.AuthProviderSecret)) + if err != nil { + return nil, fmt.Errorf("failed to create token for participant%v: %w", i, err) + } participantEndpoints := CantonParticipantEndpoints{ JSONLedgerAPIURL: fmt.Sprintf("http://participant%d.json-ledger-api.%s:%s", i, host, in.Port), GRPCLedgerAPIURL: fmt.Sprintf("participant%d.grpc-ledger-api.%s:%s", i, host, in.Port), @@ -121,6 +158,7 @@ func newCanton(ctx context.Context, in *Input) (*Output, error) { ValidatorAPIURL: fmt.Sprintf("http://participant%d.validator-api.%s:%s/api/validator", i, host, in.Port), HTTPHealthCheckURL: fmt.Sprintf("http://participant%d.http-health-check.%s:%s", i, host, in.Port), GRPCHealthCheckURL: fmt.Sprintf("participant%d.grpc-health-check.%s:%s", i, host, in.Port), + JWT: token, } endpoints.Participants = append(endpoints.Participants, participantEndpoints) } @@ -129,6 +167,7 @@ func newCanton(ctx context.Context, in *Input) (*Output, error) { UseCache: false, Type: in.Type, Family: FamilyCanton, + ChainID: in.ChainID, ContainerName: nginxReq.Name, NetworkSpecificData: &NetworkSpecificData{ CantonEndpoints: endpoints, diff --git a/framework/components/blockchain/canton/nginx.go b/framework/components/blockchain/canton/nginx.go index a1f299356..cc50e80af 100644 --- a/framework/components/blockchain/canton/nginx.go +++ b/framework/components/blockchain/canton/nginx.go @@ -191,9 +191,6 @@ func NginxContainerRequest( spliceContainerName string, ) testcontainers.ContainerRequest { nginxContainerName := framework.DefaultTCName("nginx") - if port == "" { - port = "8080" - } nginxReq := testcontainers.ContainerRequest{ Image: DefaultNginxImage, Name: nginxContainerName, diff --git a/framework/examples/myproject/go.mod b/framework/examples/myproject/go.mod index ad2bdd251..eebd2ffca 100644 --- a/framework/examples/myproject/go.mod +++ b/framework/examples/myproject/go.mod @@ -147,7 +147,7 @@ require ( github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.3 // indirect github.com/gogo/status v1.1.1 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.3 // indirect diff --git a/framework/examples/myproject/smoke_canton_test.go b/framework/examples/myproject/smoke_canton_test.go index 0593081c5..ec71e9ee3 100644 --- a/framework/examples/myproject/smoke_canton_test.go +++ b/framework/examples/myproject/smoke_canton_test.go @@ -6,11 +6,9 @@ import ( "fmt" "strings" "testing" - "time" "github.com/fullstorydev/grpcurl" "github.com/go-resty/resty/v2" - "github.com/golang-jwt/jwt/v5" "github.com/jhump/protoreflect/grpcreflect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,7 +17,6 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain/canton" ) type CfgCanton struct { @@ -48,38 +45,30 @@ func TestCantonSmoke(t *testing.T) { testParticipant := func(t *testing.T, name string, endpoints blockchain.CantonParticipantEndpoints) { t.Run(fmt.Sprintf("Test %s endpoints", name), func(t *testing.T) { - j, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ - Issuer: "", - Subject: fmt.Sprintf("user-%s", name), - Audience: []string{canton.AuthProviderAudience}, - ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), - NotBefore: jwt.NewNumericDate(time.Now()), - IssuedAt: jwt.NewNumericDate(time.Now()), - ID: "", - }).SignedString([]byte(canton.AuthProviderSecret)) + require.NoError(t, err) // JSON Ledger API fmt.Println("Calling JSON Ledger API") - resp, err := resty.New().SetBaseURL(endpoints.JSONLedgerAPIURL).SetAuthToken(j).R(). + resp, err := resty.New().SetBaseURL(endpoints.JSONLedgerAPIURL).SetAuthToken(endpoints.JWT).R(). Get("/v2/packages") assert.NoError(t, err) fmt.Println(resp) // gRPC Ledger API - use reflection fmt.Println("Calling gRPC Ledger API") - res, err := callGRPC(t.Context(), endpoints.GRPCLedgerAPIURL, "com.daml.ledger.api.v2.admin.PartyManagementService/GetParties", `{}`, []string{fmt.Sprintf("Authorization: Bearer %s", j)}) + res, err := callGRPC(t.Context(), endpoints.GRPCLedgerAPIURL, "com.daml.ledger.api.v2.admin.PartyManagementService/GetParties", `{}`, []string{fmt.Sprintf("Authorization: Bearer %s", endpoints.JWT)}) assert.NoError(t, err) fmt.Println(res) // gRPC Admin API - use reflection fmt.Println("Calling gRPC Admin API") - res, err = callGRPC(t.Context(), endpoints.AdminAPIURL, "com.digitalasset.canton.admin.participant.v30.PackageService/ListDars", `{}`, []string{fmt.Sprintf("Authorization: Bearer %s", j)}) + res, err = callGRPC(t.Context(), endpoints.AdminAPIURL, "com.digitalasset.canton.admin.participant.v30.PackageService/ListDars", `{}`, []string{fmt.Sprintf("Authorization: Bearer %s", endpoints.JWT)}) assert.NoError(t, err) fmt.Println(res) // Validator API fmt.Println("Calling Validator API") - resp, err = resty.New().SetBaseURL(endpoints.ValidatorAPIURL).SetAuthToken(j).R(). + resp, err = resty.New().SetBaseURL(endpoints.ValidatorAPIURL).SetAuthToken(endpoints.JWT).R(). Get("/v0/admin/users") assert.NoError(t, err) fmt.Println(resp) diff --git a/framework/go.mod b/framework/go.mod index 3ae0dfa95..307597847 100644 --- a/framework/go.mod +++ b/framework/go.mod @@ -17,6 +17,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.22.1 github.com/go-resty/resty/v2 v2.16.3 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/consul/sdk v0.16.2 @@ -120,7 +121,6 @@ require ( github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/status v1.1.1 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.3 // indirect From 8d99c4b8d934b5fffb77420041c1c2c9a239aa17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:09:19 +0100 Subject: [PATCH 2/5] Add changeset --- framework/.changeset/v0.13.3.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 framework/.changeset/v0.13.3.md diff --git a/framework/.changeset/v0.13.3.md b/framework/.changeset/v0.13.3.md new file mode 100644 index 000000000..e374baedb --- /dev/null +++ b/framework/.changeset/v0.13.3.md @@ -0,0 +1 @@ +- Add Canton JWT authentication From cdb5af29ea2936b88c56949babc4ac3e8930b3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:00:30 +0100 Subject: [PATCH 3/5] go mod tidy --- framework/components/fake/go.sum | 2 ++ framework/examples/myproject_cll/go.mod | 1 + framework/examples/myproject_cll/go.sum | 2 ++ 3 files changed, 5 insertions(+) diff --git a/framework/components/fake/go.sum b/framework/components/fake/go.sum index d061a2711..90892b59e 100644 --- a/framework/components/fake/go.sum +++ b/framework/components/fake/go.sum @@ -103,6 +103,8 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/framework/examples/myproject_cll/go.mod b/framework/examples/myproject_cll/go.mod index 3dc746e7e..aa909ed67 100644 --- a/framework/examples/myproject_cll/go.mod +++ b/framework/examples/myproject_cll/go.mod @@ -61,6 +61,7 @@ require ( github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect diff --git a/framework/examples/myproject_cll/go.sum b/framework/examples/myproject_cll/go.sum index 514faab07..c2cbda03e 100644 --- a/framework/examples/myproject_cll/go.sum +++ b/framework/examples/myproject_cll/go.sum @@ -136,6 +136,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= From de1cfcc22be0f2a643cdf6614546fb96f3ff15f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:17:09 +0100 Subject: [PATCH 4/5] Update test --- framework/examples/myproject/smoke_canton_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/framework/examples/myproject/smoke_canton_test.go b/framework/examples/myproject/smoke_canton_test.go index ec71e9ee3..c7e637896 100644 --- a/framework/examples/myproject/smoke_canton_test.go +++ b/framework/examples/myproject/smoke_canton_test.go @@ -45,8 +45,6 @@ func TestCantonSmoke(t *testing.T) { testParticipant := func(t *testing.T, name string, endpoints blockchain.CantonParticipantEndpoints) { t.Run(fmt.Sprintf("Test %s endpoints", name), func(t *testing.T) { - require.NoError(t, err) - // JSON Ledger API fmt.Println("Calling JSON Ledger API") resp, err := resty.New().SetBaseURL(endpoints.JSONLedgerAPIURL).SetAuthToken(endpoints.JWT).R(). From 4280c1372ca9d471c1b9d09759af3f41bb9c0ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:33:40 +0100 Subject: [PATCH 5/5] Increase timeout --- framework/components/blockchain/canton/splice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/components/blockchain/canton/splice.go b/framework/components/blockchain/canton/splice.go index 66a747661..193f0c37f 100644 --- a/framework/components/blockchain/canton/splice.go +++ b/framework/components/blockchain/canton/splice.go @@ -337,7 +337,7 @@ func SpliceContainerRequest( WaitingFor: wait.ForExec([]string{ "/bin/bash", "/app/health-check.sh", - }).WithStartupTimeout(time.Minute * 3), + }).WithStartupTimeout(time.Minute * 5), Env: map[string]string{ "DB_SERVER": postgresContainerName, "DB_USER": DefaultPostgresUser,