JSSDK 0.5.6 & @phala/fn 0.2.7: Worker Select Strategy & Factory Functions

2023-11-26

The JS-SDK 0.5.6 and @phala/fn 0.2.7 have been released to fix the worker select issue for mainnet and include a few improvements.

For the JS-SDK:

  • Improved Worker Select Strategy.
  • Factory function getClient and getContract .
  • Added alice getter to OnChainRegistry .
  • Use getter for PinkBlueprintSubmittableResult.contractId for more predicable behavior.
  • Fixed creatPruntimeClient throws unexpected Error .
  • Improved type inference on PinkCodePromise and signAndSend .
💡
We will migrate to polkadot-js 10.11 in JS-SDK 0.6.0, which the minimal node.js version bumps to v18.x.

For the @phala/fn:

  • Change the --location directive to --directory.
  • Added --outputDir directive that specified the output directory for build command.
  • Added --silient directive.
  • Added --clean directive, which made build command clean the output directory before building.
  • Include @phala/pink-env in default TypeScript configuration.
  • The minimal requirement for Node.js bumped to v18.x

Worker Select Strategy

In previous versions, we did not implement any built-in worker select strategy and only selected the first one found in the cluster. This approach causes some issues:

  • The first worker is always busy while the other workers have nothing to do.
  • The OnChainRegistry.create will fail if the first worker is broken, unless you specify a specific healthy worker.

It brings confusing when you use JS-SDK for the first time, especially most of us may not understand the concept of an offchain worker node at first.

So that’s what we included the new version: worker select strategy. Starting from v0.5.6, OnChainRegistry will use the worker select strategy names ack-first , Choose the first worker with the fastest response time in your location, and they must be available in the chain.

It is simple and it just works, thanks Promise.any !

However, it still has some drawbacks:

  • It need extra times to find a worker. If you scripting with JS-SDK, it will need extra network roundtrips for OnChainRegistry.create, which may unnecessary.
  • The additional network roundtrips increase the likelihood of failure due to network issues.
  • The strategy is too simple and does not check the workers' health. For example, the worker may still connectivity but it already fall behind,It does not have the same block height as the blockchain.

So we also have another built-in strategy: periodicityChecker . This strategy will pulling the worker list here, and randomly picks one from it. This worker list will update every 5 minutes (thanks GitHub Actions!), ensuring each worker in the list is responsive and doesn’t fall far behind the finalize block head number on chain.

Here is how to use it:

import { periodicityChecker, OnChainRegistry, options } from '@phala/sdk'
import { ApiPromise, WsProvider } from '@polkadot/api'

const apiPromise = await ApiPromise.create(options({
	provider: new WsProvder('wss://poc6.phala.network/ws'),
	noInitWarn: true
}))
const phatRegistry = await OnChainRegistry.create(apiPromise, {
	strategy: periodicityChecker()
})

Let's provide another example of how to customize the select strategy. We using the built-in fixture strategy as example here. This strategy requires a list of worker endpoint URLs as an argument and randomly selects one from it.

import { fixture, OnChainRegistry, options } from '@phala/sdk'
import { ApiPromise, WsProvider } from '@polkadot/api'

const apiPromise = await ApiPromise.create(options({
	provider: new WsProvder('wss://poc6.phala.network/ws'),
	noInitWarn: true
}))
const phatRegistry = await OnChainRegistry.create(apiPromise, {
	strategy: fixture([
		"https://phat-cluster-ca.phala.network/pruntime/0x4e4e139e",
		"https://phat-cluster-ca.phala.network/pruntime/0x46f31a5d",
		"https://phat-cluster-ca.phala.network/pruntime/0x847b1e54",
	])
})

And the implementation of fixture strategy is simple, so you can create your own load balancing algorithm:

import { type ApiPromise } from '@polkadot/api'
import createPruntimeClient from '../pruntime/createPruntimeClient'

export function fixture(endpoints: string[]) {
  return async function (
    _apiPromise: ApiPromise,
    _clusterId: string
  ): Promise<Readonly<[string, string, ReturnType<typeof createPruntimeClient>]>> {
    if (endpoints.length === 0) {
      throw new Error('No worker available.')
    }
    const picked = endpoints[Math.floor(Math.random() * endpoints.length)]
    const client = createPruntimeClient(picked)
    const info = await client.getInfo({})
    return [`0x${info.ecdhPublicKey || ''}`, picked, client] as const
  }
}

Two new functions: getClient and getContract

Honestly, the JS-SDK is still hard to use and requires a lot of boilerplate codes to get started. We are working on simplifying it, but it is not yet finished. Starting from 0.5.6, we have introduced two new functions: getClient and getContract. Talk is cheap, let’s dive into the codes.

These are the steps we take with the previous version:

import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'
import { options, OnChainRegistry, signCertificate, PinkContractPromise } from '@phala/sdk'

const contractId = '0x...'
const abi = fs.readFileSync('path/to/your/abi.json')

const api = await ApiPromise.create(
  options({
    provider: new WsProvider('wss://poc6.phala.network/ws'),
    noInitWarn: true,
  })
)
const phatRegistry = await OnChainRegistry.create(api)
const contractKey = await phatRegistry.getContractKeyOrFail(contractId)
const contract = new PinkContractPromise(api, phatRegistry, abi, contractId, contractKey)

Inspired by viem, now it can be reduce to just two lines:

import { getClient, getContract } from '@phala/sdk'

const contractId = '0x...'
const abi = fs.readFileSync('path/to/your/abi.json')

const client = await getClient({ transport: 'wss://poc6.phala.network/ws' })
const contract = await getContract({ client, contractId, abi })

Or, if you need customized the worker select strategy:

import { getClient, getContract, periodicityChecker } from '@phala/sdk'

const contractId = '0x...'
const abi = fs.readFileSync('path/to/your/abi.json')

const client = await getClient({
	transport: 'wss://poc6.phala.network/ws',
	strategy: periodicityChecker(),
})
const contract = await getContract({ client, contractId, abi })

Using polkadot-js directly is complex, and there is much more to come.

We are still working on improving the JS-SDK and lowering the barrier to interact with Phat Contract, building the new web3 infrastructure with Phala Blockchain. If you want to build a new Web3 application with Phat Contract, we would love to hear about it. Join our Discord channel and starts talking with us!

About Phala

Phala Network is a decentralized cloud that offers secure and scalable computing for Web3.

With Phat Contracts, an innovative programming model enabling trustless off-chain computation, developers can create new Web3 use cases.

Get the latest Phala Content Straight To Your Inbox.