Skip to content
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: update oval node to support oval factory deployments #83

Merged
merged 36 commits into from
Jul 31, 2024

Conversation

md0x
Copy link
Contributor

@md0x md0x commented Jul 15, 2024

Changes proposed in this PR:

  • Support for Oval Factories: Implement the new feature that allows permissionless deployments of Oval. Oval factories

  • New Configuration: Add a new config ovalConfigsShared

  • Introduce WalletManager to allocate wallets for the unlocks. WalletManager

  • Add OvalDiscovery service to find permissionless deployments of Oval throughout the factories. OvalDiscovery

  • No changes in how searchers interact with Oval. Searchers can use the oval-headers to specify a particular Oval for unlocks, or they can opt to not specify and rely on the Oval mechanism to find the necessary instance.

Signed-off-by: Pablo Maldonado <[email protected]>
Copy link

linear bot commented Jul 15, 2024

md0x added 17 commits July 16, 2024 13:38
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Copy link
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

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

This looks great! I think this is really well modularized. Only a few small comments

type WalletUsed = {
walletPubKey: string;
targetBlock: number;
count: number;
Copy link
Member

Choose a reason for hiding this comment

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

Does count mean we can use a wallet more than once in a single block? Interesting... I hadn't really considered what the node would do if it ran out of wallets. I think this makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, this is to handle scenarios where we have more oval-instances than unlockers and spikes in the number of unlocks.

I will also add alerts in those cases.

@@ -0,0 +1,134 @@
import { expect } from 'chai';
Copy link
Member

Choose a reason for hiding this comment

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

+1 on having unit tests for this module

private ovalDiscovery: OvalDiscovery;
private wallets: Record<string, Wallet> = {};
private sharedWallets: Map<string, Wallet> = new Map();
private sharedWalletUsage: Map<string, Array<WalletUsed>> = new Map();
Copy link
Member

Choose a reason for hiding this comment

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

Why did you choose to have an Array to track wallet usage rather than a mapping?

For instance, I could imagine a mapping from block->OvalInstance->WalletUsed might be a decent alternative: simple to drop blocks once they've passed (just delete from the map) and it's easy to tell if there's already been a wallet assigned (no array traversal/find).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, that’s a better data structure from the record cleanup. However, initially, I proposed the data structure with this idea in mind:

  • We want to use the same unlocker for a given oval instance, regardless of the target block. This approach is also due to a cleanup running every 5 minutes, preventing old assignments from being reused, focusing only on recent assignments.

The reason for this is that multiple unlocks to the same oval instance in target blocks close to each other are likely part of the same backrun. They belong to the same price auction, meaning nonce collisions are not possible if we reuse the unlocker. Do you agree with this?

The previous data structure seemed to work fine with that assumption. However, I do like removing the array and using only mappings that we still need to traverse, for the reason I explained above.

I’ve changed the data structure in the latest commits.

@md0x md0x marked this pull request as ready for review July 19, 2024 16:42
@md0x md0x requested a review from chrismaree as a code owner July 19, 2024 16:42
@md0x md0x requested a review from mrice32 July 19, 2024 16:42
md0x added 2 commits July 19, 2024 18:02
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
this.findOval(FACTORIES_GENESIS_BLOCK);
}

public async findOval(fromBlock: number) {
Copy link

Choose a reason for hiding this comment

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

the naming of this function is misleading. if i see "find", i expect this to be read only to return me something that matches a search, but this is a function that mutates state. perhaps this shoudl be like "updateInstances" or "fetchInstances"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed & updated


setTimeout(() => {
this.findOval(lastBlock);
}, env.ovalDiscoveryInterval * 1000);
Copy link

Choose a reason for hiding this comment

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

this is surprising, and imo should be moved out to a higher level call to decide how to loop this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This class is a service that finds Oval instances deployed through the Factories.

This recursive function acts as a subscription to the events in the contracts. Instead of a subscription, we manually fetch the events every X minutes because event subscriptions sometimes cause issues with WebSockets closing or failing.

I think having the logic of the event retrieval contained in this class and abstracted away from the rest of the code makes it simple to reason about.

The initialization of the Oval discovery service is run in the index file at a higher level:

oval-node/src/index.ts

Lines 57 to 58 in fee339d

const ovalDiscovery = OvalDiscovery.getInstance();
ovalDiscovery.initialize(provider);

Knowing this: Would you extract the logic and handle the event retrieval process differently?

Copy link

Choose a reason for hiding this comment

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

i would probably at the very least have a function that runs a singular update, and then have a separate call which runs that update in a loop continuously on the class, just to add a bit of composability and testability

};

// WalletManager class to handle wallet operations.
export class WalletManager {
Copy link

Choose a reason for hiding this comment

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

i dont really understand the logic behind how this manages the wallets, maybe need some comments

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've added some comments describing what each of the functions do

Copy link
Contributor

@Reinis-FRP Reinis-FRP left a comment

Choose a reason for hiding this comment

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

looking great!
just a nit comment on contract typings

@@ -0,0 +1,249 @@
/* Autogenerated file. Do not edit manually. */
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't these be gitignored and generated as part of build?

Also, for the sake of consistency, maybe we can also add Oval abi to this flow (currently its abi is in ts)

Copy link
Member

Choose a reason for hiding this comment

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

yup

Comment on lines +36 to +40
await Promise.map(paginatedRanges, ([fromBlock, toBlock]) => contract.queryFilter(filter, fromBlock, toBlock), {
concurrency: typeof searchConfig.concurrency == "number" ? searchConfig.concurrency : defaultConcurrency,
})
)
.flat()
Copy link
Member

Choose a reason for hiding this comment

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

did you need to use the bluebird promise due to limitations of the number of requests, that would error/fail without pagination?

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 reused this existing code, used in other parts of our codebase without modifications to minimize impact. Additionally, the pagination that this has by default, I think, makes our usage of the node URLs efficient without adding much delay. Would you change it to normal Promises?


// Initialize Oval discovery
const ovalDiscovery = OvalDiscovery.getInstance();
ovalDiscovery.initialize(provider);
Copy link
Member

Choose a reason for hiding this comment

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

do we have any idea on how much of a performance impact this has (and all the underlying async calls this triggers) on startup times? given this is running in cloud run, is this problematic?

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 did some tests, and querying 2 years of blocks in mainnet to find factories takes less than 10 seconds, querying 1 year takes 5.1 seconds. Since this only runs once when we restart the instance or when auto-scaling provisions a new instance, I think it’s fine. Do you agree?

md0x added 4 commits July 31, 2024 10:38
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
@md0x md0x merged commit aeb58ae into development Jul 31, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants