wallet: add sign methods to vault interface#1279
Conversation
74b08b3 to
588cfd1
Compare
|
@Abdulkbk ping for review |
|
No issues found by |
| Keyvault accesses persistence through `db.Store`. It does not talk directly to | ||
| SQL backends. | ||
|
|
||
| The vault API also includes digest level signing methods. `DerivePubKey`, |
| } | ||
|
|
||
| // SchnorrKeyTweak describes an additive secp256k1 tweak for Schnorr signing. | ||
| type SchnorrKeyTweak struct { |
There was a problem hiding this comment.
Can we make the conversion semantics explicit here? The existing signer code models taproot tweaks as raw []byte, while this type adds a fixed 32-byte scalar and NormalizeEvenY. That may be the right vault-level shape, but the signer-to-vault adapter needs to define exactly how current taproot tweak bytes map into this descriptor so delegation cannot silently change the tweak being signed with. (gpt-5.5)
There was a problem hiding this comment.
Changed to the modular scalar to be explicit.
There was a problem hiding this comment.
Adding a bit more detail here: the current shape makes the vault boundary take
the final additive scalar directly.
SchnorrKeyTweak.Scalar is a btcec.ModNScalar, so callers must pass the
already-computed secp256k1 tweak scalar. It is intentionally not a Taproot script
root, merkle root, or BIP341 tweak input byte string.
So any signer-to-vault adapter remains responsible for converting existing raw
Taproot tweak inputs into that final scalar before constructing the
TweakedSchnorrRequest. The vault only applies the scalar tweak and signs the
caller-computed digest.
| // corresponding public key without exposing private key material. | ||
| DerivePubKey(ctx context.Context, ref KeyLocator) (*btcec.PublicKey, error) | ||
|
|
||
| // SignECDSA signs a caller-computed 32-byte digest for the provided key |
There was a problem hiding this comment.
Can the PR document the intended layering between this primitive signing surface and the existing intent-based signer API? Keeping SignDigestIntent in the signer and unpacking it into SignECDSA, SignCompactECDSA, SignSchnorr, or SignTweakedSchnorr makes sense, but the API docs should make clear that the signer owns intent validation and dispatch while the vault intentionally exposes only primitive signing methods. (gpt-5.5)
There was a problem hiding this comment.
Created a new wallet/signing package so the vault and public signer can share the same API.
Since the vault needs to support the same signing operations exposed by the public signer, this helps avoid duplicated types and extra conversions between equivalent request and response structs.
There was a problem hiding this comment.
Also clarified the layering in the ADR and Vault.Sign docs.
The wallet/signer layer owns the higher-level intent work: choosing the signing
intent, constructing the digest, selecting sighash semantics, assembling
transactions/PSBTs/witnesses, deciding serialization format, and appending any
sighash byte.
The vault boundary stays primitive: it receives a sealed signing.Request for a
located key and caller-computed 32-byte digest, performs the private-key
operation, and returns a primitive signing.Signature.
There was a problem hiding this comment.
I am confused about the design - if the signer.go and the keyvault are using the same struct, like sign request param to pass info, there must be sth wrong with the abstraction as how can both the public API and the private internal vault require the same set of info? I think it's better to open another PR to show how it's actually used in the wallet/signer.go first.
| ScriptPubKey *ScriptPubKeyRef | ||
| } | ||
|
|
||
| // FullHDKeyRef identifies a complete wallet-root derivation path. |
There was a problem hiding this comment.
This should probably reuse the existing derivation types instead of flattening them. The flattened form loses the InternalAccount/Account split and MasterKeyFingerprint; InternalAccount is what DeriveFromKeyPath* uses to load the account row, so this API cannot round-trip the current wallet signing path. Consider modeling this as waddrmgr.KeyScope plus waddrmgr.DerivationPath or a local wrapper that embeds both so the vault locator preserves the full derivation identity. (gpt-5.5)
There was a problem hiding this comment.
It intentionally does not use InternalAccount or MasterKeyFingerprint, because we do not need them here. Those fields are tied to the legacy kvdb flow.
I changed it to use the new waddrmgr.AddrScope, which includes the full path.
|
|
||
| // KeyLocator identifies a key using one of the vault's supported reference | ||
| // forms. | ||
| type KeyLocator struct { |
There was a problem hiding this comment.
will it make sense to specify what implementations must do when zero or multiple are set and maybe adding a Validate helper to enforce how this will be used?
There was a problem hiding this comment.
It was supposed to use only one reference at a time. I converted it to sealed structures behind an interface, so now this is guaranteed by construction, and callers cannot accidentally provide multiple references.
| // | ||
| // The caller provides a wallet key locator, and the vault returns the | ||
| // corresponding public key without exposing private key material. | ||
| DerivePubKey(ctx context.Context, ref KeyLocator) (*btcec.PublicKey, error) |
There was a problem hiding this comment.
nit: can specify wether the vault needs be locked/unlocked for the newly added medthods.
| func (v *WalletVault) DerivePubKey(context.Context, | ||
| KeyLocator) (*btcec.PublicKey, error) { | ||
|
|
||
| return nil, ErrVaultSigningUnimplemented |
There was a problem hiding this comment.
not important atm, DerivePubKey isn't signing, so ErrVaultSigningUnimplemented doesn't apply.
There was a problem hiding this comment.
yeah, that's just a temporary error until the next PR. Renamed.
588cfd1 to
c7543c8
Compare
ee4b8fb to
c7543c8
Compare
Coverage Report for CI Build 29040784850Warning No base build found for commit Coverage: 55.559%Details
Uncovered Changes
Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
|
Added a new This should let us remove some duplicated code later and keep the signing request and response types in one place. |
| func TestECDSASignatureSerializeNil(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // Given |
There was a problem hiding this comment.
u mean removing or renaming to "Arrange, Act, Assert"?
There was a problem hiding this comment.
removed, btw
yyforyongyu
left a comment
There was a problem hiding this comment.
Cool. I think the current wallet/signing looks premature without another PR to show how it can be applied to wallet/signer.go first. It's weird that both the public Signer and the internal keyvault can share the same signing request, tho not necessarily wrong - either the public layer is a pass-through, so a slim, shallow layer, or the internal layer is leaking. So it will be better to show how this new pkg will be applied first, otherwise I think wallet/signing should be removed and put everything under the internal/keyvault.
There is still the option that we can revert to the previous version where we have individual methods like SignECDSA, SignSchnorr, etc., but without a concrete implementation first it's hard to tell.
| `ScriptPubKeyLocator` for raw or imported rows without exposing private key | ||
| material or database routing. | ||
|
|
||
| Wallet code builds a sealed `wallet/signing.Request` containing both the key |
There was a problem hiding this comment.
is there another consumer of signing? I don't see the necessity of making it a public module, nor making it an independent module. We can just put all the code under wallet/internal/keyvault as the vault is the only consumer?
| // corresponding public key without exposing private key material. | ||
| DerivePubKey(ctx context.Context, ref KeyLocator) (*btcec.PublicKey, error) | ||
|
|
||
| // SignECDSA signs a caller-computed 32-byte digest for the provided key |
There was a problem hiding this comment.
I am confused about the design - if the signer.go and the keyvault are using the same struct, like sign request param to pass info, there must be sth wrong with the abstraction as how can both the public API and the private internal vault require the same set of info? I think it's better to open another PR to show how it's actually used in the wallet/signer.go first.
|
@yyforyongyu I think the root issue is that the current I actually like the shape where the public signer becomes a thin adapter: // SignDigest signs a message digest based on the provided request.
func (w *Wallet) SignDigest(ctx context.Context,
request signing.Request) (signing.Signature, error) {
if err := w.state.canSign(); err != nil {
return nil, err
}
return w.keyVault.Sign(ctx, request)
}To me, this makes the layering pretty clear: the wallet signer performs So I think there are two reasonable directions:
Personally, I like the thin Mainly, I want to avoid continuing to go back and forth on the API shape in this |
c7543c8 to
4adf634
Compare
4adf634 to
96241f0
Compare
Adds a digest-level signing surface to the wallet keyvault boundary so future
signing paths can keep private key operations behind the vault.
This PR introduces a shared
wallet/signingpackage with:waddrmgr.AddrScopeSerialize()helpersThe keyvault interface is extended with:
DerivePubKeySignSignaccepts a sealedsigning.Requestcontaining a key locator, acaller-computed 32-byte digest, and the primitive signing mode. Requests do not
carry transactions, PSBTs, scripts, witnesses, sighash types, or Taproot tree
data. Digest construction, transaction assembly, signature serialization format,
and appended sighash-byte handling remain wallet-layer responsibilities.
Tweaked Schnorr signing uses an already-computed additive secp256k1 scalar plus
an even-Y normalization flag. It does not accept Taproot script roots, merkle
roots, or BIP341 tweak input bytes.
WalletVaultcurrently returns a generic key-operation unimplemented sentinelfor derivation and signing until the backing implementation is added.
The vault mock was moved into its own
mock/vaultpackage to avoid an importcycle with
waddrmgr.