-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: await fetchRootKey calls while making API calls #963
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -150,6 +150,14 @@ export interface HttpAgentOptions { | |
* Alternate root key to use for verifying certificates. If not provided, the default IC root key will be used. | ||
*/ | ||
rootKey?: ArrayBuffer; | ||
|
||
/** | ||
* Whether the agent should fetch the root key from the network. Defaults to false. | ||
* | ||
* WARNING!!! Do not enable this in production environments, | ||
* as it can be used as an attack vector by malicious nodes. | ||
*/ | ||
shouldFetchRootKey?: boolean; | ||
} | ||
|
||
function getDefaultFetch(): typeof fetch { | ||
|
@@ -245,19 +253,21 @@ other computations so that this class can stay as simple as possible while | |
allowing extensions. | ||
*/ | ||
export class HttpAgent implements Agent { | ||
public rootKey: ArrayBuffer; | ||
#identity: Promise<Identity> | null; | ||
readonly #fetch: typeof fetch; | ||
readonly #fetchOptions?: Record<string, unknown>; | ||
readonly #callOptions?: Record<string, unknown>; | ||
#timeDiffMsecs = 0; | ||
readonly host: URL; | ||
readonly #credentials: string | undefined; | ||
#rootKeyFetched = false; | ||
readonly #retryTimes; // Retry requests N times before erroring by default | ||
#backoffStrategy: BackoffStrategyFactory; | ||
readonly #maxIngressExpiryInMinutes: number; | ||
|
||
#rootKey: ArrayBuffer; | ||
readonly #shouldFetchRootKey: boolean; | ||
#fetchRootKeyPromise: Promise<ArrayBuffer> | null = null; | ||
|
||
// Public signature to help with type checking. | ||
public readonly _isAgent = true; | ||
public config: HttpAgentOptions = {}; | ||
|
@@ -288,7 +298,11 @@ export class HttpAgent implements Agent { | |
this.#fetch = options.fetch || getDefaultFetch() || fetch.bind(global); | ||
this.#fetchOptions = options.fetchOptions; | ||
this.#callOptions = options.callOptions; | ||
this.rootKey = options.rootKey ? options.rootKey : fromHex(IC_ROOT_KEY); | ||
|
||
this.#rootKey = options.rootKey ?? fromHex(IC_ROOT_KEY); | ||
this.#shouldFetchRootKey = options.shouldFetchRootKey ?? false; | ||
// kick off the fetchRootKey process asynchronously, if needed | ||
(async () => await this.fetchRootKey())(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We start fetching the root key as soon as possible, in practice this should mean that there are no further delays compared to creating the agent with an async constructor and waiting for this task to complete before making API calls. |
||
|
||
const host = determineHost(options.host); | ||
this.host = new URL(host); | ||
|
@@ -354,16 +368,9 @@ export class HttpAgent implements Agent { | |
return new this({ ...options }); | ||
} | ||
|
||
public static async create( | ||
options: HttpAgentOptions & { shouldFetchRootKey?: boolean } = { | ||
shouldFetchRootKey: false, | ||
}, | ||
): Promise<HttpAgent> { | ||
public static async create(options: HttpAgentOptions = {}): Promise<HttpAgent> { | ||
const agent = HttpAgent.createSync(options); | ||
const initPromises: Promise<ArrayBuffer | void>[] = [agent.syncTime()]; | ||
if (agent.host.toString() !== 'https://icp-api.io' && options.shouldFetchRootKey) { | ||
initPromises.push(agent.fetchRootKey()); | ||
} | ||
await Promise.all(initPromises); | ||
return agent; | ||
} | ||
|
@@ -437,7 +444,7 @@ export class HttpAgent implements Agent { | |
): Promise<SubmitResponse> { | ||
// TODO - restore this value | ||
const callSync = options.callSync ?? true; | ||
const id = await (identity !== undefined ? await identity : await this.#identity); | ||
const id = identity !== undefined ? await identity : await this.#identity; | ||
if (!id) { | ||
throw new IdentityInvalidError( | ||
"This identity has expired due this application's security policy. Please refresh your authentication.", | ||
|
@@ -1125,14 +1132,27 @@ export class HttpAgent implements Agent { | |
return cbor.decode(await response.arrayBuffer()); | ||
} | ||
|
||
async getRootKey(): Promise<ArrayBuffer> { | ||
return this.fetchRootKey(); | ||
} | ||
|
||
public async fetchRootKey(): Promise<ArrayBuffer> { | ||
if (!this.#rootKeyFetched) { | ||
const status = await this.status(); | ||
// Hex-encoded version of the replica root key | ||
this.rootKey = (status as JsonObject & { root_key: ArrayBuffer }).root_key; | ||
this.#rootKeyFetched = true; | ||
if (!this.#shouldFetchRootKey) { | ||
return this.#rootKey; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we should not fetch the root key, we return the existing root key, which is either a hard coded root key provided by the consumer or the mainnet root key. |
||
|
||
if (this.#fetchRootKeyPromise === null) { | ||
this.#fetchRootKeyPromise = this.#fetchRootKey(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we should fetch the root key, and this is the first time this function is called, we create the promise to fetch the root key. On subsequent calls to this function, the promise will have already been made so we will skip this. |
||
} | ||
return this.rootKey; | ||
|
||
return await this.#fetchRootKeyPromise; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Finally we await the promise and return the resulting root key. If the promise has already resolved, we can just continue reusing the resolved promise. |
||
} | ||
|
||
async #fetchRootKey(): Promise<ArrayBuffer> { | ||
const status = await this.status(); | ||
// Hex-encoded version of the replica root key | ||
this.#rootKey = (status as JsonObject & { root_key: ArrayBuffer }).root_key; | ||
return this.#rootKey; | ||
} | ||
|
||
public invalidateIdentity(): void { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed the public
rootKey
property in favor of the asyncgetRootKey
, since we cannot guarantee availability of the correct root key whenfetchRootKey
is set to true.