Skip to content

Commit

Permalink
Merge pull request #495 from opengsn/OG-237-acceptance-budget
Browse files Browse the repository at this point in the history
OG-237 acceptanceBalance protocol
  • Loading branch information
drortirosh authored Sep 23, 2020
2 parents 1cb39ea + 3478585 commit f999e6a
Show file tree
Hide file tree
Showing 14 changed files with 151 additions and 96 deletions.
6 changes: 3 additions & 3 deletions src/cli/CommandsLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default class CommandsLogic {

async isRelayReady (relayUrl: string): Promise<boolean> {
const response = await this.httpClient.getPingResponse(relayUrl)
return response.Ready
return response.ready
}

async waitForRelay (relayUrl: string): Promise<void> {
Expand Down Expand Up @@ -176,8 +176,8 @@ export default class CommandsLogic {
console.error(`Funding GSN relay at ${options.relayUrl}`)

const response = await this.httpClient.getPingResponse(options.relayUrl)
const relayAddress = response.RelayManagerAddress
const relayHubAddress = this.config.relayHubAddress ?? response.RelayHubAddress
const relayAddress = response.relayManagerAddress
const relayHubAddress = this.config.relayHubAddress ?? response.relayHubAddress

const relayHub = await this.contractInteractor._createRelayHub(relayHubAddress)
const stakeManagerAddress = await relayHub.stakeManager()
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/gsn-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const commander = gsnCommander(['n', 'h'])
res.push(`\tbalance: ${Web3.utils.fromWei(managerBalance)} ETH`)
}
const pingResult = statistics.relayPings.get(registeredEvent.relayUrl)
const status = pingResult?.pingResponse != null ? pingResult.pingResponse.Ready.toString() : pingResult?.error?.toString() ?? 'unknown'
const status = pingResult?.pingResponse != null ? pingResult.pingResponse.ready.toString() : pingResult?.error?.toString() ?? 'unknown'
res.push(`\tstatus: ${status}`)
console.log('- ' + res.join(' '))
})
Expand Down
17 changes: 9 additions & 8 deletions src/common/PingResponse.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Address, IntString } from '../relayclient/types/Aliases'

export default interface PingResponse {
// TODO: this should be 'worker'
RelayServerAddress: Address
RelayManagerAddress: Address
RelayHubAddress: Address
MinGasPrice: IntString
MaxAcceptanceBudget: IntString
Ready: boolean
Version: string
relayWorkerAddress: Address
relayManagerAddress: Address
relayHubAddress: Address
minGasPrice: IntString
maxAcceptanceBudget: IntString
networkId?: IntString
chainId?: IntString
ready: boolean
version: string
}
5 changes: 3 additions & 2 deletions src/relayclient/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export default class HttpClient {
this.config = config
}

async getPingResponse (relayUrl: string): Promise<PingResponse> {
const pingResponse: PingResponse = await this.httpWrapper.sendPromise(relayUrl + '/getaddr', {})
async getPingResponse (relayUrl: string, paymaster?: string): Promise<PingResponse> {
const paymasterSuffix = paymaster == null ? '' : '?paymaster=' + paymaster
const pingResponse: PingResponse = await this.httpWrapper.sendPromise(relayUrl + '/getaddr' + paymasterSuffix, {})
if (this.config.verbose) {
console.log('error, body', pingResponse)
}
Expand Down
8 changes: 4 additions & 4 deletions src/relayclient/RelayClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export const EmptyDataCallback: AsyncDataCallback = async (): Promise<PrefixedHe
export const GasPricePingFilter: PingFilter = (pingResponse, gsnTransactionDetails) => {
if (
gsnTransactionDetails.gasPrice != null &&
parseInt(pingResponse.MinGasPrice) > parseInt(gsnTransactionDetails.gasPrice)
parseInt(pingResponse.minGasPrice) > parseInt(gsnTransactionDetails.gasPrice)
) {
throw new Error(`Proposed gas price: ${gsnTransactionDetails.gasPrice}; relay's MinGasPrice: ${pingResponse.MinGasPrice}`)
throw new Error(`Proposed gas price: ${gsnTransactionDetails.gasPrice}; relay's MinGasPrice: ${pingResponse.minGasPrice}`)
}
}

Expand Down Expand Up @@ -218,7 +218,7 @@ export class RelayClient {
if (this.config.verbose) {
console.log(`attempting relay: ${JSON.stringify(relayInfo)} transaction: ${JSON.stringify(gsnTransactionDetails)}`)
}
const maxAcceptanceBudget = parseInt(relayInfo.pingResponse.MaxAcceptanceBudget)
const maxAcceptanceBudget = parseInt(relayInfo.pingResponse.maxAcceptanceBudget)
const httpRequest = await this._prepareRelayHttpRequest(relayInfo, gsnTransactionDetails)

this.emit(new GsnValidateRequestEvent())
Expand Down Expand Up @@ -267,7 +267,7 @@ export class RelayClient {
const paymaster = gsnTransactionDetails.paymaster ?? this.config.paymasterAddress

const senderNonce = await this.contractInteractor.getSenderNonce(gsnTransactionDetails.from, forwarderAddress)
const relayWorker = relayInfo.pingResponse.RelayServerAddress
const relayWorker = relayInfo.pingResponse.relayWorkerAddress
const gasPriceHex = gsnTransactionDetails.gasPrice
const gasLimitHex = gsnTransactionDetails.gas
if (gasPriceHex == null || gasLimitHex == null) {
Expand Down
6 changes: 3 additions & 3 deletions src/relayclient/RelaySelectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default class RelaySelectionManager {
if (isInfoFromEvent(raceResult.winner.relayInfo)) {
return (raceResult.winner as RelayInfo)
} else {
const managerAddress = raceResult.winner.pingResponse.RelayManagerAddress
const managerAddress = raceResult.winner.pingResponse.relayManagerAddress
if (this.config.verbose) {
console.log(`finding relay register info for manager address: ${managerAddress}; known info: ${JSON.stringify(raceResult.winner.relayInfo)}`)
}
Expand Down Expand Up @@ -118,9 +118,9 @@ export default class RelaySelectionManager {
if (this.config.verbose) {
console.log(`getRelayAddressPing URL: ${relayInfo.relayUrl}`)
}
const pingResponse = await this.httpClient.getPingResponse(relayInfo.relayUrl)
const pingResponse = await this.httpClient.getPingResponse(relayInfo.relayUrl, this.gsnTransactionDetails.paymaster)

if (!pingResponse.Ready) {
if (!pingResponse.ready) {
throw new Error(`Relay not ready ${JSON.stringify(pingResponse)}`)
}
this.pingFilter(pingResponse, this.gsnTransactionDetails)
Expand Down
4 changes: 2 additions & 2 deletions src/relayserver/HttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ export class HttpServer {
}

pingHandler (req: any, res: any): void {
const pingResponse = this.backend.pingHandler()
const pingResponse = this.backend.pingHandler(req.query.paymaster)
res.send(pingResponse)
console.log(`address ${pingResponse.RelayServerAddress} sent. ready: ${pingResponse.Ready}`)
console.log(`address ${pingResponse.relayWorkerAddress} sent. ready: ${pingResponse.ready}`)
}

async relayHandler (req: any, res: any): Promise<void> {
Expand Down
110 changes: 77 additions & 33 deletions src/relayserver/RelayServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ import { SendTransactionDetails, TransactionManager } from './TransactionManager
import { configureServer, ServerConfigParams, ServerDependencies } from './ServerConfigParams'
import { TxStoreManager } from './TxStoreManager'
import { ServerAction } from './StoredTransaction'
import { IntString } from '../relayclient/types/Aliases'
import Timeout = NodeJS.Timeout

const VERSION = '2.0.0-beta.2'
const VERSION = '2.0.0-beta.3'
const GAS_RESERVE = 100000

export class RelayServer extends EventEmitter {
Expand Down Expand Up @@ -56,6 +57,8 @@ export class RelayServer extends EventEmitter {
networkId!: number
relayHubContract!: IRelayHubInstance

trustedPaymastersGasLimits: Map<String|undefined, PaymasterGasLimits> = new Map<String|undefined, PaymasterGasLimits>()

constructor (config: Partial<ServerConfigParams>, dependencies: ServerDependencies) {
super()
this.versionManager = new VersionsManager(VERSION)
Expand All @@ -66,22 +69,24 @@ export class RelayServer extends EventEmitter {
this.managerAddress = this.transactionManager.managerKeyManager.getAddress(0)
this.workerAddress = this.transactionManager.workersKeyManager.getAddress(0)
log.setLevel(this.config.logLevel)
log.debug('config:', JSON.stringify(this.config))
log.debug('server config:', JSON.stringify(this.config, null, 2))
}

getMinGasPrice (): number {
return this.gasPrice
}

pingHandler (): PingResponse {
pingHandler (paymaster?: string): PingResponse {
return {
RelayServerAddress: this.workerAddress,
RelayManagerAddress: this.managerAddress,
RelayHubAddress: this.relayHubContract?.address ?? '',
MinGasPrice: this.getMinGasPrice().toString(),
MaxAcceptanceBudget: this.config.maxAcceptanceBudget.toString(),
Ready: this.ready,
Version: VERSION
relayWorkerAddress: this.workerAddress,
relayManagerAddress: this.managerAddress,
relayHubAddress: this.relayHubContract?.address ?? '',
minGasPrice: this.getMinGasPrice().toString(),
maxAcceptanceBudget: this._getPaymasterMaxAcceptanceBudget(paymaster),
chainId: this.chainId.toString(),
networkId: this.networkId.toString(),
ready: this.ready,
version: VERSION
}
}

Expand Down Expand Up @@ -135,29 +140,36 @@ export class RelayServer extends EventEmitter {
maxPossibleGas: number
acceptanceBudget: number
}> {
let gasLimits: PaymasterGasLimits
try {
const paymasterContract = await this.contractInteractor._createPaymaster(req.relayRequest.relayData.paymaster)
gasLimits = await paymasterContract.getGasLimits()
} catch (e) {
const error = e as Error
let message = `unknown paymaster error: ${error.message}`
if (error.message.includes('Returned values aren\'t valid, did it run Out of Gas?')) {
message = `incompatible paymaster contract: ${req.relayRequest.relayData.paymaster}`
} else if (error.message.includes('no code at address')) {
message = `'non-existent paymaster contract: ${req.relayRequest.relayData.paymaster}`
const paymaster = req.relayRequest.relayData.paymaster
let gasLimits = this.trustedPaymastersGasLimits.get(paymaster)
let acceptanceBudget: number
if (gasLimits == null) {
try {
const paymasterContract = await this.contractInteractor._createPaymaster(paymaster)
gasLimits = await paymasterContract.getGasLimits()
} catch (e) {
const error = e as Error
let message = `unknown paymaster error: ${error.message}`
if (error.message.includes('Returned values aren\'t valid, did it run Out of Gas?')) {
message = `incompatible paymaster contract: ${paymaster}`
} else if (error.message.includes('no code at address')) {
message = `'non-existent paymaster contract: ${paymaster}`
}
throw new Error(message)
}
throw new Error(message)
}
let acceptanceBudget = this.config.maxAcceptanceBudget
const paymasterAcceptanceBudget = parseInt(gasLimits.acceptanceBudget)
if (paymasterAcceptanceBudget > acceptanceBudget) {
if (!this._isTrustedPaymaster(req.relayRequest.relayData.paymaster)) {
throw new Error(
`paymaster acceptance budget too high. given: ${paymasterAcceptanceBudget} max allowed: ${this.config.maxAcceptanceBudget}`)
acceptanceBudget = this.config.maxAcceptanceBudget
const paymasterAcceptanceBudget = parseInt(gasLimits.acceptanceBudget)
if (paymasterAcceptanceBudget > acceptanceBudget) {
if (!this._isTrustedPaymaster(paymaster)) {
throw new Error(
`paymaster acceptance budget too high. given: ${paymasterAcceptanceBudget} max allowed: ${this.config.maxAcceptanceBudget}`)
}
log.debug(`Using trusted paymaster's higher than max acceptance budget: ${paymasterAcceptanceBudget}`)
acceptanceBudget = paymasterAcceptanceBudget
}
log.debug(`Using trusted paymaster's higher than max acceptance budget: ${paymasterAcceptanceBudget}`)
acceptanceBudget = paymasterAcceptanceBudget
} else {
// its a trusted paymaster. just use its acceptance budget as-is
acceptanceBudget = parseInt(gasLimits.acceptanceBudget)
}

const hubOverhead = (await this.relayHubContract.gasOverhead()).toNumber()
Expand All @@ -168,7 +180,7 @@ export class RelayServer extends EventEmitter {
})
const maxCharge =
await this.relayHubContract.calculateCharge(maxPossibleGas, req.relayRequest.relayData)
const paymasterBalance = await this.relayHubContract.balanceOf(req.relayRequest.relayData.paymaster)
const paymasterBalance = await this.relayHubContract.balanceOf(paymaster)

if (paymasterBalance.lt(maxCharge)) {
throw new Error(`paymaster balance too low: ${paymasterBalance.toString()}, maxCharge: ${maxCharge.toString()}`)
Expand Down Expand Up @@ -299,12 +311,44 @@ export class RelayServer extends EventEmitter {
process.exit(1)
}

/***
* initialize data from trusted paymasters.
* "Trusted" paymasters means that:
* - we trust their code not to alter the gas limits (getGasLimits returns constants)
* - we trust preRelayedCall to be consistent: off-chain call and on-chain calls should either both succeed
* or both revert.
* - given that, we agree to give the requested acceptanceBudget (since breaking one of the above two "invariants"
* is the only cases where the relayer will have to pay for this budget)
*
* @param paymasters list of trusted paymaster addresses
*/
async _initTrustedPaymasters (paymasters: string[] = []): Promise<void> {
this.trustedPaymastersGasLimits.clear()
for (const paymasterAddress of paymasters) {
const paymaster = await this.contractInteractor._createPaymaster(paymasterAddress)
const gasLimits = await paymaster.getGasLimits().catch((e: Error) => {
throw new Error(`not a valid paymaster address in trustedPaymasters list: ${paymasterAddress}: ${e.message}`)
})
this.trustedPaymastersGasLimits.set(paymasterAddress.toLowerCase(), gasLimits)
}
}

_getPaymasterMaxAcceptanceBudget (paymaster?: string): IntString {
const limits = this.trustedPaymastersGasLimits.get(paymaster?.toLocaleLowerCase())
if (limits != null) {
return limits.acceptanceBudget
} else {
return this.config.maxAcceptanceBudget.toString()
}
}

async init (): Promise<void> {
if (this.initialized) {
throw new Error('_init was already called')
}

await this.transactionManager._init()
await this._initTrustedPaymasters(this.config.trustedPaymasters)
this.relayHubContract = await this.contractInteractor.relayHubInstance

const relayHubAddress = this.relayHubContract.address
Expand Down Expand Up @@ -540,7 +584,7 @@ export class RelayServer extends EventEmitter {
}

_isTrustedPaymaster (paymaster: string): boolean {
return this.config.trustedPaymasters.map(it => it.toLowerCase()).includes(paymaster.toLowerCase())
return this.trustedPaymastersGasLimits.get(paymaster.toLocaleLowerCase()) != null
}

timeUnit (): number {
Expand Down
8 changes: 4 additions & 4 deletions test/TestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export async function startRelay (
}
assert.ok(res, 'can\'t ping server')
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
assert.ok(res.RelayServerAddress, `server returned unknown response ${res.toString()}`)
const relayManagerAddress = res.RelayManagerAddress
assert.ok(res.relayWorkerAddress, `server returned unknown response ${res.toString()}`)
const relayManagerAddress = res.relayManagerAddress
console.log('Relay Server Address', relayManagerAddress)
// @ts-ignore
await web3.eth.sendTransaction({
Expand All @@ -124,10 +124,10 @@ export async function startRelay (
let count = 25
while (count-- > 0) {
res = await http.getPingResponse(localhostOne)
if (res?.Ready) break
if (res?.ready) break
await sleep(500)
}
assert.ok(res.Ready, 'Timed out waiting for relay to get staked and registered')
assert.ok(res.ready, 'Timed out waiting for relay to get staked and registered')

// TODO: this is temporary hack to make helper test work!!!
// @ts-ignore
Expand Down
4 changes: 2 additions & 2 deletions test/dummies/BadHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ export default class BadHttpClient extends HttpClient {
this.stubPing = stubPing
}

async getPingResponse (relayUrl: string): Promise<PingResponse> {
async getPingResponse (relayUrl: string, paymaster?: string): Promise<PingResponse> {
if (this.failPing) {
throw new Error(BadHttpClient.message)
}
if (this.stubPing != null) {
return this.stubPing
}
return await super.getPingResponse(relayUrl)
return await super.getPingResponse(relayUrl, paymaster)
}

async relayTransaction (relayUrl: string, request: RelayTransactionRequest): Promise<PrefixedHexString> {
Expand Down
18 changes: 9 additions & 9 deletions test/relayclient/RelayClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ contract('RelayClient', function (accounts) {

describe('#_attemptRelay()', function () {
const relayUrl = localhostOne
const RelayServerAddress = accounts[1]
const relayWorkerAddress = accounts[1]
const relayManager = accounts[2]
const relayOwner = accounts[3]
let pingResponse: PingResponse
Expand All @@ -250,17 +250,17 @@ contract('RelayClient', function (accounts) {
value: (2e18).toString()
})
await stakeManager.authorizeHubByOwner(relayManager, relayHub.address, { from: relayOwner })
await relayHub.addRelayWorkers([RelayServerAddress], { from: relayManager })
await relayHub.addRelayWorkers([relayWorkerAddress], { from: relayManager })
await relayHub.registerRelayServer(2e16.toString(), '10', 'url', { from: relayManager })
await relayHub.depositFor(paymaster.address, { value: (2e18).toString() })
pingResponse = {
RelayServerAddress,
RelayManagerAddress: relayManager,
RelayHubAddress: relayManager,
MinGasPrice: '',
MaxAcceptanceBudget: 1e10.toString(),
Ready: true,
Version: ''
relayWorkerAddress: relayWorkerAddress,
relayManagerAddress: relayManager,
relayHubAddress: relayManager,
minGasPrice: '',
maxAcceptanceBudget: 1e10.toString(),
ready: true,
version: ''
}
relayInfo = {
relayInfo: {
Expand Down
Loading

0 comments on commit f999e6a

Please sign in to comment.