Skip to content

Commit

Permalink
Fixing selection default mechanism (#718)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahafn authored Dec 13, 2021
1 parent 6103353 commit 626bd78
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/common/src/types/Aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RelayFailureInfo } from './RelayFailureInfo'
import { RelayRegisteredEventInfo } from './GSNContractsDataTypes'
import { HttpProvider, IpcProvider, WebsocketProvider } from 'web3-core'
import { JsonRpcPayload, JsonRpcResponse } from 'web3-core-helpers'
import BN from 'bn.js'

export type Address = string
export type EventName = string
Expand All @@ -20,7 +21,7 @@ export type PingFilter = (pingResponse: PingResponse, gsnTransactionDetails: Gsn
export type AsyncDataCallback = (relayRequest: RelayRequest) => Promise<PrefixedHexString>

export type RelayFilter = (registeredEventInfo: RelayRegisteredEventInfo) => boolean
export type AsyncScoreCalculator = (relay: RelayRegisteredEventInfo, txDetails: GsnTransactionDetails, failures: RelayFailureInfo[]) => Promise<number>
export type AsyncScoreCalculator = (relay: RelayRegisteredEventInfo, txDetails: GsnTransactionDetails, failures: RelayFailureInfo[]) => Promise<BN>

export function notNull<TValue> (value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined
Expand Down
39 changes: 31 additions & 8 deletions packages/dev/test/provider/KnownRelaysManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ChildProcessWithoutNullStreams } from 'child_process'
import { HttpProvider } from 'web3-core'
import { ether } from '@openzeppelin/test-helpers'

import { KnownRelaysManager, DefaultRelayScore } from '@opengsn/provider/dist/KnownRelaysManager'
import { KnownRelaysManager, DefaultRelayScore, DefaultRelayFilter } from '@opengsn/provider/dist/KnownRelaysManager'
import { ContractInteractor } from '@opengsn/common/dist/ContractInteractor'
import { GSNConfig } from '@opengsn/provider/dist/GSNConfigurator'
import {
Expand All @@ -21,6 +21,7 @@ import { RelayInfoUrl, RelayRegisteredEventInfo } from '@opengsn/common/dist/typ
import { createClientLogger } from '@opengsn/provider/dist/ClientWinstonLogger'
import { registerForwarderForGsn } from '@opengsn/common/dist/EIP712/ForwarderUtil'
import { defaultEnvironment } from '@opengsn/common/dist/Environments'
import { toBN } from 'web3-utils'

const StakeManager = artifacts.require('StakeManager')
const Penalizer = artifacts.require('Penalizer')
Expand Down Expand Up @@ -161,7 +162,7 @@ contract('KnownRelaysManager 2', function (accounts) {
let logger: LoggerInterface

const transactionDetails = {
gas: '0x10000',
gas: '0x10000000',
gasPrice: '0x300000',
from: '',
data: '',
Expand Down Expand Up @@ -252,6 +253,23 @@ contract('KnownRelaysManager 2', function (accounts) {
assert.equal(relays.length, 1)
assert.equal(relays[0].relayUrl, 'stakeAndAuthorization2')
})

it('should use DefaultRelayFilter to remove unsuitable relays when none was provided', async function () {
const knownRelaysManagerWithFilter = new KnownRelaysManager(contractInteractor, logger, config)
// @ts-ignore
assert.equal(knownRelaysManagerWithFilter.relayFilter.toString(), DefaultRelayFilter.toString())
})

describe('DefaultRelayFilter', function () {
it('should filter expensive relayers', function () {
const eventInfo = { relayUrl: 'url', relayManager: accounts[0] }
assert.isFalse(DefaultRelayFilter({ ...eventInfo, pctRelayFee: '101', baseRelayFee: 1e16.toString() }))
assert.isFalse(DefaultRelayFilter({ ...eventInfo, pctRelayFee: '99', baseRelayFee: 2e17.toString() }))
assert.isFalse(DefaultRelayFilter({ ...eventInfo, pctRelayFee: '101', baseRelayFee: 2e17.toString() }))
assert.isTrue(DefaultRelayFilter({ ...eventInfo, pctRelayFee: '100', baseRelayFee: 1e17.toString() }))
assert.isTrue(DefaultRelayFilter({ ...eventInfo, pctRelayFee: '50', baseRelayFee: '0' }))
})
})
})

describe('#getRelaysSortedForTransaction()', function () {
Expand Down Expand Up @@ -309,19 +327,24 @@ contract('KnownRelaysManager 2', function (accounts) {
const relayScoreOneFailure = await DefaultRelayScore(relayInfoHighFee, transactionDetails, [failure])
const relayScoreTenFailures = await DefaultRelayScore(relayInfoHighFee, transactionDetails, Array(10).fill(failure))
const relayScoreLowFees = await DefaultRelayScore(relayInfoLowFee, transactionDetails, [])
assert.isAbove(relayScoreNoFailures, relayScoreOneFailure)
assert.isAbove(relayScoreOneFailure, relayScoreTenFailures)
assert.isAbove(relayScoreLowFees, relayScoreNoFailures)
assert.isTrue(relayScoreNoFailures.gt(relayScoreOneFailure))
assert.isTrue(relayScoreOneFailure.gt(relayScoreTenFailures))
assert.isTrue(relayScoreLowFees.gt(relayScoreNoFailures))
})
it('should use DefaultRelayScore to remove unsuitable relays when none was provided', async function () {
const knownRelaysManagerWithFilter = new KnownRelaysManager(contractInteractor, logger, configureGSN({}))
// @ts-ignore
assert.equal(knownRelaysManagerWithFilter.scoreCalculator.toString(), DefaultRelayScore.toString())
})
})
})

describe('getRelaysSortedForTransaction', function () {
const biasedRelayScore = async function (relay: RelayRegisteredEventInfo): Promise<number> {
const biasedRelayScore = async function (relay: RelayRegisteredEventInfo): Promise<BN> {
if (relay.relayUrl === 'alex') {
return await Promise.resolve(1000)
return await Promise.resolve(toBN(1000))
} else {
return await Promise.resolve(100)
return await Promise.resolve(toBN(100))
}
}
const knownRelaysManager = new KnownRelaysManager(contractInteractor, logger, configureGSN({}), undefined, biasedRelayScore)
Expand Down
8 changes: 4 additions & 4 deletions packages/dev/test/provider/RelaySelectionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { HttpClient } from '@opengsn/common/dist/HttpClient'
import { HttpWrapper } from '@opengsn/common/dist/HttpWrapper'
import { PingResponse } from '@opengsn/common/dist/PingResponse'
import { RelaySelectionManager } from '@opengsn/provider/dist/RelaySelectionManager'
import { EmptyFilter, KnownRelaysManager } from '@opengsn/provider/dist/KnownRelaysManager'
import { DefaultRelayFilter, KnownRelaysManager } from '@opengsn/provider/dist/KnownRelaysManager'
import { GasPricePingFilter } from '@opengsn/provider/dist/RelayClient'
import { PartialRelayInfo, RelayInfo } from '@opengsn/common/dist/types/RelayInfo'
import { PingFilter } from '@opengsn/common/dist/types/Aliases'
Expand Down Expand Up @@ -75,7 +75,7 @@ contract('RelaySelectionManager', function (accounts) {
logger
}).init()
httpClient = new HttpClient(new HttpWrapper(), this.logger)
knownRelaysManager = new KnownRelaysManager(contractInteractor, logger, configureGSN({}), EmptyFilter)
knownRelaysManager = new KnownRelaysManager(contractInteractor, logger, configureGSN({}), DefaultRelayFilter)
stubGetRelaysSorted = sinon.stub(knownRelaysManager, 'getRelaysSortedForTransaction')
stubGetRelaysSorted.returns(Promise.resolve([[eventInfo]]))
relaySelectionManager = await new RelaySelectionManager(transactionDetails, knownRelaysManager, httpClient, GasPricePingFilter, logger, config).init()
Expand Down Expand Up @@ -120,7 +120,7 @@ contract('RelaySelectionManager', function (accounts) {
const penalizer = await Penalizer.new(defaultEnvironment.penalizerConfiguration.penalizeBlockDelay, defaultEnvironment.penalizerConfiguration.penalizeBlockExpiration)
relayHub = await deployHub(stakeManager.address, penalizer.address)
await stake(stakeManager, relayHub, relayManager, accounts[0])
await register(relayHub, relayManager, accounts[2], preferredRelayUrl, '666', '777')
await register(relayHub, relayManager, accounts[2], preferredRelayUrl, '666', '77')

await contractInteractor.initDeployment({
relayHubAddress: relayHub.address,
Expand Down Expand Up @@ -162,7 +162,7 @@ contract('RelaySelectionManager', function (accounts) {
assert.equal(nextRelay.relayInfo.relayUrl, preferredRelayUrl)
assert.equal(nextRelay.relayInfo.relayManager, relayManager)
assert.equal(nextRelay.relayInfo.baseRelayFee, '666')
assert.equal(nextRelay.relayInfo.pctRelayFee, '777')
assert.equal(nextRelay.relayInfo.pctRelayFee, '77')
})
})

Expand Down
44 changes: 28 additions & 16 deletions packages/provider/src/KnownRelaysManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,34 @@ import {
} from '@opengsn/common/dist/types/GSNContractsDataTypes'
import { LoggerInterface } from '@opengsn/common/dist/LoggerInterface'
import { ContractInteractor } from '@opengsn/common/dist/ContractInteractor'

export const EmptyFilter: RelayFilter = (): boolean => {
import { MAX_INTEGER } from 'ethereumjs-util'
import { toBN } from 'web3-utils'
import BN from 'bn.js'

export const DefaultRelayFilter: RelayFilter = function (registeredEventInfo: RelayRegisteredEventInfo): boolean {
const maxPctRelayFee = 100
const maxBaseRelayFee = 1e17
if (
parseInt(registeredEventInfo.pctRelayFee) > maxPctRelayFee ||
parseInt(registeredEventInfo.baseRelayFee) > maxBaseRelayFee
) {
return false
}
return true
}
/**
* Basic score is reversed transaction fee, higher is better.
* Relays that failed to respond recently will be downgraded for some period of time.
*/
export const DefaultRelayScore = async function (relay: RelayRegisteredEventInfo, txDetails: GsnTransactionDetails, failures: RelayFailureInfo[]): Promise<number> {
const gasLimit = parseInt(txDetails.gas ?? '0')
const gasPrice = parseInt(txDetails.gasPrice ?? '0')
const pctFee = parseInt(relay.pctRelayFee)
const baseFee = parseInt(relay.baseRelayFee)
const transactionCost = baseFee + (gasLimit * gasPrice * (100 + pctFee)) / 100
let score = Math.max(Number.MAX_SAFE_INTEGER - transactionCost, 0)
score = score * Math.pow(0.9, failures.length)
export const DefaultRelayScore: AsyncScoreCalculator = async function (relay: RelayRegisteredEventInfo, txDetails: GsnTransactionDetails, failures: RelayFailureInfo[]): Promise<BN> {
const gasLimit = toBN(txDetails.gas ?? '0')
const gasPrice = toBN(txDetails.gasPrice ?? '0')
const pctFee = toBN(relay.pctRelayFee)
const baseFee = toBN(relay.baseRelayFee)
const transactionCost = baseFee.add(gasLimit.mul(gasPrice).muln((100 + pctFee.toNumber()) / 100)
)
let score = MAX_INTEGER.sub(transactionCost)
score = score.muln(Math.pow(0.9, failures.length))
return score
}

Expand All @@ -51,7 +63,7 @@ export class KnownRelaysManager {
constructor (contractInteractor: ContractInteractor, logger: LoggerInterface, config: GSNConfig, relayFilter?: RelayFilter, scoreCalculator?: AsyncScoreCalculator) {
this.config = config
this.logger = logger
this.relayFilter = relayFilter ?? EmptyFilter
this.relayFilter = relayFilter ?? DefaultRelayFilter
this.scoreCalculator = scoreCalculator ?? DefaultRelayScore
this.contractInteractor = contractInteractor
}
Expand Down Expand Up @@ -173,9 +185,9 @@ export class KnownRelaysManager {
}

async _sortRelaysInternal (gsnTransactionDetails: GsnTransactionDetails, activeRelays: RelayInfoUrl[]): Promise<RelayInfoUrl[]> {
const scores = new Map<string, number>()
const scores = new Map<string, BN>()
for (const activeRelay of activeRelays) {
let score = 0
let score = toBN(0)
if (isInfoFromEvent(activeRelay)) {
const eventInfo = activeRelay as RelayRegisteredEventInfo
score = await this.scoreCalculator(eventInfo, gsnTransactionDetails, this.relayFailures.get(activeRelay.relayUrl) ?? [])
Expand All @@ -187,9 +199,9 @@ export class KnownRelaysManager {
.filter(isInfoFromEvent)
.map(value => (value as RelayRegisteredEventInfo))
.sort((a, b) => {
const aScore = scores.get(a.relayManager) ?? 0
const bScore = scores.get(b.relayManager) ?? 0
return bScore - aScore
const aScore = scores.get(a.relayManager) ?? toBN(0)
const bScore = scores.get(b.relayManager) ?? toBN(0)
return bScore.cmp(aScore)
})
}

Expand Down
4 changes: 2 additions & 2 deletions packages/provider/src/RelayClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { HttpClient } from '@opengsn/common/dist/HttpClient'
import { HttpWrapper } from '@opengsn/common/dist/HttpWrapper'
import { RelaySelectionManager } from './RelaySelectionManager'
import { RelayedTransactionValidator } from './RelayedTransactionValidator'
import { DefaultRelayScore, EmptyFilter, KnownRelaysManager } from './KnownRelaysManager'
import { DefaultRelayScore, DefaultRelayFilter, KnownRelaysManager } from './KnownRelaysManager'
import { createClientLogger } from './ClientWinstonLogger'
import { defaultGsnConfig, defaultLoggerConfiguration, GSNConfig, GSNDependencies } from './GSNConfigurator'

Expand Down Expand Up @@ -448,7 +448,7 @@ export class RelayClient {

const httpClient = overrideDependencies?.httpClient ?? new HttpClient(new HttpWrapper(), this.logger)
const pingFilter = overrideDependencies?.pingFilter ?? GasPricePingFilter
const relayFilter = overrideDependencies?.relayFilter ?? EmptyFilter
const relayFilter = overrideDependencies?.relayFilter ?? DefaultRelayFilter
const asyncApprovalData = overrideDependencies?.asyncApprovalData ?? EmptyDataCallback
const asyncPaymasterData = overrideDependencies?.asyncPaymasterData ?? EmptyDataCallback
const scoreCalculator = overrideDependencies?.scoreCalculator ?? DefaultRelayScore
Expand Down

0 comments on commit 626bd78

Please sign in to comment.