Skip to content

Enable Cubist Signer integration #3965

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 51 commits into
base: master
Choose a base branch
from
Open

Conversation

geoff-vball
Copy link
Contributor

@geoff-vball geoff-vball commented May 15, 2025

Why this should be merged

The enables the rpc-signer configuration.

This is based off of this old PR.

How this works

I moved the instantiation of the signer from the config package to node.New() where we instantiate everything else that needs filesystem/networking. This way we don't need to propagate cleanup all over the place.

I also opted for a Shutdown() method on bls.Signer, which is how the other resources attached to node are managed. Currently, resources are not shutdown if we fail in another part of node.New(), but this is how the rest of the resources are also managed.

Out of scope for this PR, but we may want to consider some sort of registry of cleanup/shutdown functions that can be unwound whenever we fail.

How this was tested

Unit tests

Need to be documented in RELEASES.md?

Yes?

richardpringle and others added 30 commits March 12, 2025 14:05
The code was changed without changing the behaviour. Instead of
`os.Stat`ing the file, we just try to read the file (which returned the
same error type).
Prior to this, we were checking if the file existed before attempting to
readd it. It was unnecessary and the logic has been removed.
@geoff-vball geoff-vball marked this pull request as ready for review May 28, 2025 13:14
@StephenButtolph StephenButtolph requested review from a team and maru-ava and removed request for a team May 28, 2025 14:53
@geoff-vball geoff-vball changed the title Signers config wip Enable Cubist Signer integration May 28, 2025
Copy link
Contributor

@cam-schultz cam-schultz left a comment

Choose a reason for hiding this comment

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

Treating the signer the same way as the other Node composite fields makes sense to me. I left a few questions/comments about the config parsing pattern.

config/config.go Outdated

default:
return node.SignerPathConfig{
SigningKeyPath: getExpandedArg(v, StakingSignerKeyPathKey),
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to confirm, if StakingSignerKeyPathKey is not set, do we use the default path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's my understanding? But I'm not 100% how this expands to the default path - I just took the existing logic 😅

Comment on lines 73 to 75
if cfg.SignerPathIsSet {
return nil, errMissingStakingSigningKeyFile
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love this pattern of embedding metadata into the config struct that's only used for internal control flow. One alternative would be to return a signerPathIsSet bool from getStakingSignerConfig and pass that around, but dedicating an entire return parameter that needs to be managed by the caller for one specific configuration type is also cumbersome. I mainly wanted to call this out as a slightly awkward implementation, that may not have a cleaner alternative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I don't think passing a return parameter around is a good choice. Would renaming the boolean make more sense? I just kind of went off what was there, but I think "fromExistingFile" might make more sense?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think a private field and a getter makes sense. I agree that I wouldn't add a return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A private field in StakingConfig or in SignerPathConfig?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking in SignerPathConfig but I now realize that this is in node package instead of config which would require a constructor or a setter. I don't feel strongly about this either way but do prefer not serializing it (by omitting the json tag) if it's left public.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another idea is to just make a different type called DefaultSignerConfig so we could switch on that instead of having to do this hack + avoid the ok var passing

Copy link
Contributor

@iansuvak iansuvak left a comment

Choose a reason for hiding this comment

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

The logic LGTM

@StephenButtolph StephenButtolph requested a review from a team June 5, 2025 18:13
@joshua-kim joshua-kim requested review from joshua-kim and removed request for maru-ava June 5, 2025 18:43
@joshua-kim joshua-kim moved this to In Progress 🏗️ in avalanchego Jun 10, 2025
config/config.go Outdated
return config, nil
}

func getStakingSignerConfig(v *viper.Viper) (interface{}, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This PR uses this a lot, but as of go 1.18 it's idiomatic to use the any alias for interface{}

var errMissingStakingSigningKeyFile = errors.New("missing staking signing key file")

// GetStakingSigner returns a BLS signer based on the provided configuration.
func GetStakingSigner(
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. go's idiom is to name factory functions New*, so I think this should be NewStakingSigner
  2. Location looks a bit awkward to me - the package is the unit of abstraction and this is the node deciding which signer to use based off of the user-provided config, so I think this should live as a non-exported function in node + all of the config types we created should also be non-exported.

@@ -549,6 +551,79 @@ func TestGetSubnetConfigsFromFlags(t *testing.T) {
}
}

func TestGetStakingSigner(t *testing.T) {
testKey := "HLimS3vRibTMk9lZD4b+Z+GLuSBShvgbsu0WTLt2Kd4="
type cfg map[string]any
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we don't need this type alias

config, err := GetNodeConfig(v)

require.ErrorIs(err, tt.expectedErr)
require.IsType(tt.expectedSignerConfigType, config.StakingSignerConfig)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just do an check on the expected config instead of just a type check?

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 was slightly annoying because of the way temp dirs work in the test suite. Let me know if you think it's better or worse.

expectedErr error
}{
{
name: "default-signer",
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove the kebab-casing in these test names? Plain strings work fine and there's no reason to format it this way IMO

}

pkBytes := pubkeyResponse.GetPublicKey()
pk, err := bls.PublicKeyFromCompressedBytes(pkBytes)
if err != nil {
return nil, err
return nil, errors.Join(fmt.Errorf("failed to uncompress public key bytes: %w", err), conn.Close())
Copy link
Contributor

Choose a reason for hiding this comment

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

Same nit


return bls.SignatureFromBytes(signature)
sigBytes := resp.GetSignature()
return bls.SignatureFromBytes(sigBytes)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're now wrapping the previous error I feel like it's weird for us to not wrap this error


return bls.SignatureFromBytes(signature)
sigBytes := resp.GetSignature()
return bls.SignatureFromBytes(sigBytes)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same nit here

}

func (c *Client) Shutdown() error {
return c.connection.Close()
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar nit here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does wrapping an error in a function that only has one possible return provide any meaningful information?

Copy link
Contributor

Choose a reason for hiding this comment

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

Although there aren't any other errors in this scope, if we don't wrap this error it's not disambiguated from other grpc connection close errors. As an example, if we had another grpc conn in avalanchego and its conn close failed, we might lose visibility into which conn was the one we had to debug.

func createSignerFromFile(signingKeyPath string) (bls.Signer, error) {
signingKeyBytes, err := os.ReadFile(signingKeyPath)
if err != nil {
return nil, err
Copy link
Contributor

Choose a reason for hiding this comment

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

same wrapping nit

}, nil
}

func (c *Client) PublicKey() *bls.PublicKey {
return c.pk
}

// Sign a message. The [Client] already handles transient connection errors.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would move the comment w.r.t transient conn errors to NewClient or Client since it's applicable to the type and not just Sign

}

// [SignProofOfPossession] has the same behavior as [Sign] but will product a different signature.
Copy link
Contributor

Choose a reason for hiding this comment

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

Square brackets are so confusing... my understanding is:

  1. This is a newer convention that auto links in godoc
  2. It only works if it's linked properly + exported out of the package, so stuff a package foo that can do [Foo] (if Foo is an exported type in foo) and [bar.Bar], but if Foo.Baz is a method then you have to specify the type and can't just reference Baz.

So if you want to use square brackets I think you need to prefix the type (i.e Client.SignProofOfPossession and Client.Sign), or it'll break in godoc. I think you could alternatively just not use square brackets.

Regarding the actual comment, I think we can just say it produces a ProofOfPossession signature instead of different, since it's not clear what that difference is + we document PoP elsewhere in the repo iirc.

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 think I removed them all. I haven't used these at all before - they're from the portion of the code Richard wrote

geoff-vball and others added 5 commits June 17, 2025 11:59
Co-authored-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com>
Signed-off-by: Geoff Stuart <geoff.vball@gmail.com>
@geoff-vball geoff-vball requested a review from joshua-kim June 17, 2025 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress 🏗️
Development

Successfully merging this pull request may close these issues.

5 participants