Skip to content

feat(server): add migration command #113

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

julienrbrt
Copy link
Member

@julienrbrt julienrbrt commented May 23, 2025

Overview

Based on rollkit/cosmos-sdk-starter#63

Closes: #77
ref: #102

Summary by CodeRabbit

  • New Features

    • Introduced a migration command to transfer blockchain data from a CometBFT chain to a Rollkit chain, with progress updates during the process.
  • Chores

    • Updated dependency management to include a required package as a direct dependency.

Copy link
Contributor

coderabbitai bot commented May 23, 2025

"""

Walkthrough

A new migration command, rollkit-migration, is introduced to facilitate the migration of blockchain data from a CometBFT chain to a Rollkit chain. The implementation includes logic for loading, converting, and saving state and block data between the two systems, with error handling and progress output. Some functionality for extended commit information is marked as unimplemented.

Changes

File(s) Change Summary
go.mod Moved github.com/cometbft/cometbft-db from indirect to direct dependency.
server/migration_cmd.go Added rollkit-migration command and supporting functions for migrating state and blocks from CometBFT to Rollkit.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MigrationCmd
    participant CometBFTStore
    participant RollkitStore

    User->>MigrationCmd: Execute rollkit-migration
    MigrationCmd->>CometBFTStore: Load block and state stores
    MigrationCmd->>CometBFTStore: Get latest block and state
    MigrationCmd->>RollkitStore: Initialize Rollkit state store
    MigrationCmd->>RollkitStore: Convert and save initial state
    loop For each block (latest to 1)
        MigrationCmd->>CometBFTStore: Load block
        MigrationCmd->>MigrationCmd: Convert block to Rollkit format
        MigrationCmd->>RollkitStore: Save block components
    end
    MigrationCmd->>RollkitStore: Close store
    MigrationCmd-->>User: Print progress/completion messages
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement migration functionality from CometBFT to Rollkit (#77)
Ensure migration handles both state and block data conversion (#77)
Provide a command-line interface for initiating migration (#77)
Handle extended commit info for vote extensions (noted as TODO in code) (#77) Extended commit migration is not implemented; code panics if attempted.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Move of github.com/cometbft/cometbft-db from indirect to direct dependency (go.mod) This change is a dependency management update and not directly related to the migration feature.

Poem

A bunny with code on its mind,
Hops from CometBFT, leaving blocks behind.
To Rollkit it leaps, with data in tow,
State and blocks in a seamless flow.
With every migration, the ledger grows bright—
Hippity-hop, the chain feels right!
🐇✨
"""

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (1.64.8)

Error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2
Failed executing command with error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcc8c80 and f237f73.

📒 Files selected for processing (1)
  • server/migration_cmd.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/migration_cmd.go
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: test / Run Unit Tests
  • GitHub Check: lint / golangci-lint
  • GitHub Check: Test with Rollkit Chain
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ 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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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 generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this 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.

@julienrbrt
Copy link
Member Author

Going back to this on Monday

@julienrbrt julienrbrt marked this pull request as ready for review June 20, 2025 13:13
Copy link
Contributor

@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: 5

🧹 Nitpick comments (2)
server/migration_cmd.go (2)

157-157: Address TODO comment about signature handling.

The TODO indicates uncertainty about signature assignment.

Would you like assistance in determining the correct signature handling for the Rollkit header?


226-226: Address hardcoded DAHeight value.

The DAHeight is hardcoded to 1 with a TODO comment. This needs to be properly implemented.

How should the DAHeight be determined during migration? Would you like help implementing the logic to set this value correctly?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 468a361 and 6595f3c.

📒 Files selected for processing (2)
  • go.mod (1 hunks)
  • server/migration_cmd.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: lint / golangci-lint
  • GitHub Check: Test with Rollkit Chain
  • GitHub Check: test / Run Unit Tests
🔇 Additional comments (3)
go.mod (1)

19-19: LGTM! Necessary dependency change for migration functionality.

Moving cometbft-db to direct requirements is appropriate since the new migration command directly imports and uses this package.

server/migration_cmd.go (2)

165-165: Verify LastDataHash should reference previous block.

The LastDataHash in metadata uses the current block's DataHash, but "Last" typically refers to the previous block.

Please verify if this should reference the previous block's data hash instead of the current block's.


148-149: ```shell
#!/bin/bash
set -euo pipefail

Locate the cometBlockToRollkit function and surrounding context

rg "func cometBlockToRollkit" -n -C5 --color never

Inspect the usage of LastHeaderHash in this file

rg "LastHeaderHash" -n -C5 --color never


</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

App: cometBFTState.Version.Consensus.App,
},
ChainID: cometBFTState.ChainID,
InitialHeight: uint64(cometBFTState.LastBlockHeight), // The initial height is the migration height
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 20, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

InitialHeight should reflect the chain's actual initial height.

Setting InitialHeight to LastBlockHeight seems incorrect. It should be the height at which the chain was initialized, not the current migration height.

Consider using the actual initial height from the CometBFT state or configuration.

🤖 Prompt for AI Agents
In server/migration_cmd.go at line 222, the InitialHeight is incorrectly set to
LastBlockHeight, which represents the current migration height. Instead, update
InitialHeight to use the chain's actual initial height from the CometBFT state
or configuration, ensuring it reflects the chain's true starting height rather
than the current block height.

Copy link
Member Author

Choose a reason for hiding this comment

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

this is correct, as the start of the rollkit chain (technically a new chain), will the be the end of the last chain.

Copy link
Contributor

Choose a reason for hiding this comment

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

@julienrbrt Thank you for the clarification! That makes perfect sense - for chain migration continuity, the new Rollkit chain should indeed start where the CometBFT chain left off. I understand now that this is an intentional design decision.


✏️ Learnings added
Learnt from: julienrbrt
PR: rollkit/go-execution-abci#113
File: server/migration_cmd.go:231-231
Timestamp: 2025-06-20T13:20:04.011Z
Learning: In the rollkit migration command (server/migration_cmd.go), InitialHeight is correctly set to LastBlockHeight because the Rollkit chain is technically a new chain that starts where the CometBFT chain ended, ensuring continuity during migration.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +106 to +109
_ = extendedCommitInfo
// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
panic("Saving extended commit info is not implemented yet")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace panic with proper error handling.

Using panic for unimplemented functionality is poor user experience. Return an error instead.

-					_ = extendedCommitInfo
-					// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
-					panic("Saving extended commit info is not implemented yet")
+					// TODO: Implement SaveExtendedCommit
+					// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
+					return fmt.Errorf("saving extended commit info is not implemented yet")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_ = extendedCommitInfo
// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
panic("Saving extended commit info is not implemented yet")
}
// TODO: Implement SaveExtendedCommit
// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
return fmt.Errorf("saving extended commit info is not implemented yet")
🤖 Prompt for AI Agents
In server/migration_cmd.go around lines 106 to 109, replace the panic call used
for unimplemented saving of extended commit info with proper error handling by
returning an error instead. Modify the function to return an error indicating
that saving extended commit info is not implemented yet, avoiding abrupt
termination and improving user experience.


// Only save extended commit info if vote extensions are enabled
if enabled := cometBFTstate.ConsensusParams.ABCI.VoteExtensionsEnabled(block.Height); enabled {
extendedCommit := cometBlockStore.LoadBlockExtendedCommit(lastBlockHeight)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect height variable usage.

The code uses lastBlockHeight instead of the current iteration variable height.

-					extendedCommit := cometBlockStore.LoadBlockExtendedCommit(lastBlockHeight)
+					extendedCommit := cometBlockStore.LoadBlockExtendedCommit(height)
🤖 Prompt for AI Agents
In server/migration_cmd.go at line 80, replace the variable lastBlockHeight with
the current loop iteration variable height when calling LoadBlockExtendedCommit
to ensure the correct block height is used in each iteration.

Comment on lines +68 to +69
for height := lastBlockHeight; height > 0; height-- {
cmd.Printf("Migrating block %d...\n", height)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider iterating blocks in chronological order.

The current implementation iterates blocks backward from lastBlockHeight to 1. For consistency and potential dependencies between blocks, consider iterating forward instead.

-			for height := lastBlockHeight; height > 0; height-- {
+			for height := int64(1); height <= lastBlockHeight; height++ {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for height := lastBlockHeight; height > 0; height-- {
cmd.Printf("Migrating block %d...\n", height)
for height := int64(1); height <= lastBlockHeight; height++ {
cmd.Printf("Migrating block %d...\n", height)
🤖 Prompt for AI Agents
In server/migration_cmd.go around lines 68 to 69, the loop iterates blocks
backward from lastBlockHeight down to 1, which may cause issues with
dependencies between blocks. Change the loop to iterate forward from 1 up to
lastBlockHeight to process blocks in chronological order, ensuring consistency
and correct handling of dependencies.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (6)
server/migration_cmd.go (6)

39-56: Add proper resource cleanup for database connections.

The CometBFT block store and state store databases are opened but never explicitly closed, causing resource leaks.

Add defer statements to ensure proper cleanup:

 			cometBlockStore, cometStateStore, err := loadStateAndBlockStore(config)
 			if err != nil {
 				return err
 			}
+			defer func() {
+				if cometBlockStore != nil {
+					cometBlockStore.Close()
+				}
+			}()
+			defer func() {
+				if cometStateStore != nil {
+					cometStateStore.Close()
+				}
+			}()

74-75: Consider iterating blocks in chronological order.

Processing blocks backward may cause issues with dependencies between blocks and is counterintuitive for migration workflows.

-			for height := lastBlockHeight; height > 0; height-- {
+			for height := int64(1); height <= lastBlockHeight; height++ {

86-86: Fix incorrect height variable usage.

The code uses lastBlockHeight instead of the current iteration variable height.

-					extendedCommit := cometBlockStore.LoadBlockExtendedCommit(lastBlockHeight)
+					extendedCommit := cometBlockStore.LoadBlockExtendedCommit(height)

112-115: Replace panic with proper error handling.

Using panic for unimplemented functionality provides poor user experience and abruptly terminates the migration process.

-					_ = extendedCommitInfo
-					// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
-					panic("Saving extended commit info is not implemented yet")
+					// TODO: Implement SaveExtendedCommit
+					// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
+					return fmt.Errorf("saving extended commit info is not implemented yet for block height %d", height)

231-231: Fix incorrect InitialHeight assignment.

Setting InitialHeight to LastBlockHeight is incorrect - it should represent the chain's actual starting height, not the migration point.

-		InitialHeight:   uint64(cometBFTState.LastBlockHeight), // The initial height is the migration height
+		InitialHeight:   uint64(cometBFTState.InitialHeight), // Use the actual chain initial height

185-212: Add error handling for database closure.

The function opens databases but doesn't provide a way for callers to close them properly, leading to resource leaks.

Consider returning a cleanup function or modifying the signature to include cleanup responsibilities:

-func loadStateAndBlockStore(config *cfg.Config) (*store.BlockStore, state.Store, error) {
+func loadStateAndBlockStore(config *cfg.Config) (*store.BlockStore, state.Store, func(), error) {
 	dbType := dbm.BackendType(config.DBBackend)
 
 	if !os.FileExists(filepath.Join(config.DBDir(), "blockstore.db")) {
-		return nil, nil, fmt.Errorf("no blockstore found in %v", config.DBDir())
+		return nil, nil, nil, fmt.Errorf("no blockstore found in %v", config.DBDir())
 	}
 
 	// Get BlockStore
 	blockStoreDB, err := dbm.NewDB("blockstore", dbType, config.DBDir())
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, nil, err
 	}
 	blockStore := store.NewBlockStore(blockStoreDB)
 
 	if !os.FileExists(filepath.Join(config.DBDir(), "state.db")) {
+		blockStoreDB.Close()
-		return nil, nil, fmt.Errorf("no statestore found in %v", config.DBDir())
+		return nil, nil, nil, fmt.Errorf("no statestore found in %v", config.DBDir())
 	}
 
 	// Get StateStore
 	stateDB, err := dbm.NewDB("state", dbType, config.DBDir())
 	if err != nil {
+		blockStoreDB.Close()
-		return nil, nil, err
+		return nil, nil, nil, err
 	}
 	stateStore := state.NewStore(stateDB, state.StoreOptions{
 		DiscardABCIResponses: config.Storage.DiscardABCIResponses,
 	})
 
+	cleanup := func() {
+		blockStoreDB.Close()
+		stateDB.Close()
+	}
-	return blockStore, stateStore, nil
+	return blockStore, stateStore, cleanup, nil
 }
🧹 Nitpick comments (1)
server/migration_cmd.go (1)

166-166: Resolve signature handling uncertainty.

The TODO comment indicates uncertainty about signature handling, which is critical for block validation.

The signature assignment needs clarification. Would you like me to research the correct signature mapping between CometBFT and Rollkit block structures?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6595f3c and e7fa02a.

📒 Files selected for processing (1)
  • server/migration_cmd.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Test with Rollkit Chain
  • GitHub Check: test / Run Unit Tests
  • GitHub Check: lint / golangci-lint
🔇 Additional comments (1)
server/migration_cmd.go (1)

157-158: Verify header hash mapping correctness.

Both LastHeaderHash and LastCommitHash are set to the same value (block.LastCommitHash.Bytes()), which may be incorrect.

Verify that this mapping is correct by checking the Rollkit and CometBFT documentation for the expected semantics of these fields.

#!/bin/bash
# Search for header hash usage patterns in the codebase
echo "Searching for LastHeaderHash usage patterns..."
rg -A 3 -B 3 "LastHeaderHash"

echo -e "\nSearching for LastCommitHash usage patterns..."
rg -A 3 -B 3 "LastCommitHash"

Copy link
Contributor

@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: 1

♻️ Duplicate comments (2)
server/migration_cmd.go (2)

93-93: Fix incorrect height variable usage.

The code uses lastBlockHeight instead of the current iteration variable height, which will load the same extended commit for every block iteration.

-					extendedCommit := cometBlockStore.LoadBlockExtendedCommit(lastBlockHeight)
+					extendedCommit := cometBlockStore.LoadBlockExtendedCommit(height)

119-122: Replace panic with proper error handling.

Using panic for unimplemented functionality provides poor user experience and will crash the application unexpectedly.

-					_ = extendedCommitInfo
-					// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
-					panic("Saving extended commit info is not implemented yet")
+					// TODO: Implement SaveExtendedCommit
+					// rollkitStore.SaveExtendedCommit(context.Background(), header.Height(), &extendedCommitInfo)
+					return fmt.Errorf("saving extended commit info is not implemented yet")
🧹 Nitpick comments (1)
server/migration_cmd.go (1)

185-187: Add transaction validation during conversion.

The transaction conversion is performed without validation, which could propagate invalid data to the Rollkit store.

 	for _, tx := range block.Txs {
+		if len(tx) == 0 {
+			continue // Skip empty transactions
+		}
 		data.Txs = append(data.Txs, rollkittypes.Tx(tx))
 	}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7fa02a and bcc8c80.

📒 Files selected for processing (1)
  • server/migration_cmd.go (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: julienrbrt
PR: rollkit/go-execution-abci#113
File: server/migration_cmd.go:231-231
Timestamp: 2025-06-20T13:20:04.011Z
Learning: In the rollkit migration command (server/migration_cmd.go), InitialHeight is correctly set to LastBlockHeight because the Rollkit chain is technically a new chain that starts where the CometBFT chain ended, ensuring continuity during migration.
server/migration_cmd.go (1)
Learnt from: julienrbrt
PR: rollkit/go-execution-abci#113
File: server/migration_cmd.go:231-231
Timestamp: 2025-06-20T13:20:04.011Z
Learning: In the rollkit migration command (server/migration_cmd.go), InitialHeight is correctly set to LastBlockHeight because the Rollkit chain is technically a new chain that starts where the CometBFT chain ended, ensuring continuity during migration.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: lint / golangci-lint
  • GitHub Check: test / Run Unit Tests
  • GitHub Check: Test with Rollkit Chain
🔇 Additional comments (3)
server/migration_cmd.go (3)

173-174: Complete the signature implementation.

The TODO comment indicates incomplete signature handling. The current implementation assigns the proposer's signature but doesn't handle the SignedHeader signature properly.

Please clarify the intended behavior for the SignedHeader signature field. Should it be:

  1. The proposer's signature from the block (current implementation)
  2. A new signature over the Rollkit header
  3. Some other signature scheme?

This affects the integrity verification of migrated blocks.


128-128: LGTM: Proper resource cleanup implemented.

Good implementation of resource cleanup using errors.Join to ensure all stores are closed even if some close operations fail.


238-238: LGTM: Correct InitialHeight assignment.

Based on previous feedback, this correctly sets InitialHeight to LastBlockHeight since the Rollkit chain is a new chain starting where the CometBFT chain ended, ensuring migration continuity.

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.

[Feature Request]: Ensure CometBFT migration to Rollkit can be done smoothly
1 participant