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(storage): InMemoryDatabase for tests #133

Merged
merged 6 commits into from
Sep 26, 2024
Merged

feat(storage): InMemoryDatabase for tests #133

merged 6 commits into from
Sep 26, 2024

Conversation

distractedm1nd
Copy link
Contributor

@distractedm1nd distractedm1nd commented Sep 26, 2024

based on top of #130

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a new InMemoryDatabase for efficient in-memory data storage.
    • Added service registration functionality within account creation processes.
    • Enhanced cryptographic capabilities with new operations for service registration and key management.
  • Bug Fixes

    • Improved validation logic for service registration and account creation.
  • Documentation

    • Updated tests to reflect changes in service registration and account management processes.
  • Refactor

    • Streamlined the handling of verifying keys and simplified related logic in the main application flow.
    • Updated the continuous integration workflow to enhance testing efficiency.

Copy link

vercel bot commented Sep 26, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
prism ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 26, 2024 10:30am

Copy link

coderabbitai bot commented Sep 26, 2024

Warning

Rate limit exceeded

@distractedm1nd has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 6 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Files that changed from the base of the PR and between debdbd8 and cbdaf77.

Walkthrough

The changes introduce significant modifications across various files, enhancing functionalities related to hashchains, operations, and service registration. A new in-memory database structure is established, and workspace dependencies are updated in the Cargo.toml. The implementation of methods for creating and validating operations has been refined, particularly focusing on account creation and service management. Additionally, the refactoring of existing code improves the clarity and maintainability of the overall project structure.

Changes

Files Change Summary
crates/common/Cargo.toml Added base64.workspace = true to workspace dependencies.
crates/common/src/hashchain.rs Introduced new methods for Hashchain, updated validation logic, and removed mutable context from create_account.
crates/common/src/operation.rs Added methods for signature verification and new operations for service registration, account creation, and key management.
crates/common/src/test_utils.rs Added Service struct, modified account creation to link with services, and refactored hashchain creation logic.
crates/common/src/tree.rs Updated account insertion logic to prevent duplicates and refined service-related account creation processes.
crates/nova/src/utils.rs Modified create_pp function to include service registration before account creation.
crates/prism/src/da/mod.rs Deleted file containing Data Availability Layer functionality.
crates/prism/src/main.rs Simplified processing of verifying_key in the main function.
crates/prism/src/node_types/sequencer.rs Added asynchronous functions for epoch processing and commitment retrieval, modified operation validation logic.
crates/prism/tests/integration_tests.rs Switched from Redis to in-memory database for testing, updated user account creation logic.
crates/storage/src/inmemory.rs Introduced InMemoryDatabase struct with methods for managing nodes, values, commitments, and epochs.
crates/storage/src/lib.rs Added new module inmemory to the public interface of the storage library.

Possibly related PRs

🐇 In a world where hashchains grow,
New services bloom, and operations flow.
With in-memory magic, we store and play,
Creating accounts in a whimsical way.
Hops of joy in every line,
Code so clear, it surely shines! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 25

🧹 Outside diff range and nitpick comments (17)
crates/nova/src/utils.rs (1)

166-176: Significant changes to create_pp function structure

The function now includes service registration before account creation, which aligns with the PR objective of implementing an InMemoryDatabase for tests. This change adds complexity to the test setup process.

Consider the following:

  1. Document the reason for this change in a comment to help future maintainers understand the context.
  2. Evaluate if this change affects any existing tests or functionalities that depend on the create_pp function.
crates/common/src/test_utils.rs (1)

33-33: Consider renaming the registration field for clarity

The registration field in the Service struct holds a TestAccount. Renaming it to something more descriptive, like registration_account, could improve code readability.

crates/common/src/hashchain.rs (1)

146-146: Address the TODO regarding RegisterService permissioning.

The TODO comment indicates that RegisterService should not be permissionless until state bloat metrics are available. Implementing permission checks now can prevent potential misuse and align with security best practices.

Would you like assistance in implementing the necessary permissioning logic or should I open a GitHub issue to track this task?

crates/prism/src/node_types/sequencer.rs (11)

Line range hint 320-358: Improve Error Handling and Consistency in prove_epoch Method

In the prove_epoch method, consider the following improvements:

  1. Error Handling: Currently, if the proof generation or verification fails, the error is propagated up the call stack. It might be beneficial to add context to these errors for better debugging.

  2. Consistency in Feature Flags: The use of #[cfg(not(feature = "groth16"))] and #[cfg(feature = "groth16")] could be condensed for clarity using an if statement inside a single block, although this depends on whether conditional compilation is necessary here.

  3. Code Duplication: The code for proof generation is duplicated in both conditional blocks. You could refactor this to eliminate duplication.

Proposed refactored code:

async fn prove_epoch(
    &self,
    height: u64,
    prev_commitment: Digest,
    new_commitment: Digest,
    proofs: Vec<Proof>,
) -> Result<FinalizedEpoch> {
    let batch = Batch {
        prev_root: prev_commitment,
        new_root: new_commitment,
        proofs,
    };

    let mut stdin = SP1Stdin::new();
    stdin.write(&batch);
    let client = self.prover_client.read().await;

    info!("generating proof for epoch height {}", height);

    let proof = {
        let prover = client.prove(&self.proving_key, stdin);
        #[cfg(feature = "groth16")]
        {
            prover.groth16().run()?
        }
        #[cfg(not(feature = "groth16"))]
        {
            prover.run()?
        }
    };

    info!("successfully generated proof for epoch height {}", height);

    client.verify(&proof, &self.verifying_key)?;
    info!("verified proof for epoch height {}", height);

    let mut epoch_json = FinalizedEpoch {
        height,
        prev_commitment,
        current_commitment: new_commitment,
        proof,
        signature: None,
    };

    epoch_json.insert_signature(&self.key);
    Ok(epoch_json)
}

360-362: Simplify Method get_commitment

The method get_commitment can be simplified by removing unnecessary context wrapping if not needed.

Simplified code:

pub async fn get_commitment(&self) -> Result<Digest> {
    let tree = self.tree.read().await;
    tree.get_commitment()
}

Line range hint 365-371: Avoid Nested Result in Return Type of get_hashchain

The return type Result<Result<Hashchain, NonMembershipProof>> is nested, which can be confusing. Consider flattening the Result types for better clarity.

Rewritten method signature:

pub async fn get_hashchain(
    &self,
    id: &str,
) -> Result<Option<Hashchain>> {
    let tree = self.tree.read().await;
    let hashed_id = Digest::hash(id);
    let key_hash = KeyHash::with::<Hasher>(hashed_id);

    match tree.get(key_hash) {
        Ok(hashchain) => Ok(Some(hashchain)),
        Err(NonMembershipProof) => Ok(None),
        Err(e) => Err(e),
    }
}

Additionally, change the parameter from &String to &str to follow Rust's best practices for borrowing strings.


Line range hint 393-399: Add Validation for RegisterService and CreateAccount Operations

In the validate_and_queue_update method, the match arms for Operation::RegisterService and Operation::CreateAccount are empty, meaning no validation is performed. Consider adding validation logic to ensure these operations are valid before queuing.

Suggested addition:

match incoming_operation {
    Operation::RegisterService(_) | Operation::CreateAccount(_) => {
        // Perform necessary validation here
        incoming_operation.verify_signature()?;
    },
    Operation::AddKey(_) | Operation::RevokeKey(_) => {
        // Existing validation logic
    }
};

This ensures that signatures and other critical aspects of all operation types are validated.


418-420: Organize Imports According to Convention

Grouping imports logically improves readability. Consider grouping the use statements by their crates and alphabetically within each group.

Organized imports:

use keystore_rs::create_signing_key;
use prism_common::{operation::PublicKey, test_utils::create_mock_signing_key};
use prism_da::memory::InMemoryDataAvailabilityLayer;
use prism_storage::inmemory::InMemoryDatabase;

437-439: Remove Redundant Clones in Test Code

In the test test_validate_and_queue_update, the service_key.clone() may be unnecessary if service_key implements Copy or is not used after being moved.

Simplified code:

let op = Operation::new_register_service("service_id".to_string(), service_key.into());

447-451: Avoid Unnecessary Cloning of sequencer

Chaining multiple .clone() calls on sequencer is unnecessary if sequencer implements Clone and is not moved. Consider borrowing instead.

Simplified code:

sequencer
    .validate_and_queue_update(&op)
    .await
    .unwrap();

462-464: Remove Redundant Cloning of signing_key

The variable original_pubkey can be obtained without cloning signing_key if it's not used afterward.

Simplified code:

let original_pubkey = signing_key.into();
let service_key = create_mock_signing_key();

488-492: Optimize Key Conversion without Cloning

When converting new_key into pubkey, cloning might be unnecessary.

Simplified code:

let pubkey = new_key.verifying_key().into();
let add_key_op =
    Operation::new_add_key("[email protected]".to_string(), pubkey, &signing_key, 0)
        .unwrap();

560-561: Avoid Unnecessary Conversion in Key Assignment

In test code, avoid unnecessary .into() calls if the types align without conversion.

Simplified code:

let new_key = create_mock_signing_key();
let service_key = create_mock_signing_key();

599-601: Consistent Spacing Between Test Sections

Maintain consistent spacing to improve readability in test functions.

Add a newline after variable declarations:

let service_key = create_mock_signing_key();

let operations = vec![
    // operations
];
crates/common/src/tree.rs (3)

Line range hint 93-97: Avoid using assert! in production code within the From implementation

The use of assert!(N <= 32, "Input array must not exceed 32 bytes"); in the From<[u8; N]> for Digest implementation can cause a panic at runtime if the condition is not met. It is advisable to handle this scenario without causing a panic, perhaps by returning a Result or adjusting the implementation to safely handle arrays larger than 32 bytes.

Consider modifying the implementation to return a Digest without panicking:

impl<const N: usize> From<[u8; N]> for Digest {
    fn from(value: [u8; N]) -> Self {
        let mut digest = [0u8; 32];
        let len = N.min(32);
        digest[..len].copy_from_slice(&value[..len]);
        Self(digest)
    }
}

381-382: Improve error message clarity when service hashchain retrieval fails

The error message "Failed to get hashchain for service ID {}" may not provide sufficient context if the service does not exist. Consider specifying whether the service hashchain does not exist or if there was an error retrieving it.


736-740: Optimize logging output in test_batch_writing

The test includes multiple println! statements for debugging purposes. Consider using a logger or removing these statements if they are no longer needed to keep the test output clean.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 797cb09 and 2d525dd.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/common/Cargo.toml (1 hunks)
  • crates/common/src/hashchain.rs (7 hunks)
  • crates/common/src/operation.rs (8 hunks)
  • crates/common/src/test_utils.rs (9 hunks)
  • crates/common/src/tree.rs (11 hunks)
  • crates/nova/src/utils.rs (1 hunks)
  • crates/prism/src/da/mod.rs (0 hunks)
  • crates/prism/src/main.rs (2 hunks)
  • crates/prism/src/node_types/sequencer.rs (6 hunks)
  • crates/prism/tests/integration_tests.rs (2 hunks)
  • crates/storage/src/inmemory.rs (1 hunks)
  • crates/storage/src/lib.rs (1 hunks)
💤 Files not reviewed due to no reviewable changes (1)
  • crates/prism/src/da/mod.rs
🔇 Additional comments (21)
crates/storage/src/lib.rs (1)

2-2: LGTM! Consider re-exporting the InMemoryDatabase.

The addition of the inmemory module aligns well with the PR objectives and expands the available storage options without modifying existing functionality. This is a good approach for introducing an in-memory database for testing purposes.

Consider re-exporting the InMemoryDatabase struct (assuming it exists) similar to how Database and RedisConnection are re-exported. This would provide a consistent API for users of this crate. You could add something like:

pub use crate::inmemory::InMemoryDatabase;

to the existing re-exports.

Let's verify the contents of the new inmemory module:

This will help us understand the structure of the InMemoryDatabase and confirm if a re-export would be beneficial.

✅ Verification successful

LGTM! Re-exporting InMemoryDatabase would enhance the crate's API consistency.

The addition of the inmemory module effectively introduces an in-memory storage option for testing, aligning with the PR objectives. Re-exporting the InMemoryDatabase struct will provide a consistent and convenient API for users, similar to existing re-exports like Database and RedisConnection.

You can add the following line to crates/storage/src/lib.rs:

pub use crate::inmemory::InMemoryDatabase;
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the contents of the inmemory module

# Test: Verify the existence and contents of the inmemory module
cat crates/storage/src/inmemory.rs

Length of output: 3660

crates/common/Cargo.toml (1)

21-21: LGTM! Verify base64 crate usage.

The addition of the base64 crate as a workspace dependency is appropriate and consistent with the existing dependency declarations. This addition likely supports the implementation of the InMemoryDatabase for tests.

To ensure the base64 crate is being utilized effectively, please run the following script to check its usage across the codebase:

This will help confirm that the newly added dependency is being used appropriately in the project.

crates/prism/src/main.rs (1)

10-11: LGTM: New import added for PublicKey

The addition of use prism_common::operation::PublicKey; is appropriate given the changes in the main function for handling the verifying key.

crates/prism/tests/integration_tests.rs (2)

28-28: Please clarify the SigningKey conversion.

The change from using the verifying key to directly using the signing key for the value field simplifies the code. However, it's crucial to ensure that this modification doesn't compromise security or alter the intended functionality.

Could you please clarify:

  1. Is SigningKey the correct type for the value field in CreateAccountArgs?
  2. Does the into() method correctly convert SigningKey to the expected type?

To verify the correctness of this change, please run the following script:

#!/bin/bash
# Description: Verify the type compatibility and conversion of SigningKey

# Test: Check if SigningKey implements the correct trait for conversion
ast-grep --lang rust --pattern 'impl From<SigningKey> for $type { $$$  }'

# Test: Verify usage of SigningKey in CreateAccountArgs
rg --type rust -A 5 'struct CreateAccountArgs'

This will help confirm that the SigningKey is correctly implemented and used in the CreateAccountArgs struct.


20-20: LGTM: InMemoryDatabase implementation looks good.

The change from Redis to an in-memory database aligns with the PR objective and should improve test performance. This modification reduces external dependencies, making the tests more isolated and potentially faster.

To ensure this change doesn't affect test behavior, please run the following script:

This will help confirm that the switch to InMemoryDatabase hasn't introduced any regressions in the test suite.

Also applies to: 55-55

crates/nova/src/utils.rs (1)

Line range hint 1-576: Recommend comprehensive test review

While the changes are localized to the create_pp function, they introduce a new structure with service registration that might affect how the utility functions in this file are used in tests.

To ensure compatibility with existing test setups:

  1. Review all tests that use the create_pp function:
#!/bin/bash
# Search for tests using create_pp
rg "create_pp\(" --type rust --glob "**/tests/**"
  1. Verify if any tests need to be updated to accommodate the new service registration step:
#!/bin/bash
# Search for test setup code that might need updating
rg "TestTreeState::default\(\)" --type rust --glob "**/tests/**"
crates/storage/src/inmemory.rs (1)

75-88: ⚠️ Potential issue

Maintain consistent lock acquisition order to prevent deadlocks

In write_node_batch, you acquire locks on nodes and values in a specific order. If other methods acquire these locks in a different order, it could lead to a deadlock.

Ensure that all methods in your codebase acquire locks in the same order. For example, always lock nodes before values. Review other parts of the code to confirm consistent locking order.

Run the following script to check for consistent locking order:

This script checks for instances where nodes is locked, and within the next 5 lines, values is also locked. Ensure that no methods lock values before nodes.

✅ Verification successful

Lock acquisition order is consistent and prevents potential deadlocks.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that locks are always acquired in the same order.

# Search for occurrences of locking `nodes` and `values`
rg --type rust 'self\.nodes\.lock\(\)' -A 5 | grep 'self\.values\.lock\('

Length of output: 99


Script:

#!/bin/bash
# Description: Verify that locks are always acquired in the same order.

# Search for occurrences of locking `nodes` followed by `values`
rg --type rust 'self\.nodes\.lock\(\)' -A 5 | grep -E 'self\.values\.lock\('

Length of output: 239


Script:

#!/bin/bash
# Description: Verify that no locks are acquired in the order `values` before `nodes` to prevent deadlocks.

# Search for occurrences of locking `values` followed by `nodes`
rg --type rust 'self\.values\.lock\(\)' -A 5 | grep -E 'self\.nodes\.lock\('

Length of output: 78

crates/common/src/test_utils.rs (1)

28-35: Verify ownership and lifetimes in the Service struct

The Service struct contains a registration field of type TestAccount, which in turn contains ownership of a Hashchain. Ensure that the cloning and references are managed correctly to prevent ownership conflicts or lifetime issues.

Run the following script to check for potential ownership and lifetime issues:

crates/common/src/hashchain.rs (5)

65-69: Implementation of from_operation method is correct.

The from_operation method properly initializes a new Hashchain and performs the provided Operation, adhering to a functional approach and improving code modularity.


71-88: Refactored create_account method enhances code clarity.

Transforming create_account into an associated function that returns a Hashchain removes the need for mutable state and aligns with functional programming best practices. This improves code readability and maintainability.


90-98: Addition of register_service method improves functionality.

The new register_service method encapsulates the service registration logic within the Hashchain, promoting a cleaner and more modular code structure.


156-161: Inconsistent serialization in signature verification.

In the CreateAccount operation (lines 156-161), the signature verification serializes the operation using without_signature().without_challenge(), while in AddKey and RevokeKey operations (lines 168-171), it uses only without_signature(). Please verify that excluding the challenge in CreateAccount is intentional and that it does not compromise the integrity of the signature verification process.

Also applies to: 168-171


284-286: Ensure the consistency of key hashing.

The get_keyhash method hashes self.id to produce the KeyHash. Verify that self.id is intended to be used as the unique identifier for key hashing and that this approach is consistent with the rest of the system's design.

crates/common/src/tree.rs (8)

545-548: Verify the test test_insert_and_get for proper account insertion

In the test test_insert_and_get, it seems that the service account is not inserted before inserting the account, which might not reflect the actual logic where the service must exist prior to account creation.

Ensure that the service is properly inserted before inserting the account. Add the following line before inserting the account:

tree_state.insert_account(service.registration.clone()).unwrap();

560-565: Clarify the test case in test_insert_for_nonexistent_service_fails

In this test, the service is registered but not inserted into the tree before attempting to insert an account. Ensure that this test correctly validates that account insertion fails when the service does not exist in the tree.


572-574: Potential issue with falsifying service signing key in test

In test_insert_with_invalid_service_challenge_fails, the service signing key is replaced with a mock key to simulate an invalid challenge. Ensure that this approach accurately reflects an invalid service challenge and doesn't introduce unintended side effects.

Review the implementation to confirm that replacing service.sk effectively simulates an invalid service challenge without affecting other parts of the test.


595-597: Check error handling in test_insert_duplicate_key

The test attempts to insert an account that already exists. Confirm that the error handling correctly identifies the duplicate key and that the appropriate error is returned.


628-632: Ensure service insertion in test_update_non_existing_key

In test_update_non_existing_key, the account is created but not inserted into the tree before attempting an update. This should result in an error, as expected. Verify that the test accurately checks the update failure due to a non-existing key.


655-659: Confirm the sequence of inserts and updates in test_multiple_inserts_and_updates

The test performs multiple inserts and updates. Ensure that the service is inserted before accounts and that accounts are updated correctly. This helps validate the integrity of sequential operations.


682-686: Validate interleaved operations in test_interleaved_inserts_and_updates

This test interleaves inserts and updates between two accounts. Verify that the operations maintain the correct state in the tree and that the final root hash matches the expected value.


718-724: Check root hash changes in test_root_hash_changes

The test checks that the root hash changes after inserting a new account. Ensure that the root hash before and after the insertion are indeed different, confirming that the tree is updating correctly.

crates/prism/src/main.rs Outdated Show resolved Hide resolved
crates/nova/src/utils.rs Outdated Show resolved Hide resolved
crates/nova/src/utils.rs Outdated Show resolved Hide resolved
crates/storage/src/inmemory.rs Show resolved Hide resolved
crates/storage/src/inmemory.rs Show resolved Hide resolved
crates/prism/src/node_types/sequencer.rs Outdated Show resolved Hide resolved
crates/common/src/tree.rs Outdated Show resolved Hide resolved
crates/common/src/tree.rs Outdated Show resolved Hide resolved
crates/common/src/tree.rs Outdated Show resolved Hide resolved
crates/common/src/tree.rs Outdated Show resolved Hide resolved
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.

1 participant