Skip to content

Add bootnode component#327

Merged
canercidam merged 10 commits intomainfrom
ferranbt-bootnode
Feb 16, 2026
Merged

Add bootnode component#327
canercidam merged 10 commits intomainfrom
ferranbt-bootnode

Conversation

@ferranbt
Copy link
Contributor

This PR adds a custom bootnode component instead of having to use one of the nodes as bootnode.

@ferranbt ferranbt requested a review from canercidam as a code owner January 23, 2026 05:09
@claude
Copy link
Contributor

claude bot commented Jan 23, 2026

Claude finished @ferranbt's task in 1m 54s —— View job


Reviewing PR #327: Add bootnode component

  • Gather context and understand the changes
  • Review code quality and best practices
  • Check for bugs and edge cases
  • Evaluate security and performance
  • Provide feedback

Summary

This PR adds a dedicated bootnode component instead of using one of the nodes as a bootnode. The changes refactor the bootnode setup to use a cleaner API with ctx.UseBootnode().

Changes reviewed:

  • New Bootnode struct in components.go with Apply() method
  • New UseBootnode() helper in manifest.go for cleaner API
  • Updated AddService() signature to accept ExContext
  • Added bootnode support to RethEL component
  • Simplified bootnode setup in recipe_opstack.go
  • Registered Bootnode in catalog.go

Issues Found

🔴 Critical: Nil Pointer Dereference

The UseBootnode() method in manifest.go:185 calls bootnode.Enode.NodeID(), but bootnode.Enode is nil at this point. The Enode field is only initialized inside Bootnode.Apply() (at components.go:1024), which hasn't been called yet.

This will cause a panic at runtime.

See inline comments for the suggested fix.

What Looks Good

  • The refactoring to use ctx.UseBootnode() is a cleaner API than the previous inline setup in recipes
  • Adding bootnode support to RethEL is a good enhancement
  • The BootnodeProtocol enum provides good extensibility for future protocol versions
  • Registering the component in catalog.go follows the existing pattern

Recommendations

  1. Fix the nil pointer bug (critical - must fix before merging)
  2. Consider adding a unit test that verifies the bootnode initialization works correctly
  3. The test was removed in commit 3390c6e - consider re-adding it once the bug is fixed


e.Bootnode = &BootnodeRef{
Service: "bootnode",
ID: bootnode.Enode.NodeID(),
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Critical Bug: Nil pointer dereference

This line calls bootnode.Enode.NodeID(), but at this point bootnode.Enode is nil. The Enode field is only initialized inside the Apply() method (in components.go:1024), which hasn't been called yet.

This will cause a panic at runtime when UseBootnode() is invoked.

Suggested fix: The UseBootnode() method needs to eagerly initialize the EnodeAddr, similar to how the old code in recipe_opstack.go worked. Consider something like:

func (e *ExContext) UseBootnode() ComponentGen {
	bootnode := &Bootnode{}
	bootnode.Enode = e.Output.GetEnodeAddr()  // Initialize eagerly

	e.Bootnode = &BootnodeRef{
		Service: "bootnode",
		ID:      bootnode.Enode.NodeID(),
	}

	return bootnode
}

Then update Bootnode.Apply() to check if Enode is already set before calling ctx.Output.GetEnodeAddr():

func (b *Bootnode) Apply(ctx *ExContext) *Component {
	component := NewComponent("bootnode")

	if b.Enode == nil {
		b.Enode = ctx.Output.GetEnodeAddr()
	}
	// ... rest of the method
}

func (b *Bootnode) Apply(ctx *ExContext) *Component {
component := NewComponent("bootnode")

b.Enode = ctx.Output.GetEnodeAddr()
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: This initialization needs to be conditional to support the UseBootnode() helper method. When UseBootnode() is used, the Enode is pre-initialized to establish the BootnodeRef before Apply() runs.

Suggested change
b.Enode = ctx.Output.GetEnodeAddr()
if b.Enode == nil {
b.Enode = ctx.Output.GetEnodeAddr()
}

}

func (e *ExContext) UseBootnode() ComponentGen {
bootnode := &Bootnode{}
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing initialization: Before calling bootnode.Enode.NodeID() on line 185, you need to initialize the Enode field:

Suggested change
bootnode := &Bootnode{}
bootnode := &Bootnode{}
bootnode.Enode = e.Output.GetEnodeAddr()

@claude
Copy link
Contributor

claude bot commented Jan 26, 2026

Claude finished @ferranbt's task in 2m 18s —— View job


Reviewing PR #327: Add bootnode component

  • Gather context and understand the changes
  • Review code quality and best practices
  • Check for bugs and edge cases
  • Evaluate security and performance
  • Provide feedback

Summary

This PR adds a dedicated Bootnode component with a UseBootnode() helper method for cleaner bootnode setup in recipes.

Issues Found

🔴 Bug: Component ordering issue

In recipe_opstack.go, RethEL is added at line 79, but UseBootnode() isn't called until line 91. Since AddComponent() immediately calls Apply(), ctx.Bootnode will be nil when RethEL.Apply() checks it, and the --bootnodes flag will never be set for the L1 execution client.

See inline comment.

🔴 Bug: Enode key mismatch

In Bootnode.Apply() (line 1024), GetEnodeAddr() is called unconditionally, generating a new key even though UseBootnode() already created one. Since GetEnodeAddr() increments a counter for each call, the bootnode ends up with a different key than the one advertised to clients in ctx.Bootnode.ID.

See inline comment.

Previous Issue Status

✅ The nil pointer dereference identified in the previous review was fixed in commit a384c76.


Service: "op-geth",
ID: opGeth.Enode.NodeID(),
}
component.AddService(ctx, ctx.UseBootnode())
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: The bootnode setup on line 91 occurs after RethEL is added on line 79. Since AddComponent() immediately calls Apply(ctx), ctx.Bootnode will be nil when RethEL.Apply() runs, meaning the --bootnodes flag will never be set for the L1 execution client.

Move ctx.UseBootnode() before line 79:

func (o *OpRecipe) Apply(ctx *ExContext) *Component {
	component := NewComponent("op-recipe")

	component.AddService(ctx, ctx.UseBootnode())  // Move here

	component.AddComponent(ctx, &RethEL{})
	// ...
}

func (b *Bootnode) Apply(ctx *ExContext) *Component {
component := NewComponent("bootnode")

b.Enode = ctx.Output.GetEnodeAddr()
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: This unconditionally calls GetEnodeAddr(), which generates a new key each time (incrementing a sequence counter). However, UseBootnode() already calls GetEnodeAddr() to create the Enode and store the NodeID in ctx.Bootnode.

This means:

  1. UseBootnode() generates key readme bullets formatting #1, stores its NodeID in ctx.Bootnode.ID
  2. Bootnode.Apply() generates key CLI could print services and their ports for clarity #2 (different!), uses enode-key-2.txt

Clients will connect to the NodeID from key #1, but the bootnode will be running with key #2.

Fix: Only generate if Enode is nil:

Suggested change
b.Enode = ctx.Output.GetEnodeAddr()
if b.Enode == nil {
b.Enode = ctx.Output.GetEnodeAddr()
}

@claude
Copy link
Contributor

claude bot commented Jan 26, 2026

Claude finished @ferranbt's task in 1m 23s —— View job


Reviewing PR #327: Add bootnode component

  • Gather context and understand the changes
  • Review code quality and best practices
  • Check for bugs and edge cases
  • Evaluate security and performance
  • Provide feedback

Summary

This PR adds a dedicated Bootnode component with a UseBootnode() helper method for cleaner bootnode setup in recipes.

Issues Found

🔴 Bug 1: Component ordering - bootnode flag never set for L1 client

In recipe_opstack.go, RethEL is added at line 79, but UseBootnode() isn't called until line 91. Since AddComponent() immediately calls Apply() (see manifest.go:66), ctx.Bootnode will be nil when RethEL.Apply() runs at components.go:508.

Impact: The --bootnodes flag will never be set for the L1 execution client.

Fix: Move ctx.UseBootnode() before the first component that uses ctx.Bootnode:

func (o *OpRecipe) Apply(ctx *ExContext) *Component {
    component := NewComponent("op-recipe")

    component.AddService(ctx, ctx.UseBootnode())  // Move here FIRST

    component.AddComponent(ctx, &RethEL{})
    // ...
}

Fix this →


🔴 Bug 2: Enode key mismatch - clients connect to wrong node ID

In UseBootnode() (manifest.go:182), GetEnodeAddr() is called to create the enode and store its NodeID in ctx.Bootnode.ID. Then in Bootnode.Apply() (components.go:1024), GetEnodeAddr() is called again unconditionally, generating a different key (since each call increments a sequence counter).

Impact: Clients will try to connect to the node ID from key #1, but the bootnode service will be running with key #2.

Fix: Only generate the enode if not already set:

func (b *Bootnode) Apply(ctx *ExContext) *Component {
    component := NewComponent("bootnode")

    if b.Enode == nil {
        b.Enode = ctx.Output.GetEnodeAddr()
    }
    // ...
}

Fix this →


Previous Issue Status

✅ The nil pointer dereference identified in the first review was fixed in commit a384c76 - UseBootnode() now initializes Enode before accessing NodeID().


Notes

These two bugs were identified in my previous review but have not been addressed yet. The inline comments from that review provide additional context.

@claude
Copy link
Contributor

claude bot commented Jan 26, 2026

Claude finished @ferranbt's task in 1m 23s —— View job


Reviewing PR #327: Add bootnode component

  • Gather context and understand the changes
  • Review code quality and best practices
  • Check for bugs and edge cases
  • Evaluate security and performance
  • Provide feedback

Summary

This PR adds a dedicated Bootnode component with a UseBootnode() helper method for cleaner bootnode setup in recipes.

Previous Issues Status

Bug 1 (Component ordering) - Fixed in commit 95d6cae. UseBootnode() is now called at line 78 before RethEL is added at line 80.

Remaining Issues

🔴 Bug: Enode key mismatch - clients connect to wrong node ID

In UseBootnode() (manifest.go:182), GetEnodeAddr() is called to create the enode and store its NodeID in ctx.Bootnode.ID. Then in Bootnode.Apply() (components.go:1024), GetEnodeAddr() is called again unconditionally, overwriting b.Enode with a new key.

Since GetEnodeAddr() increments a sequence counter for each call, the bootnode ends up with a different key than the one advertised to clients.

Impact: Clients (like RethEL) will try to connect using the NodeID from key #1, but the bootnode service will be running with key #2 from the overwritten enode.

Fix: Add a nil check to preserve the pre-initialized enode:

func (b *Bootnode) Apply(ctx *ExContext) *Component {
    component := NewComponent("bootnode")

    if b.Enode == nil {
        b.Enode = ctx.Output.GetEnodeAddr()
    }
    // ...
}

See inline comment.


func (b *Bootnode) Apply(ctx *ExContext) *Component {
component := NewComponent("bootnode")

b.Enode = ctx.Output.GetEnodeAddr()
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: This unconditionally overwrites b.Enode, but UseBootnode() already initialized it at manifest.go:182 and stored the NodeID in ctx.Bootnode.ID.

Since GetEnodeAddr() increments a sequence counter, this generates a different key. Clients will connect using the NodeID from the first key, but the bootnode runs with the second key.

Suggested change
b.Enode = ctx.Output.GetEnodeAddr()
if b.Enode == nil {
b.Enode = ctx.Output.GetEnodeAddr()
}

@claude
Copy link
Contributor

claude bot commented Jan 26, 2026

Claude finished @ferranbt's task in 1m 13s —— View job


Reviewing PR #327: Add bootnode component

  • Gather context and understand the changes
  • Review code quality and best practices
  • Check for bugs and edge cases
  • Evaluate security and performance
  • Provide feedback

Summary

This PR refactors the bootnode setup to use a dedicated Bootnode component with a UseBootnode() helper for cleaner API.

Previous Issues Status

Nil pointer dereference - Fixed in commit a384c76
Component ordering - Fixed in commit 95d6cae

Remaining Issue

🔴 Bug: Enode key mismatch

In UseBootnode() (manifest.go:182), GetEnodeAddr() generates key #1 and stores its NodeID in ctx.Bootnode.ID. Then Bootnode.Apply() (components.go:1024) unconditionally calls GetEnodeAddr() again, generating key #2.

Impact: Clients connect using NodeID from key #1, but the bootnode runs with key #2.

See inline comment for the fix.


func (b *Bootnode) Apply(ctx *ExContext) *Component {
component := NewComponent("bootnode")

b.Enode = ctx.Output.GetEnodeAddr()
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Enode key mismatch

This unconditionally overwrites b.Enode, but UseBootnode() already initialized it at manifest.go:182 and stored its NodeID in ctx.Bootnode.ID.

Since GetEnodeAddr() increments a sequence counter (see artifacts.go:703), this generates a different key:

  1. UseBootnode() → key readme bullets formatting #1 (sequence=1), stores NodeID in ctx.Bootnode.ID
  2. Bootnode.Apply() → key CLI could print services and their ports for clarity #2 (sequence=2), overwrites b.Enode

Impact: Clients (like RethEL) will try to connect using the NodeID from key #1, but the bootnode service runs with key #2.

Suggested change
b.Enode = ctx.Output.GetEnodeAddr()
if b.Enode == nil {
b.Enode = ctx.Output.GetEnodeAddr()
}

@claude
Copy link
Contributor

claude bot commented Feb 16, 2026

Claude finished @canercidam's task in 2m 43s —— View job


Reviewing PR #327: Add bootnode component

  • Gather context and understand the changes
  • Review code quality and check for bugs
  • Check status of previously reported issues
  • Provide feedback

Summary

This PR adds a dedicated Bootnode component and refactors the opstack recipe to use it instead of repurposing op-geth as a bootnode. The latest commit (ab2822f) simplifies the approach by removing the UseBootnode() helper and having Bootnode.Apply() directly set ctx.Bootnode.

Previous Issues Status

  • Nil pointer dereference - Fixed (removed UseBootnode() entirely)
  • Component ordering - Fixed (Bootnode is added first at line 84)
  • Enode key mismatch - Fixed (GetEnodeAddr() is now called only once, inside Apply())

Remaining Issues

Side effect in Apply() method

Bootnode.Apply() mutates ctx.Bootnode as a side effect (components.go:1092-1096). No other component does this - all other components read from ctx but don't write to it. This creates an implicit ordering requirement: Bootnode must be added before any component that reads ctx.Bootnode, but nothing enforces or documents this.

This works correctly in the current code since Bootnode is added first in recipe_opstack.go:84, but it's a fragile pattern. If someone reorders the AddComponent calls or adds a bootnode to another recipe, the behavior depends on insertion order with no compile-time or runtime safety net.

The previous UseBootnode() approach (before the enode key bug) was actually closer to the right design - it made the ordering explicit by separating the context setup from the component creation. Consider either:

  • Re-introducing UseBootnode() but having it pass the pre-initialized Enode into Bootnode to avoid the double-GetEnodeAddr() bug
  • Adding a comment on the Apply() method documenting the ordering constraint

AddComponent vs AddService are identical

AddComponent and AddService on Component have identical implementations (manifest.go:68-74). The recipe uses AddComponent for Bootnode (line 84) but AddService for OpGeth (line 172). This inconsistency is confusing since there's no semantic difference between the two methods. This predates this PR but the new code adds to the inconsistency.


@canercidam canercidam merged commit 874bd08 into main Feb 16, 2026
17 checks passed
@canercidam canercidam deleted the ferranbt-bootnode branch February 16, 2026 15:11
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.

2 participants