Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6be6998
Baseline super-good-enough cert
Kubuxu Nov 12, 2025
374acc3
stub decodeEndorsements
wjmelements Nov 12, 2025
5a4ec65
preferEndorsements
wjmelements Nov 12, 2025
8ce3807
start decoding endorsements
Kubuxu Nov 13, 2025
ea1c8f8
notAfter as timesetamp
wjmelements Nov 13, 2025
b2538d1
encodeEndorsements
wjmelements Nov 13, 2025
174a7e8
mv cert.ts to utils
wjmelements Nov 14, 2025
0959768
utils export from cert.ts
wjmelements Nov 14, 2025
8fdb6bf
fix test
wjmelements Nov 14, 2025
67af7c0
test that createContexts prefers endorsements
wjmelements Nov 14, 2025
a34808c
encode/decode certs with domain separator
wjmelements Nov 14, 2025
953f4c8
add cert test
wjmelements Nov 15, 2025
c4ea7c7
Merge remote-tracking branch 'origin/master' into feat/super-good-eno…
wjmelements Nov 15, 2025
e5236ad
add tools/endorse-sp.js
wjmelements Nov 15, 2025
ebebd81
increase timeout for CI
wjmelements Nov 15, 2025
344d9d2
make the decodeEndorsements a bit simpler
Kubuxu Nov 15, 2025
c81971a
add more tests
Kubuxu Nov 15, 2025
7c784b7
Update packages/synapse-core/src/utils/cert.ts
wjmelements Nov 16, 2025
cdcf6d4
rm sinon
wjmelements Nov 17, 2025
7c58dd8
Update packages/synapse-core/src/utils/cert.ts
wjmelements Nov 17, 2025
6874d4a
cleanup
wjmelements Nov 17, 2025
77a4524
Update packages/synapse-core/src/utils/cert.ts
wjmelements Nov 17, 2025
1318554
Merge remote-tracking branch 'origin/master' into feat/endorsement-ce…
wjmelements Nov 25, 2025
f1a66ef
disallow undefined chainId in decodeEndorsement
wjmelements Nov 25, 2025
72047d7
accept any of the preferred endorsements
wjmelements Nov 26, 2025
57c44a7
Merge remote-tracking branch 'origin/master' into feat/endorsement-ce…
wjmelements Jan 13, 2026
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
12 changes: 12 additions & 0 deletions packages/synapse-core/src/mocks/jsonrpc/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export const ADDRESSES = {
},
}

const ENDORSEMENTS = {
'0x50724807600e804Fe842439860D5b62baa26aFff': {
notAfter: 0xffffffffn,
nonce: 0xffffffffn,
signature:
'0x1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b',
},
} as const

export const PROVIDERS = {
providerNoPDP: {
providerId: 1n,
Expand Down Expand Up @@ -64,6 +73,7 @@ export const PROVIDERS = {
minProvingPeriodInEpochs: 30n,
location: 'us-east',
paymentTokenAddress: ADDRESSES.calibration.usdfcToken,
endorsements: ENDORSEMENTS,
},
},
],
Expand Down Expand Up @@ -91,6 +101,7 @@ export const PROVIDERS = {
minProvingPeriodInEpochs: 30n,
location: 'us-east',
paymentTokenAddress: ADDRESSES.calibration.usdfcToken,
endorsements: ENDORSEMENTS,
},
},
],
Expand Down Expand Up @@ -118,6 +129,7 @@ export const PROVIDERS = {
minProvingPeriodInEpochs: 30n,
location: 'us-east',
paymentTokenAddress: ADDRESSES.calibration.usdfcToken,
endorsements: ENDORSEMENTS,
},
},
],
Expand Down
151 changes: 151 additions & 0 deletions packages/synapse-core/src/utils/cert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { TypedDataToPrimitiveTypes } from 'abitype'
import type { Account, Address, Chain, Client, Hex, Transport } from 'viem'
import { bytesToBigInt, bytesToHex, concat, hexToBytes, numberToHex, recoverTypedDataAddress } from 'viem'
import { signTypedData } from 'viem/actions'
import { randU256 } from '../utils/rand.ts'

export type Endorsement = {
/**
* Unique nonce to suport nonce based revocation.
*/
nonce: bigint
/**
* This certificate becomes invalid after `notAfter` timestamp.
*/
notAfter: bigint
}

export type SignedEndorsement = Endorsement & {
signature: Hex
}

export const EIP712Endorsement = {
Endorsement: [
{ name: 'nonce', type: 'uint64' },
{ name: 'notAfter', type: 'uint64' },
{ name: 'providerId', type: 'uint256' },
],
} as const

export type TypedEn = TypedDataToPrimitiveTypes<typeof EIP712Endorsement>['Endorsement']

export type SignCertOptions = {
nonce?: bigint // uint64
notAfter: bigint // uint64
providerId: bigint
}

/**
* Signs an endorsement certificate for a specific provider
* @param client - The client to use to sign the message
* @param options - nonce (randomised if null), not after and who to sign it for
* @returns encoded certificate data abiEncodePacked([nonce, notAfter, signature]), the provider id is implicit by where it will get placed in registry.
*/
export async function signEndorsement(client: Client<Transport, Chain, Account>, options: SignCertOptions) {
const nonce = (options.nonce ?? randU256()) & 0xffffffffffffffffn
const signature = await signTypedData(client, {
account: client.account,
domain: {
name: 'Storage Endorsement',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be a const

version: '1',
chainId: client.chain.id,
},
types: EIP712Endorsement,
primaryType: 'Endorsement',
message: {
nonce: nonce,
notAfter: options.notAfter,
providerId: options.providerId,
},
})

const encodedNonce = numberToHex(nonce, { size: 8 })
const encodedNotAfter = numberToHex(options.notAfter, { size: 8 })

return concat([encodedNonce, encodedNotAfter, signature])
}

export async function decodeEndorsement(
providerId: bigint,
chainId: number | bigint,
hexData: Hex
): Promise<{
address: Address | null
endorsement: SignedEndorsement
}> {
if (hexData.length !== 164) {
return {
address: null,
endorsement: {
nonce: 0n,
notAfter: 0n,
signature: '0x',
},
}
}
Comment on lines +76 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just throw err?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to reduce use of try/catch for expected situations because it can mask unexpected situations

const data = hexToBytes(hexData)
const endorsement: SignedEndorsement = {
nonce: bytesToBigInt(data.slice(0, 8)),
notAfter: bytesToBigInt(data.slice(8, 16)),
signature: bytesToHex(data.slice(16)),
}
const address = await recoverTypedDataAddress({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into why this was async (in principle it should not be). It is only because of this async import:

export async function recoverPublicKey({
  hash,
  signature,
}: RecoverPublicKeyParameters): Promise<RecoverPublicKeyReturnType> {
  const hashHex = isHex(hash) ? hash : toHex(hash)

  const { secp256k1 } = await import('@noble/curves/secp256k1')

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, that's nasty, but oh well

domain: {
name: 'Storage Endorsement',
version: '1',
chainId,
},
types: EIP712Endorsement,
primaryType: 'Endorsement',
message: {
nonce: endorsement.nonce,
notAfter: endorsement.notAfter,
providerId: providerId,
},
signature: endorsement.signature,
}).catch(() => {
return null
})
return { address, endorsement }
}

/**
* Validates endorsement capabilities, if any, filtering out invalid ones
* @returns mapping of valid endorsements to expiry, nonce, signature
*/
export async function decodeEndorsements(
providerId: bigint,
chainId: number | bigint,
capabilities: Record<string, Hex>
): Promise<Record<Address, SignedEndorsement>> {
const now = Date.now() / 1000
const result: Record<Address, SignedEndorsement> = {}

for (const hexData of Object.values(capabilities)) {
try {
const { address, endorsement } = await decodeEndorsement(providerId, chainId, hexData)
if (address && endorsement.notAfter > now) {
result[address] = endorsement
}
} catch {
// Skip invalid endorsements
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this catch now

Copy link
Contributor Author

@wjmelements wjmelements Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would need to make sure that signatures that fail to recover don't throw. Should have a test case covering this in case that behavior changes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 looks to me like a failure here would be of the fatal kind now since you're doing so much in decodeEndorsement

}

return result
}

/**
* @returns a list of capability keys and a list of capability values for the ServiceProviderRegistry
*/
export function encodeEndorsements(endorsements: Record<Address, SignedEndorsement>): [string[], Hex[]] {
const keys: string[] = []
const values: Hex[] = []
Object.values(endorsements).forEach((value, index) => {
keys.push(`endorsement${index.toString()}`)
values.push(
concat([numberToHex(value.nonce, { size: 8 }), numberToHex(value.notAfter, { size: 8 }), value.signature])
)
})
return [keys, values]
}
1 change: 1 addition & 0 deletions packages/synapse-core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './calibration.ts'
export * from './capabilities.ts'
export * from './cert.ts'
export * from './constants.ts'
export * from './decode-pdp-errors.ts'
export * from './format.ts'
Expand Down
14 changes: 13 additions & 1 deletion packages/synapse-core/src/utils/pdp-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Hex } from 'viem'
import { bytesToHex, hexToString, isHex, numberToBytes, stringToHex, toBytes } from 'viem'
import type { PDPOffering } from '../warm-storage/providers.ts'
import { decodeAddressCapability } from './capabilities.ts'
import { decodeEndorsements, encodeEndorsements } from './cert.ts'

// Standard capability keys for PDP product type (must match ServiceProviderRegistry.sol REQUIRED_PDP_KEYS)
export const CAP_SERVICE_URL = 'serviceURL'
Expand All @@ -18,7 +19,11 @@ export const CAP_PAYMENT_TOKEN = 'paymentTokenAddress'
* Decode PDP capabilities from keys/values arrays into a PDPOffering object.
* Based on Curio's capabilitiesToOffering function.
*/
export function decodePDPCapabilities(capabilities: Record<string, Hex>): PDPOffering {
export async function decodePDPCapabilities(
providerId: bigint,
chainId: number | bigint,
capabilities: Record<string, Hex>
): Promise<PDPOffering> {
return {
serviceURL: hexToString(capabilities.serviceURL),
minPieceSizeInBytes: BigInt(capabilities.minPieceSizeInBytes),
Expand All @@ -29,6 +34,7 @@ export function decodePDPCapabilities(capabilities: Record<string, Hex>): PDPOff
minProvingPeriodInEpochs: BigInt(capabilities.minProvingPeriodInEpochs),
location: hexToString(capabilities.location),
paymentTokenAddress: decodeAddressCapability(capabilities.paymentTokenAddress),
endorsements: await decodeEndorsements(providerId, chainId, capabilities),
}
}

Expand Down Expand Up @@ -62,6 +68,12 @@ export function encodePDPCapabilities(
capabilityKeys.push(CAP_PAYMENT_TOKEN)
capabilityValues.push(pdpOffering.paymentTokenAddress)

if (pdpOffering.endorsements != null) {
const [endorsementKeys, endorsementValues] = encodeEndorsements(pdpOffering.endorsements)
capabilityKeys.push(...endorsementKeys)
capabilityValues.push(...endorsementValues)
}

if (capabilities != null) {
for (const [key, value] of Object.entries(capabilities)) {
capabilityKeys.push(key)
Expand Down
8 changes: 6 additions & 2 deletions packages/synapse-core/src/warm-storage/data-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ export async function getDataSets(client: Client<Transport, Chain>, options: Get
],
})
// getProviderWithProduct returns {providerId, providerInfo, product, productCapabilityValues}
const pdpCaps = decodePDPCapabilities(
const pdpCaps = await decodePDPCapabilities(
dataSet.providerId,
client.chain.id,
capabilitiesListToObject(pdpOffering.product.capabilityKeys, pdpOffering.productCapabilityValues)
)

Expand Down Expand Up @@ -178,7 +180,9 @@ export async function getDataSet(client: Client<Transport, Chain>, options: GetD
})

// getProviderWithProduct returns {providerId, providerInfo, product, productCapabilityValues}
const pdpCaps = decodePDPCapabilities(
const pdpCaps = await decodePDPCapabilities(
dataSet.providerId,
client.chain.id,
capabilitiesListToObject(pdpOffering.product.capabilityKeys, pdpOffering.productCapabilityValues)
)

Expand Down
12 changes: 9 additions & 3 deletions packages/synapse-core/src/warm-storage/providers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { AbiParametersToPrimitiveTypes, ExtractAbiFunction } from 'abitype'
import type { Chain, Client, Hex, Transport } from 'viem'
import type { Address, Chain, Client, Hex, Transport } from 'viem'
import { readContract } from 'viem/actions'
import type * as Abis from '../abis/index.ts'
import { getChain } from '../chains.ts'
import { capabilitiesListToObject } from '../utils/capabilities.ts'
import type { SignedEndorsement } from '../utils/cert.ts'
import { decodePDPCapabilities } from '../utils/pdp-capabilities.ts'

export type getProviderType = ExtractAbiFunction<typeof Abis.serviceProviderRegistry, 'getProvider'>
Expand All @@ -23,6 +24,7 @@ export interface PDPOffering {
minProvingPeriodInEpochs: bigint
location: string
paymentTokenAddress: Hex
endorsements?: Record<Address, SignedEndorsement>
}

export interface PDPProvider extends ServiceProviderInfo {
Expand Down Expand Up @@ -59,7 +61,9 @@ export async function readProviders(client: Client<Transport, Chain>): Promise<P
providers.push({
id: provider.providerId,
...provider.providerInfo,
pdp: decodePDPCapabilities(
pdp: await decodePDPCapabilities(
provider.providerId,
client.chain.id,
capabilitiesListToObject(provider.product.capabilityKeys, provider.productCapabilityValues)
),
})
Expand All @@ -83,7 +87,9 @@ export async function getProvider(client: Client<Transport, Chain>, options: Get
return {
id: provider.providerId,
...provider.providerInfo,
pdp: decodePDPCapabilities(
pdp: await decodePDPCapabilities(
provider.providerId,
client.chain.id,
capabilitiesListToObject(provider.product.capabilityKeys, provider.productCapabilityValues)
),
}
Expand Down
Loading
Loading