Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/big-clowns-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@celo/actions': minor
'@celo/core': minor
---

Add utilies for generating a proof-of-possession and parsingSignatures
5 changes: 5 additions & 0 deletions .changeset/odd-memes-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@celo/celocli': patch
---

Convert account:proof-of-posession and account:authorize to use viem instead of web3 based functions
32 changes: 32 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"@celo/actions": "0.2.0",
"@celo/celocli": "8.0.1",
"@celo/core": "0.0.1",
"@celo/dev-utils": "0.1.2",
"@celo/base": "7.0.3",
"@celo/connect": "7.0.0",
"@celo/contractkit": "10.0.2",
"@celo/cryptographic-utils": "6.0.0",
"@celo/explorer": "5.0.18",
"@celo/governance": "5.1.9",
"@celo/keystores": "5.0.16",
"@celo/metadata-claims": "1.0.4",
"@celo/phone-utils": "6.0.7",
"@celo/transactions-uri": "5.0.15",
"@celo/utils": "8.0.3",
"@celo/wallet-base": "8.0.2",
"@celo/wallet-hsm": "8.0.2",
"@celo/wallet-hsm-aws": "8.0.2",
"@celo/wallet-hsm-azure": "8.0.2",
"@celo/wallet-hsm-gcp": "8.0.2",
"@celo/wallet-ledger": "8.0.2",
"@celo/wallet-local": "8.0.2",
"@celo/wallet-remote": "8.0.2",
"@celo/typescript": "0.0.1",
"@celo/viem-account-ledger": "1.2.2"
},
"changesets": []
}
21 changes: 19 additions & 2 deletions packages/actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"types": "./dist/cjs/multicontract-interactions/stake/index.d.ts",
"import": "./dist/mjs/multicontract-interactions/stake/index.js",
"require": "./dist/cjs/multicontract-interactions/stake/index.js"
},
"./authorization": {
"types": "./dist/cjs/multicontract-interactions/authorize/index.d.ts",
"import": "./dist/mjs/multicontract-interactions/authorize/index.js",
"require": "./dist/cjs/multicontract-interactions/authorize/index.js"
}
},
"author": "cLabs",
Expand Down Expand Up @@ -73,12 +78,12 @@
{
"name": "require('@celo/actions') (cjs)",
"path": "dist/cjs/index.js",
"limit": "120 kB"
"limit": "200 kB"
},
{
"name": "import * from '@celo/actions' (esm)",
"path": "dist/mjs/index.js",
"limit": "25 kB",
"limit": "50 kB",
"import": "*"
},
{
Expand All @@ -99,11 +104,23 @@
"limit": "50 kB",
"import": "{ getAccountsContract }"
},
{
"name": "import { authorizeVoteSigner } from '@celo/actions/contracts/accounts' (esm)",
"path": "dist/mjs/contracts/accounts.js",
"limit": "50 kB",
"import": "{ authorizeVoteSigner }"
},
{
"name": "import * from '@celo/actions/staking' (esm)",
"path": "dist/mjs/multicontract-interactions/stake/index.js",
"limit": "60 kB",
"import": "*"
},
{
"name": "import * from '@celo/actions/authorization' (esm)",
"path": "dist/mjs/multicontract-interactions/authorize/index.js",
"limit": "40 kB",
"import": "*"
}
]
}
39 changes: 39 additions & 0 deletions packages/actions/src/contracts/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { accountsABI } from '@celo/abis'
import { Address, getContract, GetContractReturnType } from 'viem'
import { Clients, PublicCeloClient } from '../client.js'
import type { ProofOfPossession } from '../multicontract-interactions/authorize/proof-of-possession.js'
import { resolveAddress } from './registry.js'

export type AccountsContract<C extends Clients = Clients> = GetContractReturnType<
Expand Down Expand Up @@ -29,3 +30,41 @@ export const signerToAccount = async (
args: [signer],
})
}

// AUTHORIZATION FUNCTIONS

export const authorizeVoteSigner = async (
clients: Required<Clients>,
signer: Address,
proofOfSigningKeyPossession: ProofOfPossession
) => {
return clients.wallet.writeContract({
address: await resolveAddress(clients.public, 'Accounts'),
abi: accountsABI,
functionName: 'authorizeVoteSigner',
args: [
signer,
proofOfSigningKeyPossession.v,
proofOfSigningKeyPossession.r,
proofOfSigningKeyPossession.s,
],
})
}

export const authorizeValidatorSigner = async (
clients: Required<Clients>,
signer: Address,
proofOfSigningKeyPossession: ProofOfPossession
) => {
return clients.wallet.writeContract({
address: await resolveAddress(clients.public, 'Accounts'),
abi: accountsABI,
functionName: 'authorizeValidatorSigner',
args: [
signer,
proofOfSigningKeyPossession.v,
proofOfSigningKeyPossession.r,
proofOfSigningKeyPossession.s,
],
})
}
15 changes: 13 additions & 2 deletions packages/actions/src/contracts/validators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { validatorsABI } from '@celo/abis'
import { getContract, GetContractReturnType } from 'viem'
import { Clients } from '../client.js'
import { Address, getContract, GetContractReturnType } from 'viem'
import { Clients, PublicCeloClient } from '../client.js'
import { resolveAddress } from './registry.js'

export async function getValidatorsContract<T extends Clients = Clients>(
Expand All @@ -16,3 +16,14 @@ export type ValidatorsContract<T extends Clients = Clients> = GetContractReturnT
typeof validatorsABI,
T
>

// METHODS

export const isValidator = async (client: PublicCeloClient, account: Address): Promise<boolean> => {
return client.readContract({
address: await resolveAddress(client, 'Validators'),
abi: validatorsABI,
functionName: 'isValidator',
args: [account],
})
}
6 changes: 6 additions & 0 deletions packages/actions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export * from './client.js'
export { ContractName } from './contract-name.js'
export { resolveAddress } from './contracts/registry.js'
export { isValidator } from './contracts/validators.js'
export {
generateProofOfKeyPossession,
generateProofOfKeyPossessionLocally,
parseSignatureOfAddress,
} from './multicontract-interactions/authorize/proof-of-possession.js'
export { getGasPriceOnCelo } from './rpc-methods.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './proof-of-possession.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { serializeSignature } from '@celo/core'
import { privateKeyToAccount } from 'viem/accounts'
import { describe, expect, it } from 'vitest'
import {
generateProofOfKeyPossessionLocally,
parseSignatureOfAddress,
} from './proof-of-possession.js'

// Test constants
const TEST_PRIVATE_KEY = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
const TEST_SIGNER = privateKeyToAccount(TEST_PRIVATE_KEY).address
const TEST_ACCOUNT = TEST_SIGNER // Use the same address for proof-of-possession tests

describe('authorize proof-of-possession functions', () => {
// Note: wallet client tests are not included because anvil doesn't support personal_sign
// These tests focus on local signing which works independently

describe('generateProofOfKeyPossessionLocally', () => {
it('generates valid proof of possession signature locally', async () => {
const result = await generateProofOfKeyPossessionLocally(TEST_PRIVATE_KEY, TEST_ACCOUNT)

expect(result).toHaveProperty('v')
expect(result).toHaveProperty('r')
expect(result).toHaveProperty('s')
expect(typeof result.v).toBe('number')
expect([27, 28]).toContain(result.v)
expect(result.r).toMatch(/^0x[a-fA-F0-9]{64}$/)
expect(result.s).toMatch(/^0x[a-fA-F0-9]{64}$/)
})

it('works with different private keys', async () => {
const privateKey1 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
const privateKey2 = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'

// Use different account addresses to avoid signature validation errors
const account1 = privateKeyToAccount(privateKey1).address
const account2 = privateKeyToAccount(privateKey2).address

const result1 = await generateProofOfKeyPossessionLocally(privateKey1, account1)
const result2 = await generateProofOfKeyPossessionLocally(privateKey2, account2)

expect(serializeSignature(result1)).not.toBe(serializeSignature(result2))
})
})

describe('parseSignatureOfAddress', () => {
it('parses signature correctly', async () => {
// First generate a signature
const signature = await generateProofOfKeyPossessionLocally(TEST_PRIVATE_KEY, TEST_ACCOUNT)
const serializedSig = serializeSignature(signature)

// Then parse it
const parsed = await parseSignatureOfAddress(TEST_ACCOUNT, TEST_SIGNER, serializedSig)

expect(parsed.v).toBe(signature.v)
expect(parsed.r).toBe(signature.r)
expect(parsed.s).toBe(signature.s)
})

it('throws error for invalid signer', async () => {
// Generate a signature with one signer but try to parse with different expected signer
const signature = await generateProofOfKeyPossessionLocally(TEST_PRIVATE_KEY, TEST_ACCOUNT)
const serializedSig = serializeSignature(signature)

const wrongSigner = '0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb'

await expect(
parseSignatureOfAddress(TEST_ACCOUNT, wrongSigner, serializedSig)
).rejects.toThrow('Unable to parse signature')
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { parseSignature } from '@celo/core'
import { Account, Address, encodePacked, Hex, keccak256 } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { WalletCeloClient } from '../../client'

export type ProofOfPossession = {
v: number
r: Hex
s: Hex
}

// PROOF OF POSSESSION FUNCTIONS

export const generateProofOfKeyPossession = async (
client: WalletCeloClient,
account: Address,
signer: Account
): Promise<ProofOfPossession> => {
// Use the same hash generation as soliditySha3({ type: 'address', value: account })
const hash = keccak256(encodePacked(['address'], [account]))

const signature = await client.signMessage({
account: signer,
message: { raw: hash },
})
return parseSignature(hash, signature, signer.address)
}

export const generateProofOfKeyPossessionLocally = async (
privateKey: Hex,
account: Address
): Promise<ProofOfPossession> => {
const hash = keccak256(encodePacked(['address'], [account]))

// To match ContractKit behavior, we need to add Ethereum message prefix
// ContractKit passes the hash as a "message" to signMessage, which adds the prefix
const messageLength = 32 // hash is always 32 bytes
const prefix = `\x19Ethereum Signed Message:\n${messageLength}`
const prefixedHash = keccak256(encodePacked(['string', 'bytes'], [prefix, hash]))

const localAccount = privateKeyToAccount(privateKey)
const signature = await localAccount.sign({ hash: prefixedHash })
const signerAddress = localAccount.address

// Parse using the prefixed hash for validation
return parseSignature(prefixedHash, signature, signerAddress)
}
// For parsing existing signatures (equivalent to parseSignatureOfAddress)

export const parseSignatureOfAddress = (address: Address, signer: Address, signature: Hex) => {
const hash = keccak256(encodePacked(['address'], [address]))

// To match ContractKit behavior, use prefixed hash for parsing
const messageLength = 32 // hash is always 32 bytes
const prefix = `\x19Ethereum Signed Message:\n${messageLength}`
const prefixedHash = keccak256(encodePacked(['string', 'bytes'], [prefix, hash]))

return parseSignature(prefixedHash, signature, signer)
}
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"dependencies": {
"@celo/abis": "13.0.0-post-audit.0",
"@celo/core": "0.0.1",
"@celo/actions": "0.2.0",
"@celo/base": "^7.0.3",
"@celo/compliance": "1.0.28",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export abstract class BaseCommand extends Command {
// NOTE: adjust logic here later to take in account commands which
// don't use --from but --account or other flags to pass in which account
// should be used
const accountAddress = res.flags.from as StrongAddress
const accountAddress = (res.flags.from || res.flags.signer) as StrongAddress

if (res.flags.useLedger) {
try {
Expand Down
Loading
Loading