From 8313b7e27da2c86b0b087af1e33537b8bd529e0e Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Wed, 12 Nov 2025 17:01:53 +0100 Subject: [PATCH 1/9] Feat: Allow non registered eth and ton users to call chain Signed-off-by: Jakub Dzikowski --- .../src/client/api/PublicKeyContractAPI.ts | 6 --- chain-api/src/types/dtos.ts | 30 ------------- chain-api/src/validators/IsUserAlias.spec.ts | 19 ++++++++ chain-api/src/validators/IsUserAlias.ts | 27 +++++++++--- chain-connect/src/chainApis/PublicKeyApi.ts | 25 +---------- chain-connect/src/types/publicKeyApi.ts | 5 +-- chain-test/src/e2e/TestClients.ts | 28 +++++------- .../PublicKeyContract.migration.spec.ts | 8 +--- .../src/contracts/PublicKeyContract.spec.ts | 40 +++++++++++++++++ chaincode/src/contracts/PublicKeyContract.ts | 36 +++++++--------- .../contracts/authenticate.testutils.spec.ts | 11 ----- .../src/contracts/authenticate.ton.spec.ts | 43 ++++++------------- 12 files changed, 124 insertions(+), 154 deletions(-) diff --git a/chain-api/src/client/api/PublicKeyContractAPI.ts b/chain-api/src/client/api/PublicKeyContractAPI.ts index 211e083827..e32ea59bfb 100644 --- a/chain-api/src/client/api/PublicKeyContractAPI.ts +++ b/chain-api/src/client/api/PublicKeyContractAPI.ts @@ -17,7 +17,6 @@ import { GetMyProfileDto, GetPublicKeyDto, PublicKey, - RegisterEthUserDto, RegisterUserDto, UpdatePublicKeyDto, UserProfile, @@ -30,7 +29,6 @@ export interface PublicKeyContractAPI extends CommonContractAPI { GetPublicKey(user?: string | GetPublicKeyDto): Promise>; UpdatePublicKey(dto: UpdatePublicKeyDto): Promise>; RegisterUser(dto: RegisterUserDto): Promise>; - RegisterEthUser(dto: RegisterEthUserDto): Promise>; GetMyProfile(dto: GetMyProfileDto): Promise>; } @@ -51,10 +49,6 @@ export const publicKeyContractAPI = (client: ChainClient): PublicKeyContractAPI return client.submitTransaction("RegisterUser", dto) as Promise>; }, - RegisterEthUser(dto: RegisterEthUserDto) { - return client.submitTransaction("RegisterEthUser", dto) as Promise>; - }, - UpdatePublicKey(dto: UpdatePublicKeyDto) { return client.submitTransaction("UpdatePublicKey", dto) as Promise>; }, diff --git a/chain-api/src/types/dtos.ts b/chain-api/src/types/dtos.ts index f8c73f5b64..265ec2c41c 100644 --- a/chain-api/src/types/dtos.ts +++ b/chain-api/src/types/dtos.ts @@ -618,36 +618,6 @@ export class RegisterUserDto extends SubmitCallDTO { signatureQuorum?: number; } -/** - * @description - * - * Dto for secure method to save public keys for Eth users. - * Method is called and signed by Curators - */ -@JSONSchema({ - description: `Dto for secure method to save public keys for Eth users. Method is called and signed by Curators` -}) -export class RegisterEthUserDto extends SubmitCallDTO { - @JSONSchema({ description: "Public secp256k1 key (compact or non-compact, hex or base64)." }) - @IsNotEmpty() - publicKey: string; -} - -/** - * @description - * - * Dto for secure method to save public keys for TON users. - * Method is called and signed by Curators - */ -@JSONSchema({ - description: `Dto for secure method to save public keys for TON users. Method is called and signed by Curators` -}) -export class RegisterTonUserDto extends SubmitCallDTO { - @JSONSchema({ description: "TON user public key (Ed25519 in base64)." }) - @IsNotEmpty() - publicKey: string; -} - export class UpdatePublicKeyDto extends SubmitCallDTO { @JSONSchema({ description: diff --git a/chain-api/src/validators/IsUserAlias.spec.ts b/chain-api/src/validators/IsUserAlias.spec.ts index 60bf9a6548..0ac9be2361 100644 --- a/chain-api/src/validators/IsUserAlias.spec.ts +++ b/chain-api/src/validators/IsUserAlias.spec.ts @@ -21,6 +21,11 @@ class TestDto extends ChainCallDTO { user: UserAlias; } +class TestClientDto extends ChainCallDTO { + @IsUserAlias({ clientAliasOnly: true }) + user: UserAlias; +} + class TestArrayDto extends ChainCallDTO { @IsUserAlias({ each: true }) users: UserAlias[]; @@ -101,6 +106,20 @@ it("should validate array of user aliases", async () => { await expect(invalid).rejects.toThrow(`users property with values eth|${invalidChecksumEth} are not valid`); }); +it("should validate client alias only", async () => { + // Given + const validPlain = { user: "client|123" as UserAlias }; + const invalidPlain = { user: "service|123" as UserAlias }; + + // When + const valid = await createValidDTO(TestClientDto, validPlain); + const invalid = createValidDTO(TestClientDto, invalidPlain); + + // Then + expect(valid.user).toBe(validPlain.user); + await expect(invalid).rejects.toThrow(`Only string following the format of 'client|' is allowed`); +}); + it("should support schema generation", () => { // When const schema1 = generateSchema(TestDto); diff --git a/chain-api/src/validators/IsUserAlias.ts b/chain-api/src/validators/IsUserAlias.ts index 75a55f9641..4ce586ef32 100644 --- a/chain-api/src/validators/IsUserAlias.ts +++ b/chain-api/src/validators/IsUserAlias.ts @@ -108,6 +108,10 @@ export function isValidUserAlias(value: unknown): value is UserAlias { return meansValidUserAlias(result); } +function requiresClientAliasOnly(args: ValidationArguments): boolean { + return args.constraints?.[0] as boolean; +} + const customMessages = { [UserAliasValidationResult.INVALID_ETH_USER_ALIAS]: "User alias starting with 'eth|' must end with valid checksumed eth address without 0x prefix.", @@ -115,6 +119,8 @@ const customMessages = { "User alias starting with 'ton|' must end with valid bounceable base64 TON address." }; +const clientAliasOnlyMessage = "Only string following the format of 'client|' is allowed"; + const genericMessage = "Expected string following the format of 'client|', or 'eth|', " + "or 'ton|', or valid system-level username."; @@ -126,21 +132,32 @@ class IsUserAliasConstraint implements ValidatorConstraintInterface { return value.every((val) => this.validate(val, args)); } const result = validateUserAlias(value); - return meansValidUserAlias(result); + + if (requiresClientAliasOnly(args)) { + return meansValidUserAlias(result) && (value as string)?.startsWith("client|"); + } else { + return meansValidUserAlias(result); + } } defaultMessage(args: ValidationArguments): string { const value = args.value; + const defaultMessage = requiresClientAliasOnly(args) ? clientAliasOnlyMessage : genericMessage; + if (Array.isArray(value)) { const invalidValues = value.filter((val) => !meansValidUserAlias(validateUserAlias(val))); - return `${args.property} property with values ${invalidValues} are not valid GalaChain user aliases. ${genericMessage}`; + return `${args.property} property with values ${invalidValues} are not valid GalaChain user aliases. ${defaultMessage}`; } const result = validateUserAlias(args.value); - const details = customMessages[result] ?? genericMessage; + const details = customMessages[result] ?? defaultMessage; return `${args.property} property with value ${args.value} is not a valid GalaChain user alias. ${details}`; } } +interface IsUserAliasOptions extends ValidationOptions { + clientAliasOnly?: boolean; +} + /** * @description * @@ -154,14 +171,14 @@ class IsUserAliasConstraint implements ValidatorConstraintInterface { * @param options * */ -export function IsUserAlias(options?: ValidationOptions) { +export function IsUserAlias(options?: IsUserAliasOptions) { return function (object: object, propertyName: string) { registerDecorator({ name: "isUserAlias", target: object.constructor, propertyName, options, - constraints: [], + constraints: options?.clientAliasOnly ? [true] : [], validator: IsUserAliasConstraint }); }; diff --git a/chain-connect/src/chainApis/PublicKeyApi.ts b/chain-connect/src/chainApis/PublicKeyApi.ts index e4bbd0e8c9..00155dec10 100644 --- a/chain-connect/src/chainApis/PublicKeyApi.ts +++ b/chain-connect/src/chainApis/PublicKeyApi.ts @@ -12,17 +12,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - GetMyProfileDto, - RegisterEthUserDto, - RegisterUserDto, - UpdatePublicKeyDto, - UserProfile -} from "@gala-chain/api"; +import { GetMyProfileDto, RegisterUserDto, UpdatePublicKeyDto, UserProfile } from "@gala-chain/api"; import { plainToInstance } from "class-transformer"; import { GalaChainProvider } from "../GalaChainClient"; -import { RegisterEthUserRequest, RegisterUserRequest, UpdatePublicKeyRequest } from "../types"; +import { RegisterUserRequest, UpdatePublicKeyRequest } from "../types"; import { GalaChainBaseApi } from "./GalaChainBaseApi"; /** @@ -73,21 +67,6 @@ export class PublicKeyApi extends GalaChainBaseApi { }); } - /** - * Registers a new Ethereum user on the GalaChain network. - * @param dto - The Ethereum user registration request data - * @returns Promise resolving to registration confirmation - */ - public RegisterEthUser(dto: RegisterEthUserRequest) { - return this.connection.submit({ - method: "RegisterEthUser", - payload: dto, - sign: true, - url: this.chainCodeUrl, - requestConstructor: RegisterEthUserDto - }); - } - /** * Updates the public key for the current user. * @param dto - The public key update request data diff --git a/chain-connect/src/types/publicKeyApi.ts b/chain-connect/src/types/publicKeyApi.ts index 2a3bef4da3..2e13f9da67 100644 --- a/chain-connect/src/types/publicKeyApi.ts +++ b/chain-connect/src/types/publicKeyApi.ts @@ -12,12 +12,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RegisterEthUserDto, RegisterUserDto, UpdatePublicKeyDto } from "@gala-chain/api"; +import { RegisterUserDto, UpdatePublicKeyDto } from "@gala-chain/api"; import { ConstructorArgs } from "./utils"; type RegisterUserRequest = ConstructorArgs; -type RegisterEthUserRequest = ConstructorArgs; type UpdatePublicKeyRequest = ConstructorArgs; -export { RegisterUserRequest, RegisterEthUserRequest, UpdatePublicKeyRequest }; +export { RegisterUserRequest, UpdatePublicKeyRequest }; diff --git a/chain-test/src/e2e/TestClients.ts b/chain-test/src/e2e/TestClients.ts index 83bb4b08df..39ff5de527 100644 --- a/chain-test/src/e2e/TestClients.ts +++ b/chain-test/src/e2e/TestClients.ts @@ -20,7 +20,6 @@ import { ContractConfig, GalaChainResponseType, PublicKeyContractAPI, - RegisterEthUserDto, RegisterUserDto, commonContractAPI, createValidSubmitDTO, @@ -295,7 +294,7 @@ async function createForAdmin(opts?: T): Promise createRegisteredUser(pk, userAlias) + createRegisteredUser: async (userAlias: string) => createRegisteredUser(pk, userAlias) }; } @@ -366,25 +365,18 @@ function getAdminUser() { */ async function createRegisteredUser( client: TestChainClient & PublicKeyContractAPI, - userAlias?: string + userAlias: string ): Promise { const user = ChainUser.withRandomKeys(userAlias); - if (userAlias === undefined) { - const dto = await createValidSubmitDTO(RegisterEthUserDto, { publicKey: user.publicKey }); - const response = await client.RegisterEthUser(dto.signed(client.privateKey)); - if (response.Status !== GalaChainResponseType.Success) { - throw new Error(`Failed to register eth user: ${response.Message}`); - } - } else { - const dto = await createValidSubmitDTO(RegisterUserDto, { - user: user.identityKey, - publicKey: user.publicKey - }); - const response = await client.RegisterUser(dto.signed(client.privateKey)); - if (response.Status !== GalaChainResponseType.Success) { - throw new Error(`Failed to register user: ${response.Message}`); - } + const dto = await createValidSubmitDTO(RegisterUserDto, { + user: user.identityKey, + publicKey: user.publicKey + }); + + const response = await client.RegisterUser(dto.signed(client.privateKey)); + if (response.Status !== GalaChainResponseType.Success) { + throw new Error(`Failed to register user: ${response.Message}`); } return user; diff --git a/chaincode/src/contracts/PublicKeyContract.migration.spec.ts b/chaincode/src/contracts/PublicKeyContract.migration.spec.ts index 107a586160..09a22643b3 100644 --- a/chaincode/src/contracts/PublicKeyContract.migration.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.migration.spec.ts @@ -16,7 +16,6 @@ import { ChainCallDTO, GalaChainResponse, GalaChainSuccessResponse, - RegisterEthUserDto, SubmitCallDTO, UpdateUserRolesDto, UserProfileStrict, @@ -68,12 +67,7 @@ describe("Migration from allowedOrgs to allowedRoles", () => { return resp.Data; } - test("When: User is registered with no allowed role", async () => { - const dto = await createValidSubmitDTO(RegisterEthUserDto, { publicKey: user.publicKey }).signed( - adminPrivateKey - ); - expect(await chaincode.invoke("PublicKeyContract:RegisterEthUser", dto)).toEqual(transactionSuccess()); - + test("When: User has no allowed role", async () => { const profile = await getUserProfile(); expect(profile).toEqual(expect.objectContaining({ alias: user.identityKey })); expect(profile.roles).not.toContain(allowedRole); diff --git a/chaincode/src/contracts/PublicKeyContract.spec.ts b/chaincode/src/contracts/PublicKeyContract.spec.ts index 4e94cf8670..20396f4b8b 100644 --- a/chaincode/src/contracts/PublicKeyContract.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.spec.ts @@ -105,6 +105,46 @@ describe("RegisterUser", () => { ); }); + it("should register user twice (idempotency)", async () => { + // Given + const chaincode = new TestChaincode([PublicKeyContract]); + const plain = { user: "client|user2" as UserAlias, publicKey }; + const adminPrivateKey = process.env.DEV_ADMIN_PRIVATE_KEY as string; + + const dto1 = await createValidSubmitDTO(RegisterUserDto, plain).signed(adminPrivateKey); + const dto2 = await createValidSubmitDTO(RegisterUserDto, plain).signed(adminPrivateKey); + const dto3 = await createValidSubmitDTO(RegisterUserDto, { + ...plain, + publicKey: signatures.genKeyPair().publicKey + }).signed(adminPrivateKey); + + // When + const response1 = await chaincode.invoke("PublicKeyContract:RegisterUser", dto1); + const response2 = await chaincode.invoke("PublicKeyContract:RegisterUser", dto2); + const response3 = await chaincode.invoke("PublicKeyContract:RegisterUser", dto3); + + // Then + expect(response1).toEqual(transactionSuccess()); + expect(response2).toEqual(transactionSuccess()); + expect(response3).toEqual(expect.objectContaining({ Status: 0, ErrorKey: "PK_EXISTS" })); + + expect(await getPublicKey(chaincode, dto1.user)).toEqual( + transactionSuccess({ + publicKey: PublicKeyService.normalizePublicKey(publicKey), + signing: SigningScheme.ETH + }) + ); + + expect(await getUserProfile(chaincode, ethAddress)).toEqual( + transactionSuccess({ + alias: dto1.user, + ethAddress, + roles: UserProfile.DEFAULT_ROLES, + signatureQuorum: 1 + }) + ); + }); + it("should fail when user publicKey and UserProfile are already registered", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); diff --git a/chaincode/src/contracts/PublicKeyContract.ts b/chaincode/src/contracts/PublicKeyContract.ts index 00caa0de9b..2ce2c77af2 100644 --- a/chaincode/src/contracts/PublicKeyContract.ts +++ b/chaincode/src/contracts/PublicKeyContract.ts @@ -18,18 +18,15 @@ import { GetMyProfileDto, GetPublicKeyDto, PublicKey, - RegisterEthUserDto, - RegisterTonUserDto, RegisterUserDto, RemoveSignerDto, SigningScheme, + SubmitCallDTO, UpdatePublicKeyDto, UpdateQuorumDto, UpdateUserRolesDto, - UserAlias, UserProfile, - ValidationFailedError, - signatures + ValidationFailedError } from "@gala-chain/api"; import { Info } from "fabric-contract-api"; @@ -100,30 +97,29 @@ export class PublicKeyContract extends GalaContract { } @Submit({ - in: RegisterEthUserDto, + in: SubmitCallDTO, out: "string", - description: "Registers a new user on chain under alias derived from eth address.", + description: + "Registration of eth| users is no longer required. This method will be removed in the future.", + deprecated: true, ...requireRegistrarAuth }) - public async RegisterEthUser(ctx: GalaChainContext, dto: RegisterEthUserDto): Promise { - const providedPkHex = signatures.getNonCompactHexPublicKey(dto.publicKey); - const ethAddress = signatures.getEthAddress(providedPkHex); - const userAlias = `eth|${ethAddress}` as UserAlias; - - return PublicKeyService.registerUser(ctx, dto.publicKey, undefined, userAlias, SigningScheme.ETH, 1); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async RegisterEthUser(ctx: GalaChainContext, dto: SubmitCallDTO): Promise { + return "Registration of eth| users is no longer required."; } @Submit({ - in: RegisterTonUserDto, + in: SubmitCallDTO, out: "string", - description: "Registers a new user on chain under alias derived from TON address.", + description: + "Registration of ton| users is no longer required. This method will be removed in the future.", + deprecated: true, ...requireRegistrarAuth }) - public async RegisterTonUser(ctx: GalaChainContext, dto: RegisterTonUserDto): Promise { - const address = signatures.ton.getTonAddress(Buffer.from(dto.publicKey, "base64")); - const userAlias = `ton|${address}` as UserAlias; - - return PublicKeyService.registerUser(ctx, dto.publicKey, undefined, userAlias, SigningScheme.TON, 1); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async RegisterTonUser(ctx: GalaChainContext, dto: SubmitCallDTO): Promise { + return "Registration of ton| users is no longer required."; } @Submit({ diff --git a/chaincode/src/contracts/authenticate.testutils.spec.ts b/chaincode/src/contracts/authenticate.testutils.spec.ts index 6784e40fe4..20896ba848 100644 --- a/chaincode/src/contracts/authenticate.testutils.spec.ts +++ b/chaincode/src/contracts/authenticate.testutils.spec.ts @@ -19,13 +19,11 @@ import { GetObjectDto, GetPublicKeyDto, PublicKey, - RegisterTonUserDto, RegisterUserDto, SigningScheme, UserAlias, UserProfile, UserRef, - UserRole, createValidDTO, createValidSubmitDTO, signatures @@ -106,15 +104,6 @@ export async function createTonUser(): Promise { return { alias, privateKey, publicKey, tonAddress }; } -export async function createRegisteredTonUser(chaincode: TestChaincode): Promise { - const user = await createTonUser(); - const dto = await createValidSubmitDTO(RegisterTonUserDto, { publicKey: user.publicKey }); - const signedDto = dto.signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); - const response = await chaincode.invoke("PublicKeyContract:RegisterTonUser", signedDto); - expect(response).toEqual(transactionSuccess()); - return user; -} - export function createSignedDto(unsigned: ChainCallDTO, privateKey: string) { const dto = instanceToInstance(unsigned); const keyBuff = signatures.normalizePrivateKey(privateKey); diff --git a/chaincode/src/contracts/authenticate.ton.spec.ts b/chaincode/src/contracts/authenticate.ton.spec.ts index 4417ef84eb..e15bd00666 100644 --- a/chaincode/src/contracts/authenticate.ton.spec.ts +++ b/chaincode/src/contracts/authenticate.ton.spec.ts @@ -17,12 +17,7 @@ import { TestChaincode, transactionSuccess } from "@gala-chain/test"; import { instanceToPlain, plainToClass } from "class-transformer"; import { PublicKeyContract } from "./PublicKeyContract"; -import { - TonUser, - createRegisteredTonUser, - createTonSignedDto, - createTonUser -} from "./authenticate.testutils.spec"; +import { TonUser, createTonSignedDto, createTonUser } from "./authenticate.testutils.spec"; /** * Tests below cover a wide range of scenarios for GetMyProfile method for TON signing scheme, @@ -56,10 +51,6 @@ const signerAdd = labeled("signer address")( ); const _________ = labeled("raw dto")(() => ({})); -type UserRegistered = (ch: TestChaincode) => Promise; -const ___registered = labeled("user registered")((ch) => createRegisteredTonUser(ch)); -const notRegistered = labeled("user not registered")(() => createTonUser()); - type Expectation = (response: unknown, user: TonUser) => void; const Success = labeled("Success")((response, user) => { expect(response).toEqual( @@ -79,30 +70,20 @@ const Error: (errorKey: string) => Expectation = (errorKey) => }); test.each([ - [__valid, _________, ___registered, Error("MISSING_SIGNER")], - [__valid, _________, notRegistered, Error("MISSING_SIGNER")], - [__valid, signerKey, ___registered, Success], - [__valid, signerKey, notRegistered, Error("USER_NOT_REGISTERED")], - [__valid, signerRef, ___registered, Success], - [__valid, signerAdd, ___registered, Success], - [__valid, signerRef, notRegistered, Error("USER_NOT_REGISTERED")], - [invalid, _________, ___registered, Error("MISSING_SIGNER")], - [invalid, _________, notRegistered, Error("MISSING_SIGNER")], - [invalid, signerKey, ___registered, Error("PK_INVALID_SIGNATURE")], - [invalid, signerKey, notRegistered, Error("PK_INVALID_SIGNATURE")], - [invalid, signerRef, ___registered, Error("PK_INVALID_SIGNATURE")], - [invalid, signerRef, notRegistered, Error("USER_NOT_REGISTERED")] -])("(%s, %s, %s) => %s", performTest); + [__valid, _________, Error("MISSING_SIGNER")], + [__valid, signerKey, Success], + [__valid, signerRef, Success], + [__valid, signerAdd, Success], + [invalid, _________, Error("MISSING_SIGNER")], + [invalid, signerKey, Error("PK_INVALID_SIGNATURE")], + [invalid, signerRef, Error("PK_INVALID_SIGNATURE")], + [invalid, signerAdd, Error("PK_INVALID_SIGNATURE")] +])("(%s, %s) => %s", performTest); -async function performTest( - signatureFn: Signature, - publicKeyFn: PublicKey, - createUserFn: UserRegistered, - expectation: Expectation -) { +async function performTest(signatureFn: Signature, publicKeyFn: PublicKey, expectation: Expectation) { // Given const chaincode = new TestChaincode([PublicKeyContract]); - const userObj = await createUserFn(chaincode); + const userObj = await createTonUser(); const dto = new ChainCallDTO(); dto.signing = SigningScheme.TON; From 54b6de42bbc18ac9c7a26a7fb5c3ae357dec6194 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Thu, 13 Nov 2025 14:54:20 +0100 Subject: [PATCH 2/9] Refactor: Deprecate RegisterEthUser and RegisterTonUser methods, update user registration process - Removed allowNonRegisteredUsers configuration and related checks. - Updated user registration to use RegisterUser for all user types. - Adjusted tests and documentation to reflect changes in user registration. - Enhanced user profile retrieval to support unregistered users with default profiles. - Deprecated methods now return messages indicating their removal in future versions. Signed-off-by: Jakub Dzikowski --- chain-test/src/unit/fixture.ts | 5 +- .../__test__/MockedChaincodeClient.spec.ts | 54 +++-- .../PublicKeyContract.multisig.spec.ts | 33 +--- .../src/contracts/PublicKeyContract.spec.ts | 187 +++++++++++------- .../PublicKeyContract.spec.ts.snap | 24 +-- .../src/contracts/authenticate.eth.spec.ts | 91 +++------ .../contracts/authenticate.testutils.spec.ts | 11 +- .../src/contracts/authenticate.ton.spec.ts | 8 +- chaincode/src/contracts/authenticate.ts | 6 +- chaincode/src/services/PublicKeyService.ts | 30 ++- chaincode/src/types/GalaChainContext.ts | 23 +-- docs/authorization.md | 60 +----- 12 files changed, 221 insertions(+), 311 deletions(-) diff --git a/chain-test/src/unit/fixture.ts b/chain-test/src/unit/fixture.ts index 574ed35cd0..722933d606 100644 --- a/chain-test/src/unit/fixture.ts +++ b/chain-test/src/unit/fixture.ts @@ -18,6 +18,7 @@ import { ClassConstructor, GalaChainResponse, RangedChainObject, + SigningScheme, UserAlias, UserProfile, signatures @@ -109,7 +110,6 @@ interface CallingUserDataDryRun { */ interface GalaChainContextConfig { readonly adminPublicKey?: string; - readonly allowNonRegisteredUsers?: boolean; } interface TestOperationContext { @@ -129,8 +129,7 @@ type TestGalaChainContext = Context & { readonly logger: GalaLoggerInstance; set callingUserData(d: CallingUserData); get callingUser(): UserAlias; - get callingUserEthAddress(): string; - get callingUserTonAddress(): string; + get callingUserAddress(): { address: string; signing: SigningScheme }; get callingUserRoles(): string[]; get callingUserSignedBy(): UserAlias[]; get callingUserSignatureQuorum(): number; diff --git a/chaincode/src/__test__/MockedChaincodeClient.spec.ts b/chaincode/src/__test__/MockedChaincodeClient.spec.ts index 1d388ab4f5..6c5667eaeb 100644 --- a/chaincode/src/__test__/MockedChaincodeClient.spec.ts +++ b/chaincode/src/__test__/MockedChaincodeClient.spec.ts @@ -16,9 +16,10 @@ import { ChainCallDTO, GetPublicKeyDto, PublicKey, - RegisterEthUserDto, + RegisterUserDto, SigningScheme, UserAlias, + asValidUserAlias, createValidChainObject, createValidDTO, createValidSubmitDTO, @@ -37,7 +38,7 @@ beforeAll(async () => { // Prepare user data const userKeys = signatures.genKeyPair(); - const userAlias = `eth|${signatures.getEthAddress(userKeys.publicKey)}`; + const userAlias = asValidUserAlias(`client|user1`); user = { ...userKeys, base64PublicKey: signatures.getCompactBase64PublicKey(userKeys.publicKey), @@ -75,10 +76,16 @@ it("should support the global state", async () => { const client1 = createClient(chaincodeDir); const client2 = createClient(chaincodeDir); - const registerDto = await createValidSubmitDTO(RegisterEthUserDto, { publicKey: user.publicKey }); - registerDto.sign(admin.privateKey); + const registerDto = ( + await createValidSubmitDTO(RegisterUserDto, { + user: user.alias, + publicKey: user.publicKey + }) + ) + .withPublicKeySignedBy(user.privateKey) + .signed(admin.privateKey); - const getProfileDto = await createValidDTO(GetPublicKeyDto, { user: user.alias }); + const getPublicKeyDto = await createValidDTO(GetPublicKeyDto, { user: user.alias }); const expectedPublicKey = await createValidChainObject(PublicKey, { publicKey: user.base64PublicKey, @@ -86,18 +93,18 @@ it("should support the global state", async () => { }); // initially the key is missing - const noKeyResponse = await client1.evaluateTransaction("GetPublicKey", getProfileDto); + const noKeyResponse = await client1.evaluateTransaction("GetPublicKey", getPublicKeyDto); expect(noKeyResponse).toEqual(transactionErrorKey("PK_NOT_FOUND")); // When - const registerResponse = await client1.submitTransaction("RegisterEthUser", registerDto); + const registerResponse = await client1.submitTransaction("RegisterUser", registerDto); // Then expect(registerResponse).toEqual(transactionSuccess()); // both clients can get the key - const keyResponse1 = await client1.evaluateTransaction("GetPublicKey", getProfileDto); - const keyResponse2 = await client2.evaluateTransaction("GetPublicKey", getProfileDto); + const keyResponse1 = await client1.evaluateTransaction("GetPublicKey", getPublicKeyDto); + const keyResponse2 = await client2.evaluateTransaction("GetPublicKey", getPublicKeyDto); expect(keyResponse1).toEqual(transactionSuccess(expectedPublicKey)); expect(keyResponse2).toEqual(transactionSuccess(expectedPublicKey)); }); @@ -107,10 +114,13 @@ it("should not change the state for evaluateTransaction", async () => { const client = createClient(chaincodeDir); const otherUser = signatures.genKeyPair(); - const otherUserAlias = `eth|${signatures.getEthAddress(otherUser.publicKey)}` as UserAlias; + const otherUserAlias = asValidUserAlias(`client|other-user`); - const registerDto = await createValidSubmitDTO(RegisterEthUserDto, { publicKey: otherUser.publicKey }); - registerDto.sign(admin.privateKey); + const registerDto = ( + await createValidSubmitDTO(RegisterUserDto, { user: otherUserAlias, publicKey: otherUser.publicKey }) + ) + .withPublicKeySignedBy(otherUser.privateKey) + .signed(admin.privateKey); const getProfileDto = await createValidDTO(GetPublicKeyDto, { user: otherUserAlias }); @@ -119,7 +129,7 @@ it("should not change the state for evaluateTransaction", async () => { expect(noKeyResponse1).toEqual(transactionErrorKey("PK_NOT_FOUND")); // initially the key is missing // When - const registerEvaluateResponse = await client.evaluateTransaction("RegisterEthUser", registerDto); + const registerEvaluateResponse = await client.evaluateTransaction("RegisterUser", registerDto); // Then expect(registerEvaluateResponse).toEqual(transactionSuccess()); // evaluate does not change the state @@ -131,24 +141,6 @@ it("should not change the state for evaluateTransaction", async () => { }); it.skip("should support key collision validation", async () => { - // Given - const transactionDelayMs = 100; - const client1 = createClient(chaincodeDir, transactionDelayMs); - const client2 = createClient(chaincodeDir, transactionDelayMs); - - const otherUser = signatures.genKeyPair(); - const registerDto = await createValidSubmitDTO(RegisterEthUserDto, { publicKey: otherUser.publicKey }); - registerDto.sign(admin.privateKey); - - // When - const parallelCalls = await Promise.all([ - client1.submitTransaction("RegisterEthUser", registerDto), - client2.submitTransaction("RegisterEthUser", registerDto) - ]); - - // Then - expect(parallelCalls).toEqual([transactionSuccess(), "MVCC_CONFLICT"]); // change the last value - // Set transaction delay, and call two conflicting transactions in parallel (either the same client or different client) throw new Error("Not implemented"); }); diff --git a/chaincode/src/contracts/PublicKeyContract.multisig.spec.ts b/chaincode/src/contracts/PublicKeyContract.multisig.spec.ts index 74e2af8114..317b8f96c5 100644 --- a/chaincode/src/contracts/PublicKeyContract.multisig.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.multisig.spec.ts @@ -44,19 +44,6 @@ import { getUserProfile } from "./authenticate.testutils.spec"; -let prevEnv: string | undefined; - -beforeAll(() => { - // we are enabling ALLOW_NON_REGISTERED_USERS for this test suite - // so the registration of signers is not required - prevEnv = process.env.ALLOW_NON_REGISTERED_USERS; - process.env.ALLOW_NON_REGISTERED_USERS = "true"; -}); - -afterAll(() => { - process.env.ALLOW_NON_REGISTERED_USERS = prevEnv; -}); - describe("PublicKeyContract Multisignature", () => { describe("RegisterUser", () => { it("should register user with 3 signers", async () => { @@ -78,21 +65,6 @@ describe("PublicKeyContract Multisignature", () => { }); const signedDto = dto.signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); - // ensure fetching default users work - const p1Resp = await chaincode.invoke( - "PublicKeyContract:GetMyProfile", - new GetMyProfileDto().expiresInMs(60_000).signed(key1.privateKey) - ); - expect(p1Resp).toEqual( - transactionSuccess({ - alias: `eth|${ethAddresses[0]}`, - ethAddress: ethAddresses[0], - roles: UserProfile.DEFAULT_ROLES, - signatureQuorum: 1, - signers: [`eth|${ethAddresses[0]}`] - }) - ); - // When const response = await chaincode.invoke("PublicKeyContract:RegisterUser", signedDto); @@ -335,7 +307,6 @@ describe("PublicKeyContract Multisignature", () => { // signing is broken => recovers public key to non-existing user // and we cannot use default user for multisig even if the feature is enabled - expect(process.env.ALLOW_NON_REGISTERED_USERS).toEqual("true"); expect(resp3).toEqual(transactionErrorKey("UNAUTHORIZED")); expect(resp3).toEqual(transactionErrorMessageContains(`is not allowed to sign ${alias}.`)); }); @@ -533,8 +504,8 @@ describe("PublicKeyContract Multisignature", () => { const response = await chaincode.invoke("PublicKeyContract:UpdatePublicKey", dto); // Then - const errMsg = `Public key is not saved for user ${alias}`; - expect(response).toEqual(transactionErrorKey("PK_NOT_FOUND")); + const errMsg = `No address known for user ${alias}`; + expect(response).toEqual(transactionErrorKey("UNAUTHORIZED")); expect(response).toEqual(transactionErrorMessageContains(errMsg)); }); }); diff --git a/chaincode/src/contracts/PublicKeyContract.spec.ts b/chaincode/src/contracts/PublicKeyContract.spec.ts index 458fe87636..4a23cec370 100644 --- a/chaincode/src/contracts/PublicKeyContract.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.spec.ts @@ -17,10 +17,9 @@ import { GalaChainSuccessResponse, GetMyProfileDto, GetPublicKeyDto, - RegisterEthUserDto, - RegisterTonUserDto, RegisterUserDto, SigningScheme, + SubmitCallDTO, UpdatePublicKeyDto, UpdateUserRolesDto, UserAlias, @@ -44,10 +43,11 @@ import { PublicKeyService } from "../services"; import { PublicKeyContract } from "./PublicKeyContract"; import { createDerSignedDto, + createEthUser, createRegisteredMultiSigUserForUsers, - createRegisteredTonUser, createRegisteredUser, createSignedDto, + createTonUser, createUser, getMyProfile, getPublicKey, @@ -334,73 +334,30 @@ describe("RegisterUser", () => { ); }); - it("RegisterEthUser should register user with eth address", async () => { + it("RegisterEthUser should return deprecation message", async () => { // Given - const keyPair = signatures.genKeyPair(); - const publicKey = keyPair.publicKey; - const pkHex = signatures.getNonCompactHexPublicKey(publicKey); - const ethAddress = signatures.getEthAddress(pkHex); - const alias = `eth|${ethAddress}` as UserAlias; - const chaincode = new TestChaincode([PublicKeyContract]); - const dto = await createValidSubmitDTO(RegisterEthUserDto, { publicKey }); + const dto = await createValidSubmitDTO(SubmitCallDTO, {}); const signedDto = dto.signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); // When const response = await chaincode.invoke("PublicKeyContract:RegisterEthUser", signedDto); // Then - expect(response).toEqual(transactionSuccess(alias)); - - expect(await getPublicKey(chaincode, alias)).toEqual( - transactionSuccess({ - publicKey: PublicKeyService.normalizePublicKey(publicKey), - signing: SigningScheme.ETH - }) - ); - - expect(await getUserProfile(chaincode, ethAddress)).toEqual( - transactionSuccess({ - alias, - ethAddress, - roles: UserProfile.DEFAULT_ROLES, - signatureQuorum: 1 - }) - ); + expect(response).toEqual(transactionSuccess("Registration of eth| users is no longer required.")); }); - it("RegisterTonUser should register user with ton address", async () => { + it("RegisterTonUser should return deprecation message", async () => { // Given - const tonKeyPair = await signatures.ton.genKeyPair(); - const publicKey = Buffer.from(tonKeyPair.publicKey).toString("base64"); - const address = signatures.ton.getTonAddress(tonKeyPair.publicKey); - const alias = `ton|${address}` as UserAlias; - const chaincode = new TestChaincode([PublicKeyContract]); - const dto = await createValidSubmitDTO(RegisterTonUserDto, { publicKey }); + const dto = await createValidSubmitDTO(SubmitCallDTO, {}); const signedDto = dto.signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); // When const response = await chaincode.invoke("PublicKeyContract:RegisterTonUser", signedDto); // Then - expect(response).toEqual(transactionSuccess(alias)); - - expect(await getPublicKey(chaincode, alias)).toEqual( - transactionSuccess({ - publicKey, - signing: SigningScheme.TON - }) - ); - - expect(await getUserProfile(chaincode, address)).toEqual( - transactionSuccess({ - alias, - tonAddress: address, - roles: UserProfile.DEFAULT_ROLES, - signatureQuorum: 1 - }) - ); + expect(response).toEqual(transactionSuccess("Registration of ton| users is no longer required.")); }); }); @@ -559,18 +516,22 @@ describe("UpdatePublicKey", () => { it("should update TON public key", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); - const user = await createRegisteredTonUser(chaincode); + const user = await createTonUser(); const newPair = await signatures.ton.genKeyPair(); + const newPrivateKeyBase64 = newPair.secretKey.toString("base64"); const dto = ( await createValidSubmitDTO(UpdatePublicKeyDto, { - publicKey: Buffer.from(newPair.publicKey).toString("base64"), + publicKey: newPair.publicKey.toString("base64"), signerPublicKey: user.publicKey, signing: SigningScheme.TON }) ) - .withPublicKeySignedBy(Buffer.from(newPair.secretKey).toString("base64")) + .withPublicKeySignedBy(newPrivateKeyBase64) .signed(user.privateKey); + // no public key saved for this user + expect(await getPublicKey(chaincode, user.alias)).toEqual(transactionErrorKey("PK_NOT_FOUND")); + // When const response = await chaincode.invoke("PublicKeyContract:UpdatePublicKey", dto); @@ -588,8 +549,8 @@ describe("UpdatePublicKey", () => { it("should prevent from changing key type", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); - const tonUser = await createRegisteredTonUser(chaincode); - const ethUser = await createRegisteredUser(chaincode); + const tonUser = await createTonUser(); + const ethUser = await createEthUser(); const ethKeyPair = signatures.genKeyPair(); const tonKeyPair = await signatures.ton.genKeyPair(); @@ -708,10 +669,11 @@ describe("VerifySignature", () => { }); describe("GetMyProfile", () => { - it("should get saved profile (ETH)", async () => { + it("should get saved profile (client|)", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); const user = await createRegisteredUser(chaincode); + expect(user.alias).toContain("client|"); // regular signing const dto1 = new GetMyProfileDto(); @@ -746,10 +708,52 @@ describe("GetMyProfile", () => { expect(resp3).toEqual(resp1); }); - it("should get saved profile (TON)", async () => { + it("should get unregistered profile (eth|)", async () => { + // Given + const chaincode = new TestChaincode([PublicKeyContract]); + const user = await createEthUser(); + expect(user.alias).toContain("eth|"); + + // regular signing + const dto1 = new GetMyProfileDto(); + dto1.sign(user.privateKey); + + // DER + signerPublicKey + const dto2 = new GetMyProfileDto(); + dto2.signerPublicKey = user.publicKey; + dto2.sign(user.privateKey, true); + + // DER + signerAddress + const dto3 = new GetMyProfileDto(); + dto3.signerAddress = asValidUserRef(user.ethAddress); + dto3.sign(user.privateKey, true); + + // When + const resp1 = await chaincode.invoke("PublicKeyContract:GetMyProfile", dto1); + const resp2 = await chaincode.invoke("PublicKeyContract:GetMyProfile", dto2); + const resp3 = await chaincode.invoke("PublicKeyContract:GetMyProfile", dto3); + + // Then + expect(resp1).toEqual( + transactionSuccess({ + alias: user.alias, + ethAddress: user.ethAddress, + roles: [UserRole.EVALUATE, UserRole.SUBMIT], + signatureQuorum: 1, + signers: [user.alias] + }) + ); + expect(resp2).toEqual(resp1); + + // it is not possible to recover public key from the provided payload + expect(resp3).toEqual(transactionErrorKey("USER_NOT_REGISTERED")); + }); + + it("should get unregistered profile (ton|)", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); - const user = await createRegisteredTonUser(chaincode); + const user = await createTonUser(); + expect(user.alias).toContain("ton|"); const dto1 = new GetMyProfileDto(); dto1.signing = SigningScheme.TON; @@ -775,10 +779,12 @@ describe("GetMyProfile", () => { signers: [`ton|${user.tonAddress}`] }) ); - expect(resp2).toEqual(resp1); + + // it is not possible to recover public key from the provided payload + expect(resp2).toEqual(transactionErrorKey("USER_NOT_REGISTERED")); }); - it("should get saved profile (ETH) with multiple public keys", async () => { + it("should get saved profile (client| with multiple signers)", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); @@ -848,7 +854,7 @@ describe("GetMyProfile", () => { }); describe("UpdateUserRoles", () => { - it("should allow registrar to update user roles", async () => { + it("should allow registrar to update user roles for registered client| user", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); @@ -858,11 +864,36 @@ describe("UpdateUserRoles", () => { const user = await createRegisteredUser(chaincode); const userProfile = await getUserProfile(chaincode, user.ethAddress); - expect(userProfile.Data?.roles).not.toContain("CUSTOM_ROLE"); + expect(userProfile.Data?.roles).not.toContain("CUSTOM_CLIENT_ROLE"); + expect(userProfile.Data?.alias).toContain("client|"); const dto = await createValidSubmitDTO(UpdateUserRolesDto, { user: user.alias, - roles: ["CUSTOM_ROLE"] + roles: ["CUSTOM_CLIENT_ROLE"] + }).signed(adminPrivateKey); + + // When + const response = await chaincode.invoke("PublicKeyContract:UpdateUserRoles", dto); + + // Then + expect(response).toEqual(transactionSuccess()); + + const updatedUserProfile = await getUserProfile(chaincode, user.ethAddress); + expect(updatedUserProfile.Data?.roles).toContain("CUSTOM_CLIENT_ROLE"); + }); + + it("should allow registrar to update user roles for non-registered eth| user", async () => { + // Given + const chaincode = new TestChaincode([PublicKeyContract]); + const adminPrivateKey = process.env.DEV_ADMIN_PRIVATE_KEY as string; + + const user = await createEthUser(); + const userProfile = await getUserProfile(chaincode, user.ethAddress); + expect(userProfile.Data).toBeUndefined(); + + const dto = await createValidSubmitDTO(UpdateUserRolesDto, { + user: user.alias, + roles: ["CUSTOM_ETH_ROLE"] }).signed(adminPrivateKey); // When @@ -872,7 +903,31 @@ describe("UpdateUserRoles", () => { expect(response).toEqual(transactionSuccess()); const updatedUserProfile = await getUserProfile(chaincode, user.ethAddress); - expect(updatedUserProfile.Data?.roles).toContain("CUSTOM_ROLE"); + expect(updatedUserProfile.Data?.roles).toContain("CUSTOM_ETH_ROLE"); + }); + + it("should allow registrar to update user roles for non-registered ton| user", async () => { + // Given + const chaincode = new TestChaincode([PublicKeyContract]); + const adminPrivateKey = process.env.DEV_ADMIN_PRIVATE_KEY as string; + + const user = await createTonUser(); + const userProfile = await getUserProfile(chaincode, user.tonAddress); + expect(userProfile.Data).toBeUndefined(); + + const dto = await createValidSubmitDTO(UpdateUserRolesDto, { + user: user.alias, + roles: ["CUSTOM_TON_ROLE"] + }).signed(adminPrivateKey); + + // When + const response = await chaincode.invoke("PublicKeyContract:UpdateUserRoles", dto); + + // Then + expect(response).toEqual(transactionSuccess()); + + const updatedUserProfile = await getUserProfile(chaincode, user.tonAddress); + expect(updatedUserProfile.Data?.roles).toContain("CUSTOM_TON_ROLE"); }); it("should not allow user to update roles if they do not have the registrar role", async () => { diff --git a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap index cb4a8f33f2..c0218d6c35 100644 --- a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap +++ b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap @@ -1117,9 +1117,9 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Registers a new user on chain under alias derived from eth address. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", + "deprecated": true, + "description": "Registration of eth| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { - "description": "Dto for secure method to save public keys for Eth users. Method is called and signed by Curators", "properties": { "dtoExpiresAt": { "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", @@ -1141,11 +1141,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "publicKey": { - "description": "Public secp256k1 key (compact or non-compact, hex or base64).", - "minLength": 1, - "type": "string", - }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -1177,9 +1172,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "string", }, }, - "required": [ - "publicKey", - ], "type": "object", }, "isWrite": true, @@ -1207,9 +1199,9 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Registers a new user on chain under alias derived from TON address. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", + "deprecated": true, + "description": "Registration of ton| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { - "description": "Dto for secure method to save public keys for TON users. Method is called and signed by Curators", "properties": { "dtoExpiresAt": { "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", @@ -1231,11 +1223,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "publicKey": { - "description": "TON user public key (Ed25519 in base64).", - "minLength": 1, - "type": "string", - }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -1267,9 +1254,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "string", }, }, - "required": [ - "publicKey", - ], "type": "object", }, "isWrite": true, diff --git a/chaincode/src/contracts/authenticate.eth.spec.ts b/chaincode/src/contracts/authenticate.eth.spec.ts index 74dcbf90b2..55475d36db 100644 --- a/chaincode/src/contracts/authenticate.eth.spec.ts +++ b/chaincode/src/contracts/authenticate.eth.spec.ts @@ -130,71 +130,32 @@ const testFn = async ( expectation(response, userObj); }; -describe("regular flow", () => { - test.each([ - [__valid___, _________, ___registered, Success], - [__valid___, _________, notRegistered, Error("USER_NOT_REGISTERED")], - [__valid___, signerKey, ___registered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")], - [__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], - [__valid___, signerKey, notRegistered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")], - [__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], - [__valid___, signerAdd, ___registered, Error("REDUNDANT_SIGNER_ADDRESS")], - [__valid___, _wrongAdd, ___registered, Error("ADDRESS_MISMATCH")], - [__valid___, signerAdd, notRegistered, Error("REDUNDANT_SIGNER_ADDRESS")], - [__valid___, _wrongAdd, notRegistered, Error("ADDRESS_MISMATCH")], - [__validDER, _________, ___registered, Error("MISSING_SIGNER")], - [__validDER, _________, notRegistered, Error("MISSING_SIGNER")], - [__validDER, signerKey, ___registered, Success], - [__validDER, _wrongKey, ___registered, Error("PK_INVALID_SIGNATURE")], - [__validDER, signerKey, notRegistered, Error("USER_NOT_REGISTERED")], - [__validDER, signerAdd, ___registered, Success], - [__validDER, _wrongAdd, ___registered, Error("USER_NOT_REGISTERED")], - [__validDER, signerAdd, notRegistered, Error("USER_NOT_REGISTERED")], - [invalid___, _________, ___registered, Error("USER_NOT_REGISTERED")], // tries to get other user's profile - [invalid___, _________, notRegistered, Error("USER_NOT_REGISTERED")], - [invalid___, signerKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], - [invalid___, signerKey, notRegistered, Error("PUBLIC_KEY_MISMATCH")], - [invalid___, signerAdd, ___registered, Error("ADDRESS_MISMATCH")], - [invalid___, signerAdd, notRegistered, Error("ADDRESS_MISMATCH")] - ])("(sig: %s, dto: %s, user: %s) => %s", testFn); -}); - -describe("allowNonRegisteredUsers enabled", () => { - beforeAll(() => { - process.env.ALLOW_NON_REGISTERED_USERS = "true"; - }); - - afterAll(() => { - delete process.env.ALLOW_NON_REGISTERED_USERS; - }); - - test.each([ - [__valid___, _________, ___registered, Success], - [__valid___, _________, notRegistered, SuccessNoCustomAlias], - [__valid___, signerKey, ___registered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")], - [__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], - [__valid___, signerKey, notRegistered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")], - [__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], - [__valid___, signerAdd, ___registered, Error("REDUNDANT_SIGNER_ADDRESS")], - [__valid___, _wrongAdd, ___registered, Error("ADDRESS_MISMATCH")], - [__valid___, signerAdd, notRegistered, Error("REDUNDANT_SIGNER_ADDRESS")], - [__valid___, _wrongAdd, notRegistered, Error("ADDRESS_MISMATCH")], - [__validDER, _________, ___registered, Error("MISSING_SIGNER")], - [__validDER, _________, notRegistered, Error("MISSING_SIGNER")], - [__validDER, signerKey, ___registered, Success], - [__validDER, _wrongKey, ___registered, Error("PK_INVALID_SIGNATURE")], - [__validDER, signerKey, notRegistered, SuccessNoCustomAlias], - [__validDER, signerAdd, ___registered, Success], - [__validDER, _wrongAdd, ___registered, Error("USER_NOT_REGISTERED")], - [__validDER, signerAdd, notRegistered, Error("USER_NOT_REGISTERED")], - [invalid___, _________, ___registered, SuccessUnknownKey], // it's just recovered to a different key - [invalid___, _________, notRegistered, SuccessUnknownKey], // it's just recovered to a different key - [invalid___, signerKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], - [invalid___, signerKey, notRegistered, Error("PUBLIC_KEY_MISMATCH")], - [invalid___, signerAdd, ___registered, Error("ADDRESS_MISMATCH")], - [invalid___, signerAdd, notRegistered, Error("ADDRESS_MISMATCH")] - ])("(sig: %s, dto: %s, user: %s) => %s", testFn); -}); +test.each([ + [__valid___, _________, ___registered, Success], + [__valid___, _________, notRegistered, SuccessNoCustomAlias], + [__valid___, signerKey, ___registered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")], + [__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], + [__valid___, signerKey, notRegistered, Error("REDUNDANT_SIGNER_PUBLIC_KEY")], + [__valid___, _wrongKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], + [__valid___, signerAdd, ___registered, Error("REDUNDANT_SIGNER_ADDRESS")], + [__valid___, _wrongAdd, ___registered, Error("ADDRESS_MISMATCH")], + [__valid___, signerAdd, notRegistered, Error("REDUNDANT_SIGNER_ADDRESS")], + [__valid___, _wrongAdd, notRegistered, Error("ADDRESS_MISMATCH")], + [__validDER, _________, ___registered, Error("MISSING_SIGNER")], + [__validDER, _________, notRegistered, Error("MISSING_SIGNER")], + [__validDER, signerKey, ___registered, Success], + [__validDER, _wrongKey, ___registered, Error("PK_INVALID_SIGNATURE")], + [__validDER, signerKey, notRegistered, SuccessNoCustomAlias], + [__validDER, signerAdd, ___registered, Success], + [__validDER, _wrongAdd, ___registered, Error("USER_NOT_REGISTERED")], + [__validDER, signerAdd, notRegistered, Error("USER_NOT_REGISTERED")], + [invalid___, _________, ___registered, SuccessUnknownKey], // it's just recovered to a different key + [invalid___, _________, notRegistered, SuccessUnknownKey], // it's just recovered to a different key + [invalid___, signerKey, ___registered, Error("PUBLIC_KEY_MISMATCH")], + [invalid___, signerKey, notRegistered, Error("PUBLIC_KEY_MISMATCH")], + [invalid___, signerAdd, ___registered, Error("ADDRESS_MISMATCH")], + [invalid___, signerAdd, notRegistered, Error("ADDRESS_MISMATCH")] +])("(sig: %s, dto: %s, user: %s) => %s", testFn); interface User { alias: string; diff --git a/chaincode/src/contracts/authenticate.testutils.spec.ts b/chaincode/src/contracts/authenticate.testutils.spec.ts index 4f86f303ec..bea1b077d4 100644 --- a/chaincode/src/contracts/authenticate.testutils.spec.ts +++ b/chaincode/src/contracts/authenticate.testutils.spec.ts @@ -53,6 +53,13 @@ export async function createUser(): Promise { return { alias: name, privateKey, publicKey, ethAddress }; } +export async function createEthUser(): Promise { + const { privateKey, publicKey } = signatures.genKeyPair(); + const ethAddress = signatures.getEthAddress(publicKey); + const alias = `eth|${ethAddress}` as UserAlias; + return { alias, privateKey, publicKey, ethAddress }; +} + export async function createRegisteredUser(chaincode: TestChaincode): Promise { const { alias, privateKey, publicKey, ethAddress } = await createUser(); const dto = await createValidSubmitDTO(RegisterUserDto, { user: alias, publicKey }); @@ -97,8 +104,8 @@ export async function createRegisteredMultiSigUser( export async function createTonUser(): Promise { const pair = await signatures.ton.genKeyPair(); - const privateKey = Buffer.from(pair.secretKey).toString("base64"); - const publicKey = Buffer.from(pair.publicKey).toString("base64"); + const privateKey = pair.secretKey.toString("base64"); + const publicKey = pair.publicKey.toString("base64"); const tonAddress = signatures.ton.getTonAddress(pair.publicKey); const alias = `ton|${tonAddress}` as UserAlias; return { alias, privateKey, publicKey, tonAddress }; diff --git a/chaincode/src/contracts/authenticate.ton.spec.ts b/chaincode/src/contracts/authenticate.ton.spec.ts index e15bd00666..bcee4c2df3 100644 --- a/chaincode/src/contracts/authenticate.ton.spec.ts +++ b/chaincode/src/contracts/authenticate.ton.spec.ts @@ -72,12 +72,12 @@ const Error: (errorKey: string) => Expectation = (errorKey) => test.each([ [__valid, _________, Error("MISSING_SIGNER")], [__valid, signerKey, Success], - [__valid, signerRef, Success], - [__valid, signerAdd, Success], + [__valid, signerRef, Error("USER_NOT_REGISTERED")], + [__valid, signerAdd, Error("USER_NOT_REGISTERED")], [invalid, _________, Error("MISSING_SIGNER")], [invalid, signerKey, Error("PK_INVALID_SIGNATURE")], - [invalid, signerRef, Error("PK_INVALID_SIGNATURE")], - [invalid, signerAdd, Error("PK_INVALID_SIGNATURE")] + [invalid, signerRef, Error("USER_NOT_REGISTERED")], + [invalid, signerAdd, Error("USER_NOT_REGISTERED")] ])("(%s, %s) => %s", performTest); async function performTest(signatureFn: Signature, publicKeyFn: PublicKey, expectation: Expectation) { diff --git a/chaincode/src/contracts/authenticate.ts b/chaincode/src/contracts/authenticate.ts index 44c69d5dee..c3e86b6452 100644 --- a/chaincode/src/contracts/authenticate.ts +++ b/chaincode/src/contracts/authenticate.ts @@ -289,11 +289,7 @@ async function getUserProfile( return profile; } - if (ctx.config.allowNonRegisteredUsers) { - return PublicKeyService.getDefaultUserProfile(publicKey, signing); - } - - throw new UserNotRegisteredError(address); + return PublicKeyService.getDefaultUserProfile(address, signing); } async function getUserProfileAndPublicKey( diff --git a/chaincode/src/services/PublicKeyService.ts b/chaincode/src/services/PublicKeyService.ts index a6a2caca48..a393eae8fa 100644 --- a/chaincode/src/services/PublicKeyService.ts +++ b/chaincode/src/services/PublicKeyService.ts @@ -185,8 +185,7 @@ export class PublicKeyService { return pk; } - public static getDefaultUserProfile(publicKey: string, signing: SigningScheme): UserProfileStrict { - const address = this.getUserAddress(publicKey, signing); + public static getDefaultUserProfile(address: string, signing: SigningScheme): UserProfileStrict { const profile = new UserProfile(); profile.alias = asValidUserAlias(`${signing.toLowerCase()}|${address}`); profile.ethAddress = signing === SigningScheme.ETH ? address : undefined; @@ -357,22 +356,13 @@ export class PublicKeyService { } const currentPublicKeyObj = await PublicKeyService.getPublicKey(ctx, userAlias); - if (currentPublicKeyObj === undefined) { - throw new PkNotFoundError(userAlias); - } - if (currentPublicKeyObj.publicKey === undefined) { + if (currentPublicKeyObj && currentPublicKeyObj.publicKey === undefined) { throw new NotImplementedError("UpdatePublicKey for multisig is not supported"); } - const currentSigning = currentPublicKeyObj.signing ?? SigningScheme.ETH; - if (currentSigning !== signing) { - const msg = `Current public key signing scheme ${currentSigning} does not match new signing scheme ${signing}`; - throw new ValidationFailedError(msg); - } - // need to fetch userProfile from old address - const oldAddress = PublicKeyService.getUserAddress(currentPublicKeyObj.publicKey, signing); + const oldAddress = ctx.callingUserAddress.address; const userProfile = await PublicKeyService.getUserProfile(ctx, oldAddress); const signatureQuorum = userProfile?.signatureQuorum ?? 1; @@ -414,14 +404,22 @@ export class PublicKeyService { roles: string[] ): Promise { const publicKey = await PublicKeyService.getPublicKey(ctx, user); + const allowedUnregisteredUsers = user.startsWith("eth|") || user.startsWith("ton|"); const address = publicKey ? PublicKeyService.getUserAddress(publicKey.publicKey, publicKey.signing ?? SigningScheme.ETH) - : user; + : allowedUnregisteredUsers + ? user.slice(4) + : user; - const userProfile = await PublicKeyService.getUserProfile(ctx, address); + let userProfile = await PublicKeyService.getUserProfile(ctx, address); if (userProfile === undefined) { - throw new UserProfileNotFoundError(user); + if (allowedUnregisteredUsers) { + const signing = user.startsWith("eth|") ? SigningScheme.ETH : SigningScheme.TON; + userProfile = PublicKeyService.getDefaultUserProfile(address, signing); + } else { + throw new UserProfileNotFoundError(user); + } } const currentRolesSet = new Set(userProfile.roles); diff --git a/chaincode/src/types/GalaChainContext.ts b/chaincode/src/types/GalaChainContext.ts index 8273179881..e56b9db99e 100644 --- a/chaincode/src/types/GalaChainContext.ts +++ b/chaincode/src/types/GalaChainContext.ts @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { UnauthorizedError, UserAlias, UserProfile, UserRole } from "@gala-chain/api"; +import { SigningScheme, UnauthorizedError, UserAlias, UserProfile, UserRole } from "@gala-chain/api"; import { Context } from "fabric-contract-api"; import { ChaincodeStub, Timestamp } from "fabric-shim"; @@ -29,7 +29,6 @@ function getTxUnixTime(ctx: Context): number { export interface GalaChainContextConfig { readonly adminPublicKey?: string; - readonly allowNonRegisteredUsers?: boolean; } class GalaChainContextConfigImpl implements GalaChainContextConfig { @@ -38,10 +37,6 @@ class GalaChainContextConfigImpl implements GalaChainContextConfig { get adminPublicKey(): string | undefined { return this.config.adminPublicKey ?? process.env.DEV_ADMIN_PUBLIC_KEY; } - - get allowNonRegisteredUsers(): boolean | undefined { - return this.config.allowNonRegisteredUsers ?? process.env.ALLOW_NON_REGISTERED_USERS === "true"; - } } export class GalaChainContext extends Context { @@ -83,18 +78,14 @@ export class GalaChainContext extends Context { return this.callingUserValue; } - get callingUserEthAddress(): string { - if (this.callingUserEthAddressValue === undefined) { - throw new UnauthorizedError(`No ETH address known for user ${this.callingUserValue}`); + get callingUserAddress(): { address: string; signing: SigningScheme } { + if (this.callingUserEthAddressValue !== undefined) { + return { address: this.callingUserEthAddressValue, signing: SigningScheme.ETH }; } - return this.callingUserEthAddressValue; - } - - get callingUserTonAddress(): string { - if (this.callingUserTonAddressValue === undefined) { - throw new UnauthorizedError(`No TON address known for user ${this.callingUserValue}`); + if (this.callingUserTonAddressValue !== undefined) { + return { address: this.callingUserTonAddressValue, signing: SigningScheme.TON }; } - return this.callingUserTonAddressValue; + throw new UnauthorizedError(`No address known for user ${this.callingUserValue}`); } get callingUserRoles(): string[] { diff --git a/docs/authorization.md b/docs/authorization.md index 01fa901150..e9c6f38ebf 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -229,16 +229,8 @@ During registration, you can specify multiple signers using the `signers` field: const { publicKey: pk1, privateKey: sk1 } = signatures.genKeyPair(); const { publicKey: pk2, privateKey: sk2 } = signatures.genKeyPair(); -// Register user1 and user2 first (may be skipped if you allow non-registered users) -await pkContract.RegisterEthUser(createValidSubmitDTO(RegisterEthUserDto, { - publicKey: pk1 -}).signed(adminKey)); - -await pkContract.RegisterEthUser(createValidSubmitDTO(RegisterEthUserDto, { - publicKey: pk2 -}).signed(adminKey)); - // Register multisig user with signers +// Note: Individual signers do not need to be registered separately const reg = await createValidSubmitDTO(RegisterUserDto, { user: "client|multisig", signers: [pk1, pk2].map((k) => asValidUserRef(signatures.getEthAddress(k)), @@ -325,18 +317,12 @@ This feature is supported only for Ethereum signing scheme (secp256k1) with non- **Example 1: Corporate Treasury Setup** ```typescript -// Register individual signers first +// Generate keys for signers const treasuryKeys = Array.from({ length: 5 }, () => signatures.genKeyPair()); const signerRefs = treasuryKeys.map((k) => asValidUserRef(signatures.getEthAddress(k.publicKey))); -// Register each signer -for (let i = 0; i < treasuryKeys.length; i++) { - await pkContract.RegisterEthUser(createValidSubmitDTO(RegisterEthUserDto, { - publicKey: treasuryKeys[i].publicKey - }).signed(adminKey)); -} - // Register corporate treasury with 5 signers requiring 3 signatures +// Note: Individual signers do not need to be registered separately const treasuryRegistration = await createValidSubmitDTO(RegisterUserDto, { user: "client|treasury", signers: signerRefs, @@ -387,19 +373,16 @@ async emergencyAction(ctx: GalaChainContext, dto: EmergencyActionDto): Promise` or `ton|` respectively. +This method requires the user to provide their public key (secp256k1 for Ethereum, ed25519 for TON) and allows you to specify a custom `alias` parameter. + +**Note**: The `RegisterEthUser` and `RegisterTonUser` methods are deprecated and will be removed in a future version. Registration of `eth|` and `ton|` users is no longer required - you can use `RegisterUser` with the appropriate alias format (`eth|` or `ton|`) if needed. Access to registration methods is now controlled as follows: - **Role-based authorization (RBAC)**: Requires the `REGISTRAR` role @@ -551,30 +534,3 @@ If you want to call external chaincode and authorize your call as a chaincode: Remember to allow the chaincode in `allowedOriginChaincodes` transaction property in the target chaincode. -## Optional User Registration - -By default, GalaChain requires every user to be registered before they can interact with the chaincode. However, in scenarios with a large number of users, you might want to make user registration optional. - -You can allow non-registered users to access the chaincode in one of two ways: - -1. **Environment Variable**: Set the `ALLOW_NON_REGISTERED_USERS` environment variable to `true` for the chaincode container. - - ```bash - ALLOW_NON_REGISTERED_USERS=true - ``` - -2. **Contract Configuration**: Set the `allowNonRegisteredUsers` property to `true` in your contract's configuration. - - ```typescript - class MyContract extends GalaContract { - constructor() { - super("MyContract", version, { - allowNonRegisteredUsers: true - }); - } - } - ``` - -When non-registered users are allowed, they still need to sign the DTO with their private key. The public key is recovered from the signature. In this case, the user's alias will be `eth|` or `ton|`, and they will be granted default `EVALUATE` and `SUBMIT` roles. - -Registration remains necessary if you need to assign custom aliases or roles to users. From 598953b4a00b3e41d9bb62799342d04a0c6716d3 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Fri, 14 Nov 2025 13:36:33 +0100 Subject: [PATCH 3/9] Invalidate non-saved user profiles Signed-off-by: Jakub Dzikowski --- .../src/contracts/PublicKeyContract.spec.ts | 25 +++++++++++++++++++ chaincode/src/services/PublicKeyService.ts | 6 ++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/chaincode/src/contracts/PublicKeyContract.spec.ts b/chaincode/src/contracts/PublicKeyContract.spec.ts index 4a23cec370..bb75815654 100644 --- a/chaincode/src/contracts/PublicKeyContract.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.spec.ts @@ -519,6 +519,11 @@ describe("UpdatePublicKey", () => { const user = await createTonUser(); const newPair = await signatures.ton.genKeyPair(); const newPrivateKeyBase64 = newPair.secretKey.toString("base64"); + const newAddress = PublicKeyService.getUserAddress( + newPair.publicKey.toString("base64"), + SigningScheme.TON + ); + const dto = ( await createValidSubmitDTO(UpdatePublicKeyDto, { publicKey: newPair.publicKey.toString("base64"), @@ -531,6 +536,7 @@ describe("UpdatePublicKey", () => { // no public key saved for this user expect(await getPublicKey(chaincode, user.alias)).toEqual(transactionErrorKey("PK_NOT_FOUND")); + expect(await getUserProfile(chaincode, user.tonAddress)).toEqual(transactionErrorKey("OBJECT_NOT_FOUND")); // When const response = await chaincode.invoke("PublicKeyContract:UpdatePublicKey", dto); @@ -544,6 +550,25 @@ describe("UpdatePublicKey", () => { signing: SigningScheme.TON }) ); + + // new profile is saved + expect(await getUserProfile(chaincode, newAddress)).toEqual( + transactionSuccess({ + alias: user.alias, + tonAddress: newAddress, + roles: UserProfile.DEFAULT_ROLES, + signatureQuorum: 1 + }) + ); + + // old non-existent profile is invalidated + expect(await getUserProfile(chaincode, user.tonAddress)).toEqual( + transactionSuccess({ + alias: "client|invalidated", + ethAddress: "0000000000000000000000000000000000000000", + roles: [] + }) + ); }); it("should prevent from changing key type", async () => { diff --git a/chaincode/src/services/PublicKeyService.ts b/chaincode/src/services/PublicKeyService.ts index a393eae8fa..f9ffae518b 100644 --- a/chaincode/src/services/PublicKeyService.ts +++ b/chaincode/src/services/PublicKeyService.ts @@ -37,7 +37,6 @@ import { PkExistsError, PkInvalidSignatureError, PkMissingError, - PkNotFoundError, ProfileExistsError, UserProfileNotFoundError } from "./PublicKeyError"; @@ -367,9 +366,8 @@ export class PublicKeyService { const signatureQuorum = userProfile?.signatureQuorum ?? 1; // invalidate old user profile to prevent double registration under old public key - if (userProfile !== undefined) { - await PublicKeyService.invalidateUserProfile(ctx, oldAddress); - } + // do it also for unregistered users (no check if profile exists) + await PublicKeyService.invalidateUserProfile(ctx, oldAddress); // ensure no user profile exists under new address const newAddress = PublicKeyService.getUserAddress(newPublicKey, signing); From 532303bf522574fab81a388ba6707e2c9f1c8faa Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Mon, 15 Dec 2025 15:31:06 +0100 Subject: [PATCH 4/9] Test fixes Signed-off-by: Jakub Dzikowski --- .../e2e/__snapshots__/api.spec.ts.snap | 82 +++++++++++++++++-- .../chaincode-template/e2e/apples.spec.ts | 4 +- .../chaincode-template/e2e/burnNFT.spec.ts | 4 +- .../chaincode-template/e2e/lockNFT.spec.ts | 8 +- .../chaincode-template/e2e/simpleNFT.spec.ts | 4 +- .../src/pk/__snapshots__/api.spec.ts.snap | 82 +++++++++++++++++-- chain-test/src/e2e/TestClients.ts | 4 +- chain-test/src/unit/fixture.ts | 3 +- .../__test__/MockedChaincodeClient.spec.ts | 2 - chaincode/src/contracts/GalaContract.ts | 8 +- .../src/contracts/PublicKeyContract.spec.ts | 26 ------ chaincode/src/contracts/PublicKeyContract.ts | 1 - .../PublicKeyContract.spec.ts.snap | 74 +++++++++++++++++ chaincode/src/contracts/authenticate.ts | 2 +- chaincode/src/services/PublicKeyService.ts | 9 +- chaincode/src/types/GalaChainContext.ts | 8 +- 16 files changed, 246 insertions(+), 75 deletions(-) diff --git a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap index c0faa4dd30..2b2f7ea3b2 100644 --- a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap @@ -12194,9 +12194,9 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Registers a new user on chain under alias derived from eth address. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", + "deprecated": true, + "description": "Registration of eth| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { - "description": "Dto for secure method to save public keys for Eth users. Method is called and signed by Curators", "properties": { "dtoExpiresAt": { "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", @@ -12218,8 +12218,77 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "publicKey": { - "description": "Public secp256k1 key (compact or non-compact, hex or base64).", + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "type": "object", + }, + "isWrite": true, + "methodName": "RegisterEthUser", + "responseSchema": { + "properties": { + "Data": { + "type": "string", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "deprecated": true, + "description": "Registration of ton| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", + "dtoSchema": { + "properties": { + "dtoExpiresAt": { + "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' or 'prefix' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", "minLength": 1, "type": "string", }, @@ -12246,13 +12315,10 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "string", }, }, - "required": [ - "publicKey", - ], "type": "object", }, "isWrite": true, - "methodName": "RegisterEthUser", + "methodName": "RegisterTonUser", "responseSchema": { "properties": { "Data": { diff --git a/chain-cli/chaincode-template/e2e/apples.spec.ts b/chain-cli/chaincode-template/e2e/apples.spec.ts index c50ae6696c..dad8fc1d40 100644 --- a/chain-cli/chaincode-template/e2e/apples.spec.ts +++ b/chain-cli/chaincode-template/e2e/apples.spec.ts @@ -53,8 +53,8 @@ describe("Apple trees", () => { beforeAll(async () => { client = await TestClients.createForAdmin(appleContractConfig); - user = await client.createRegisteredUser(); - user2 = await client.createRegisteredUser(); + user = ChainUser.withRandomKeys(); + user2 = ChainUser.withRandomKeys(); }); afterAll(async () => { diff --git a/chain-cli/chaincode-template/e2e/burnNFT.spec.ts b/chain-cli/chaincode-template/e2e/burnNFT.spec.ts index 538afab6b8..98191bc00e 100644 --- a/chain-cli/chaincode-template/e2e/burnNFT.spec.ts +++ b/chain-cli/chaincode-template/e2e/burnNFT.spec.ts @@ -49,8 +49,8 @@ describe("NFT Burn scenario", () => { beforeAll(async () => { client = await TestClients.createForAdmin(); - user1 = await client.createRegisteredUser(); - user2 = await client.createRegisteredUser(); + user1 = ChainUser.withRandomKeys(); + user2 = ChainUser.withRandomKeys(); await mintTokensToUsers(client.assets, nftClassKey, [ { user: user1, quantity: new BigNumber(1) }, diff --git a/chain-cli/chaincode-template/e2e/lockNFT.spec.ts b/chain-cli/chaincode-template/e2e/lockNFT.spec.ts index efb92aabda..4402301a12 100644 --- a/chain-cli/chaincode-template/e2e/lockNFT.spec.ts +++ b/chain-cli/chaincode-template/e2e/lockNFT.spec.ts @@ -55,8 +55,8 @@ describe("NFT lock scenario", () => { beforeAll(async () => { client = await TestClients.createForAdmin(); - user1 = await client.createRegisteredUser(); - user2 = await client.createRegisteredUser(); + user1 = ChainUser.withRandomKeys(); + user2 = ChainUser.withRandomKeys(); await mintTokensToUsers(client.assets, nftClassKey, [ { user: user1, quantity: new BigNumber(2) }, @@ -207,8 +207,8 @@ describe("lock with allowances", () => { beforeAll(async () => { client = await TestClients.createForAdmin(); - user1 = await client.createRegisteredUser(); - user2 = await client.createRegisteredUser(); + user1 = ChainUser.withRandomKeys(); + user2 = ChainUser.withRandomKeys(); await mintTokensToUsers(client.assets, nftClassKey, [ { user: user1, quantity: new BigNumber(2) }, diff --git a/chain-cli/chaincode-template/e2e/simpleNFT.spec.ts b/chain-cli/chaincode-template/e2e/simpleNFT.spec.ts index 53971da122..118cc75eda 100644 --- a/chain-cli/chaincode-template/e2e/simpleNFT.spec.ts +++ b/chain-cli/chaincode-template/e2e/simpleNFT.spec.ts @@ -47,8 +47,8 @@ describe("Simple NFT scenario", () => { beforeAll(async () => { client = await TestClients.createForAdmin(); - user1 = await client.createRegisteredUser(); - user2 = await client.createRegisteredUser(); + user1 = ChainUser.withRandomKeys(); + user2 = ChainUser.withRandomKeys(); }); afterAll(async () => { diff --git a/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap index 51fcbb1aee..acd00d0e74 100644 --- a/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap @@ -1005,9 +1005,9 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Registers a new user on chain under alias derived from eth address. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", + "deprecated": true, + "description": "Registration of eth| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { - "description": "Dto for secure method to save public keys for Eth users. Method is called and signed by Curators", "properties": { "dtoExpiresAt": { "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", @@ -1029,8 +1029,77 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "publicKey": { - "description": "Public secp256k1 key (compact or non-compact, hex or base64).", + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "type": "object", + }, + "isWrite": true, + "methodName": "RegisterEthUser", + "responseSchema": { + "properties": { + "Data": { + "type": "string", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "deprecated": true, + "description": "Registration of ton| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", + "dtoSchema": { + "properties": { + "dtoExpiresAt": { + "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' or 'prefix' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", "minLength": 1, "type": "string", }, @@ -1057,13 +1126,10 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "string", }, }, - "required": [ - "publicKey", - ], "type": "object", }, "isWrite": true, - "methodName": "RegisterEthUser", + "methodName": "RegisterTonUser", "responseSchema": { "properties": { "Data": { diff --git a/chain-test/src/e2e/TestClients.ts b/chain-test/src/e2e/TestClients.ts index 39ff5de527..ba560d27da 100644 --- a/chain-test/src/e2e/TestClients.ts +++ b/chain-test/src/e2e/TestClients.ts @@ -239,8 +239,8 @@ async function create( * @example * ```typescript * const adminClients = await TestClients.createForAdmin(); - * const user1 = await adminClients.createRegisteredUser(); - * const user2 = await adminClients.createRegisteredUser("alice"); + * const user1 = ChainUser.withRandomKeys(); + * const user2 = ChainUser.withRandomKeys("alice"); * ``` */ export type AdminChainClients = ChainClients< diff --git a/chain-test/src/unit/fixture.ts b/chain-test/src/unit/fixture.ts index b5cdaf7b38..f44e5747a3 100644 --- a/chain-test/src/unit/fixture.ts +++ b/chain-test/src/unit/fixture.ts @@ -18,7 +18,6 @@ import { ClassConstructor, GalaChainResponse, RangedChainObject, - SigningScheme, UserAlias, UserProfile, signatures @@ -127,7 +126,7 @@ type TestGalaChainContext = Context & { readonly logger: GalaLoggerInstance; set callingUserData(d: CallingUserData); get callingUser(): UserAlias; - get callingUserAddress(): { address: string; signing: SigningScheme }; + get callingUserAddress(): string; get callingUserRoles(): string[]; get callingUserSignedBy(): UserAlias[]; get callingUserSignatureQuorum(): number; diff --git a/chaincode/src/__test__/MockedChaincodeClient.spec.ts b/chaincode/src/__test__/MockedChaincodeClient.spec.ts index 9f9529ef4f..569fa480a7 100644 --- a/chaincode/src/__test__/MockedChaincodeClient.spec.ts +++ b/chaincode/src/__test__/MockedChaincodeClient.spec.ts @@ -17,8 +17,6 @@ import { GetPublicKeyDto, PublicKey, RegisterUserDto, - SigningScheme, - UserAlias, asValidUserAlias, createValidChainObject, createValidDTO, diff --git a/chaincode/src/contracts/GalaContract.ts b/chaincode/src/contracts/GalaContract.ts index 53afe7affb..a9cdb789cb 100644 --- a/chaincode/src/contracts/GalaContract.ts +++ b/chaincode/src/contracts/GalaContract.ts @@ -183,12 +183,8 @@ export abstract class GalaContract extends Contract { // If the caller public key is provided, we use it to set the dry run on behalf of the user. if (dto.callerPublicKey) { const ethAddr = signatures.getEthAddress(signatures.getNonCompactHexPublicKey(dto.callerPublicKey)); - const userProfile = await PublicKeyService.getUserProfile(ctx, ethAddr); - - if (!userProfile) { - throw new NotFoundError(`User profile for ${ethAddr} not found`); - } - + const savedProfile = await PublicKeyService.getUserProfile(ctx, ethAddr); + const userProfile = savedProfile ?? PublicKeyService.getDefaultUserProfile(ethAddr); ctx.setDryRunOnBehalfOf({ ...userProfile }); diff --git a/chaincode/src/contracts/PublicKeyContract.spec.ts b/chaincode/src/contracts/PublicKeyContract.spec.ts index e84e3ebffa..52cd2f8d90 100644 --- a/chaincode/src/contracts/PublicKeyContract.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.spec.ts @@ -18,7 +18,6 @@ import { GetMyProfileDto, GetPublicKeyDto, RegisterUserDto, - SigningScheme, SubmitCallDTO, UpdatePublicKeyDto, UpdateUserRolesDto, @@ -47,7 +46,6 @@ import { createRegisteredMultiSigUserForUsers, createRegisteredUser, createSignedDto, - createTonUser, createUser, getMyProfile, getPublicKey, @@ -763,30 +761,6 @@ describe("UpdateUserRoles", () => { expect(updatedUserProfile.Data?.roles).toContain("CUSTOM_ETH_ROLE"); }); - it("should allow registrar to update user roles for non-registered ton| user", async () => { - // Given - const chaincode = new TestChaincode([PublicKeyContract]); - const adminPrivateKey = process.env.DEV_ADMIN_PRIVATE_KEY as string; - - const user = await createTonUser(); - const userProfile = await getUserProfile(chaincode, user.tonAddress); - expect(userProfile.Data).toBeUndefined(); - - const dto = await createValidSubmitDTO(UpdateUserRolesDto, { - user: user.alias, - roles: ["CUSTOM_TON_ROLE"] - }).signed(adminPrivateKey); - - // When - const response = await chaincode.invoke("PublicKeyContract:UpdateUserRoles", dto); - - // Then - expect(response).toEqual(transactionSuccess()); - - const updatedUserProfile = await getUserProfile(chaincode, user.tonAddress); - expect(updatedUserProfile.Data?.roles).toContain("CUSTOM_TON_ROLE"); - }); - it("should not allow user to update roles if they do not have the registrar role", async () => { // Given const chaincode = new TestChaincode([PublicKeyContract]); diff --git a/chaincode/src/contracts/PublicKeyContract.ts b/chaincode/src/contracts/PublicKeyContract.ts index 34b9d04b36..761bc1cf60 100644 --- a/chaincode/src/contracts/PublicKeyContract.ts +++ b/chaincode/src/contracts/PublicKeyContract.ts @@ -20,7 +20,6 @@ import { PublicKey, RegisterUserDto, RemoveSignerDto, - SigningScheme, SubmitCallDTO, UpdatePublicKeyDto, UpdateQuorumDto, diff --git a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap index 38c28cb435..3e123e3954 100644 --- a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap +++ b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap @@ -1079,6 +1079,80 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "object", }, }, + { + "deprecated": true, + "description": "Registration of ton| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", + "dtoSchema": { + "properties": { + "dtoExpiresAt": { + "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' or 'prefix' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "type": "object", + }, + "isWrite": true, + "methodName": "RegisterTonUser", + "responseSchema": { + "properties": { + "Data": { + "type": "string", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, { "description": "Registers a new user on chain under provided user alias. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { diff --git a/chaincode/src/contracts/authenticate.ts b/chaincode/src/contracts/authenticate.ts index eb92fc3515..c724d95ec6 100644 --- a/chaincode/src/contracts/authenticate.ts +++ b/chaincode/src/contracts/authenticate.ts @@ -277,7 +277,7 @@ async function getUserProfile(ctx: GalaChainContext, publicKey: string): Promise return profile; } - return PublicKeyService.getDefaultUserProfile(address, signing); + return PublicKeyService.getDefaultUserProfile(address); } async function getUserProfileAndPublicKey( diff --git a/chaincode/src/services/PublicKeyService.ts b/chaincode/src/services/PublicKeyService.ts index c5b369635c..464f216a3a 100644 --- a/chaincode/src/services/PublicKeyService.ts +++ b/chaincode/src/services/PublicKeyService.ts @@ -171,7 +171,7 @@ export class PublicKeyService { return pk; } - public static getDefaultUserProfile(address: string, signing: SigningScheme): UserProfileStrict { + public static getDefaultUserProfile(address: string): UserProfileStrict { const profile = new UserProfile(); profile.alias = asValidUserAlias(`eth|${address}`); profile.ethAddress = address; @@ -320,7 +320,7 @@ export class PublicKeyService { } // need to fetch userProfile from old address - const oldAddress = ctx.callingUserAddress.address; + const oldAddress = ctx.callingUserAddress; const userProfile = await PublicKeyService.getUserProfile(ctx, oldAddress); const signatureQuorum = userProfile?.signatureQuorum ?? 1; @@ -360,7 +360,7 @@ export class PublicKeyService { const allowedUnregisteredUsers = user.startsWith("eth|") || user.startsWith("ton|"); const address = publicKey - ? PublicKeyService.getUserAddress(publicKey.publicKey, publicKey.signing ?? SigningScheme.ETH) + ? PublicKeyService.getUserAddress(publicKey.publicKey) : allowedUnregisteredUsers ? user.slice(4) : user; @@ -368,8 +368,7 @@ export class PublicKeyService { let userProfile = await PublicKeyService.getUserProfile(ctx, address); if (userProfile === undefined) { if (allowedUnregisteredUsers) { - const signing = user.startsWith("eth|") ? SigningScheme.ETH : SigningScheme.TON; - userProfile = PublicKeyService.getDefaultUserProfile(address, signing); + userProfile = PublicKeyService.getDefaultUserProfile(address); } else { throw new UserProfileNotFoundError(user); } diff --git a/chaincode/src/types/GalaChainContext.ts b/chaincode/src/types/GalaChainContext.ts index 5416571f75..9c8f8a286c 100644 --- a/chaincode/src/types/GalaChainContext.ts +++ b/chaincode/src/types/GalaChainContext.ts @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SigningScheme, UnauthorizedError, UserAlias, UserProfile, UserRole } from "@gala-chain/api"; +import { UnauthorizedError, UserAlias, UserProfile, UserRole } from "@gala-chain/api"; import { Context } from "fabric-contract-api"; import { ChaincodeStub, Timestamp } from "fabric-shim"; @@ -77,11 +77,11 @@ export class GalaChainContext extends Context { return this.callingUserValue; } - get callingUserAddress(): { address: string; signing: SigningScheme } { + get callingUserAddress(): string { if (this.callingUserEthAddressValue !== undefined) { - return { address: this.callingUserEthAddressValue, signing: SigningScheme.ETH }; + return this.callingUserEthAddressValue; } - return this.callingUserEthAddressValue; + throw new UnauthorizedError(`No address known for user ${this.callingUserValue}`); } get callingUserRoles(): string[] { From cc09eb33b87613cec802f3668851c1d26e634da1 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Mon, 15 Dec 2025 16:22:25 +0100 Subject: [PATCH 5/9] Fix tests Signed-off-by: Jakub Dzikowski --- .../e2e/__snapshots__/api.spec.ts.snap | 74 ------------------- .../src/pk/__snapshots__/api.spec.ts.snap | 74 ------------------- chaincode/src/contracts/PublicKeyContract.ts | 13 ---- .../PublicKeyContract.spec.ts.snap | 74 ------------------- chaincode/src/services/PublicKeyService.ts | 25 ++++++- 5 files changed, 21 insertions(+), 239 deletions(-) diff --git a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap index 2b2f7ea3b2..1d5b202bca 100644 --- a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap @@ -12267,80 +12267,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "object", }, }, - { - "deprecated": true, - "description": "Registration of ton| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", - "dtoSchema": { - "properties": { - "dtoExpiresAt": { - "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", - "type": "number", - }, - "dtoOperation": { - "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", - "minLength": 1, - "type": "string", - }, - "multisig": { - "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' or 'prefix' fields, and the signing scheme must be ETH.", - "items": {}, - "minItems": 2, - "type": "array", - }, - "prefix": { - "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", - "minLength": 1, - "type": "string", - }, - "signature": { - "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. -Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", - "minLength": 1, - "type": "string", - }, - "signerAddress": { - "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", - "minLength": 1, - "type": "string", - }, - "signerPublicKey": { - "description": "Public key of the user who signed the DTO.", - "minLength": 1, - "type": "string", - }, - "uniqueKey": { - "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. -The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", - "minLength": 1, - "type": "string", - }, - }, - "type": "object", - }, - "isWrite": true, - "methodName": "RegisterTonUser", - "responseSchema": { - "properties": { - "Data": { - "type": "string", - }, - "Message": { - "type": "string", - }, - "Status": { - "description": "Indicates Error (0) or Success (1)", - "enum": [ - 0, - 1, - ], - }, - }, - "required": [ - "Status", - ], - "type": "object", - }, - }, { "description": "Registers a new user on chain under provided user alias. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { diff --git a/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap index acd00d0e74..8f524cc1b5 100644 --- a/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap @@ -1078,80 +1078,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "object", }, }, - { - "deprecated": true, - "description": "Registration of ton| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", - "dtoSchema": { - "properties": { - "dtoExpiresAt": { - "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", - "type": "number", - }, - "dtoOperation": { - "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", - "minLength": 1, - "type": "string", - }, - "multisig": { - "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' or 'prefix' fields, and the signing scheme must be ETH.", - "items": {}, - "minItems": 2, - "type": "array", - }, - "prefix": { - "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", - "minLength": 1, - "type": "string", - }, - "signature": { - "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. -Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", - "minLength": 1, - "type": "string", - }, - "signerAddress": { - "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", - "minLength": 1, - "type": "string", - }, - "signerPublicKey": { - "description": "Public key of the user who signed the DTO.", - "minLength": 1, - "type": "string", - }, - "uniqueKey": { - "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. -The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", - "minLength": 1, - "type": "string", - }, - }, - "type": "object", - }, - "isWrite": true, - "methodName": "RegisterTonUser", - "responseSchema": { - "properties": { - "Data": { - "type": "string", - }, - "Message": { - "type": "string", - }, - "Status": { - "description": "Indicates Error (0) or Success (1)", - "enum": [ - 0, - 1, - ], - }, - }, - "required": [ - "Status", - ], - "type": "object", - }, - }, { "description": "Registers a new user on chain under provided user alias. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { diff --git a/chaincode/src/contracts/PublicKeyContract.ts b/chaincode/src/contracts/PublicKeyContract.ts index 761bc1cf60..29e1de1d5f 100644 --- a/chaincode/src/contracts/PublicKeyContract.ts +++ b/chaincode/src/contracts/PublicKeyContract.ts @@ -107,19 +107,6 @@ export class PublicKeyContract extends GalaContract { return "Registration of eth| users is no longer required."; } - @Submit({ - in: SubmitCallDTO, - out: "string", - description: - "Registration of ton| users is no longer required. This method will be removed in the future.", - deprecated: true, - ...requireRegistrarAuth - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async RegisterTonUser(ctx: GalaChainContext, dto: SubmitCallDTO): Promise { - return "Registration of ton| users is no longer required."; - } - @Submit({ in: UpdateUserRolesDto, description: "Updates roles for the user with alias provided in DTO.", diff --git a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap index 3e123e3954..38c28cb435 100644 --- a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap +++ b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap @@ -1079,80 +1079,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "object", }, }, - { - "deprecated": true, - "description": "Registration of ton| users is no longer required. This method will be removed in the future. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", - "dtoSchema": { - "properties": { - "dtoExpiresAt": { - "description": "Unit timestamp when the DTO expires. If the timestamp is in the past, the DTO is not valid.", - "type": "number", - }, - "dtoOperation": { - "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", - "minLength": 1, - "type": "string", - }, - "multisig": { - "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' or 'prefix' fields, and the signing scheme must be ETH.", - "items": {}, - "minItems": 2, - "type": "array", - }, - "prefix": { - "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", - "minLength": 1, - "type": "string", - }, - "signature": { - "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. -Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", - "minLength": 1, - "type": "string", - }, - "signerAddress": { - "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", - "minLength": 1, - "type": "string", - }, - "signerPublicKey": { - "description": "Public key of the user who signed the DTO.", - "minLength": 1, - "type": "string", - }, - "uniqueKey": { - "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. -The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", - "minLength": 1, - "type": "string", - }, - }, - "type": "object", - }, - "isWrite": true, - "methodName": "RegisterTonUser", - "responseSchema": { - "properties": { - "Data": { - "type": "string", - }, - "Message": { - "type": "string", - }, - "Status": { - "description": "Indicates Error (0) or Success (1)", - "enum": [ - 0, - 1, - ], - }, - }, - "required": [ - "Status", - ], - "type": "object", - }, - }, { "description": "Registers a new user on chain under provided user alias. Transaction updates the chain (submit). Allowed roles: REGISTRAR.", "dtoSchema": { diff --git a/chaincode/src/services/PublicKeyService.ts b/chaincode/src/services/PublicKeyService.ts index 464f216a3a..7a1457d4ea 100644 --- a/chaincode/src/services/PublicKeyService.ts +++ b/chaincode/src/services/PublicKeyService.ts @@ -36,6 +36,7 @@ import { PkExistsError, PkInvalidSignatureError, PkMissingError, + PkNotFoundError, ProfileExistsError, UserProfileNotFoundError } from "./PublicKeyError"; @@ -314,13 +315,29 @@ export class PublicKeyService { } const currentPublicKeyObj = await PublicKeyService.getPublicKey(ctx, userAlias); + const oldAddress = ctx.callingUserAddress; + + // If public key exists, verify ownership by checking address match + if (currentPublicKeyObj !== undefined) { + if (currentPublicKeyObj.publicKey === undefined) { + throw new NotImplementedError("UpdatePublicKey for multisig is not supported"); + } - if (currentPublicKeyObj && currentPublicKeyObj.publicKey === undefined) { - throw new NotImplementedError("UpdatePublicKey for multisig is not supported"); + // Verify ownership: ensure the address derived from the stored public key matches the calling user's address + const storedAddress = PublicKeyService.getUserAddress(currentPublicKeyObj.publicKey); + if (storedAddress !== oldAddress) { + throw new UnauthorizedError( + `Public key address mismatch: stored public key maps to ${storedAddress} but signature-derived address is ${oldAddress}` + ); + } + } else { + // No public key exists - only allow creation for eth| users + if (!userAlias.startsWith("eth|")) { + throw new PkNotFoundError(userAlias); + } } // need to fetch userProfile from old address - const oldAddress = ctx.callingUserAddress; const userProfile = await PublicKeyService.getUserProfile(ctx, oldAddress); const signatureQuorum = userProfile?.signatureQuorum ?? 1; @@ -357,7 +374,7 @@ export class PublicKeyService { roles: string[] ): Promise { const publicKey = await PublicKeyService.getPublicKey(ctx, user); - const allowedUnregisteredUsers = user.startsWith("eth|") || user.startsWith("ton|"); + const allowedUnregisteredUsers = user.startsWith("eth|"); const address = publicKey ? PublicKeyService.getUserAddress(publicKey.publicKey) From 090d4f596c01ea4c5d1546ca79b1eb4ea4e4cc00 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Mon, 15 Dec 2025 16:29:15 +0100 Subject: [PATCH 6/9] Update docs Signed-off-by: Jakub Dzikowski --- chain-test/src/e2e/TestClients.ts | 2 +- docs/authorization.md | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/chain-test/src/e2e/TestClients.ts b/chain-test/src/e2e/TestClients.ts index ba560d27da..4779b5a185 100644 --- a/chain-test/src/e2e/TestClients.ts +++ b/chain-test/src/e2e/TestClients.ts @@ -240,7 +240,7 @@ async function create( * ```typescript * const adminClients = await TestClients.createForAdmin(); * const user1 = ChainUser.withRandomKeys(); - * const user2 = ChainUser.withRandomKeys("alice"); + * const user2 = await adminClients.createRegisteredUser("alice"); * ``` */ export type AdminChainClients = ChainClients< diff --git a/docs/authorization.md b/docs/authorization.md index 4f0f61f855..82c10386cc 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -330,16 +330,12 @@ async emergencyAction(ctx: GalaChainContext, dto: EmergencyActionDto): Promise` or `ton|`) if needed. +**Note**: The `RegisterEthUser` method is deprecated and will be removed in a future version. Registration of `eth|` users is no longer needed. Access to registration methods is now controlled as follows: - **Role-based authorization (RBAC)**: Requires the `REGISTRAR` role From 3a52cf58404e716e5d0269ee9cdd73f8c98869b2 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Mon, 15 Dec 2025 16:37:38 +0100 Subject: [PATCH 7/9] Fix test Signed-off-by: Jakub Dzikowski --- chain-cli/chaincode-template/e2e/gcclient.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chain-cli/chaincode-template/e2e/gcclient.spec.ts b/chain-cli/chaincode-template/e2e/gcclient.spec.ts index b897b92d53..18d67064ec 100644 --- a/chain-cli/chaincode-template/e2e/gcclient.spec.ts +++ b/chain-cli/chaincode-template/e2e/gcclient.spec.ts @@ -19,7 +19,7 @@ import { GalaChainResponse, GetMyProfileDto, PublicKeyContractAPI, - RegisterEthUserDto, + RegisterUserDto, UserProfile, publicKeyContractAPI, randomUniqueKey, @@ -43,7 +43,7 @@ jest.setTimeout(30000); const describeIfNonMockedChaincode = process.env.CURATORORG_MOCKED_CHAINCODE_DIR === undefined ? describe : describe.skip; -describeIfNonMockedChaincode("Chaincode client (PartnerOrg1)", () => { +describe("Chaincode client (PartnerOrg1)", () => { let client: ChainClient & CustomAPI; beforeAll(() => { @@ -110,15 +110,15 @@ describeIfNonMockedChaincode("Chaincode client (CuratorOrg)", () => { it("should register another user", async () => { // Given - const newUser = ChainUser.withRandomKeys(); + const newUser = ChainUser.withRandomKeys("new-user"); - const dto = new RegisterEthUserDto(); + const dto = new RegisterUserDto(); dto.publicKey = newUser.publicKey; dto.uniqueKey = randomUniqueKey(); dto.sign(getAdminPrivateKey(), false); // When - const response = await client.RegisterEthUser(dto); + const response = await client.RegisterUser(dto); // Then expect(response).toEqual(GalaChainResponse.Success(newUser.identityKey)); From 3a8d69b6b7365df79ada9a681d49eb8f9c5ceb26 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Mon, 12 Jan 2026 16:45:42 +0100 Subject: [PATCH 8/9] Fix failing test Signed-off-by: Jakub Dzikowski --- chain-cli/chaincode-template/e2e/gcclient.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain-cli/chaincode-template/e2e/gcclient.spec.ts b/chain-cli/chaincode-template/e2e/gcclient.spec.ts index 18d67064ec..776c07a531 100644 --- a/chain-cli/chaincode-template/e2e/gcclient.spec.ts +++ b/chain-cli/chaincode-template/e2e/gcclient.spec.ts @@ -43,7 +43,7 @@ jest.setTimeout(30000); const describeIfNonMockedChaincode = process.env.CURATORORG_MOCKED_CHAINCODE_DIR === undefined ? describe : describe.skip; -describe("Chaincode client (PartnerOrg1)", () => { +describeIfNonMockedChaincode("Chaincode client (PartnerOrg1)", () => { let client: ChainClient & CustomAPI; beforeAll(() => { From 05a7ba492f8865583081f56e8116707e5e02fff1 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Tue, 13 Jan 2026 13:56:10 +0100 Subject: [PATCH 9/9] Fix registration test Signed-off-by: Jakub Dzikowski --- chain-cli/chaincode-template/e2e/gcclient.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/chain-cli/chaincode-template/e2e/gcclient.spec.ts b/chain-cli/chaincode-template/e2e/gcclient.spec.ts index 776c07a531..826f3876c9 100644 --- a/chain-cli/chaincode-template/e2e/gcclient.spec.ts +++ b/chain-cli/chaincode-template/e2e/gcclient.spec.ts @@ -21,6 +21,7 @@ import { PublicKeyContractAPI, RegisterUserDto, UserProfile, + createValidSubmitDTO, publicKeyContractAPI, randomUniqueKey, signatures @@ -112,10 +113,10 @@ describeIfNonMockedChaincode("Chaincode client (CuratorOrg)", () => { // Given const newUser = ChainUser.withRandomKeys("new-user"); - const dto = new RegisterUserDto(); - dto.publicKey = newUser.publicKey; - dto.uniqueKey = randomUniqueKey(); - dto.sign(getAdminPrivateKey(), false); + const dto = await createValidSubmitDTO(RegisterUserDto, { + user: newUser.identityKey, + publicKey: newUser.publicKey + }).signed(getAdminPrivateKey()); // When const response = await client.RegisterUser(dto);