node: calculate digests using Vaa type or using message prefix

This commit is contained in:
Conor Patrick 2023-02-23 02:31:15 +00:00
parent 64fb51d68d
commit d4e0445785
17 changed files with 227 additions and 57 deletions

View File

@ -320,7 +320,7 @@ func (s *nodePrivilegedService) InjectGovernanceVAA(ctx context.Context, req *no
}
// Generate digest of the unsigned VAA.
digest := v.SigningMsg()
digest := v.SigningDigest()
s.logger.Info("governance VAA constructed",
zap.Any("vaa", v),

View File

@ -68,7 +68,7 @@ func runGovernanceVAAVerify(cmd *cobra.Command, args []string) {
log.Fatalf("invalid update: %v", err)
}
digest := v.SigningMsg().Bytes()
digest := v.SigningDigest().Bytes()
if err != nil {
panic(err)
}

View File

@ -331,7 +331,10 @@ func SubmitObservationsToContract(
return nil, fmt.Errorf("acct: failed to marshal accountant observation request: %w", err)
}
digest := vaa.SigningMsg(append(submitObservationPrefix, bytes...))
digest, err := vaa.MessageSigningDigest(submitObservationPrefix, bytes)
if err != nil {
return nil, fmt.Errorf("acct: failed to sign accountant Observation request: %w", err)
}
sigBytes, err := ethCrypto.Sign(digest.Bytes(), gk)
if err != nil {

View File

@ -152,6 +152,6 @@ func (msg *MessagePublication) CreateVAA(gsIndex uint32) *vaa.VAA {
func (msg *MessagePublication) CreateDigest() string {
v := msg.CreateVAA(0) // The guardian set index is not part of the digest, so we can pass in zero.
db := v.SigningMsg()
db := v.SigningDigest()
return hex.EncodeToString(db.Bytes())
}

View File

@ -680,6 +680,6 @@ func (tk tokenKey) String() string {
func (gov *ChainGovernor) HashFromMsg(msg *common.MessagePublication) string {
v := msg.CreateVAA(0) // We can pass zero in as the guardian set index because it is not part of the digest.
digest := v.SigningMsg()
digest := v.SigningDigest()
return hex.EncodeToString(digest.Bytes())
}

View File

@ -104,7 +104,7 @@ func (d *DiscordNotifier) LookupGroupID(groupName string) (string, error) {
type Observation interface {
GetEmitterChain() vaa.ChainID
MessageID() string
SigningMsg() common.Hash
SigningDigest() common.Hash
}
func (d *DiscordNotifier) MissingSignaturesOnObservation(o Observation, hasSigs, wantSigs int, quorum bool, missing []string) error {
@ -145,7 +145,7 @@ func (d *DiscordNotifier) MissingSignaturesOnObservation(o Observation, hasSigs,
Title: "Message with missing signatures",
Fields: []discord.EmbedField{
{Name: "Message ID", Value: wrapCode(o.MessageID()), Inline: true},
{Name: "Digest", Value: wrapCode(hex.EncodeToString(o.SigningMsg().Bytes())), Inline: true},
{Name: "Digest", Value: wrapCode(hex.EncodeToString(o.SigningDigest().Bytes())), Inline: true},
{Name: "Quorum", Value: quorumText, Inline: true},
{Name: "Source Chain", Value: caser.String(o.GetEmitterChain().String()), Inline: false},
{Name: "Missing Guardians", Value: missingText.String(), Inline: false},

View File

@ -28,7 +28,7 @@ func (p *Processor) broadcastSignature(
signature []byte,
txhash []byte,
) {
digest := o.SigningMsg()
digest := o.SigningDigest()
obsv := gossipv1.SignedObservation{
Addr: crypto.PubkeyToAddress(p.gk.PublicKey).Bytes(),
Hash: digest.Bytes(),

View File

@ -25,7 +25,7 @@ var (
// handleInjection processes a pre-populated VAA injected locally.
func (p *Processor) handleInjection(ctx context.Context, v *vaa.VAA) {
// Generate digest of the unsigned VAA.
digest := v.SigningMsg()
digest := v.SigningDigest()
// The internal originator is responsible for logging the full VAA, just log the digest here.
supervisor.Logger(ctx).Info("signing injected VAA",

View File

@ -129,7 +129,7 @@ func (p *Processor) handleMessage(ctx context.Context, k *common.MessagePublicat
}
// Generate digest of the unsigned VAA.
digest := v.SigningMsg()
digest := v.SigningDigest()
// Sign the digest using our node's guardian key.
s, err := crypto.Sign(digest.Bytes(), p.gk)

View File

@ -227,7 +227,7 @@ func (p *Processor) handleInboundSignedVAAWithQuorum(ctx context.Context, m *gos
}
// Calculate digest for logging
digest := v.SigningMsg()
digest := v.SigningDigest()
hash := hex.EncodeToString(digest.Bytes())
if p.gs == nil {

View File

@ -30,9 +30,9 @@ type (
GetEmitterChain() vaa.ChainID
// MessageID returns a human-readable emitter_chain/emitter_address/sequence tuple.
MessageID() string
// SigningMsg returns the hash of the signing body of the observation. This is used
// SigningDigest returns the hash of the hash signing body of the observation. This is used
// for signature generation and verification.
SigningMsg() ethcommon.Hash
SigningDigest() ethcommon.Hash
// IsReliable returns whether this message is considered reliable meaning it can be reobserved.
IsReliable() bool
// HandleQuorum finishes processing the observation once a quorum of signatures have

View File

@ -617,7 +617,7 @@ func UnmarshalBatch(data []byte) (*BatchVAA, error) {
// check for malformed data - verify that the hash of the observation matches what was supplied
// the guardian has no interest in or use for observations after the batch has been signed, but still check
obsHash := headless.SigningMsg()
obsHash := headless.SigningDigest()
if obsHash != v.Hashes[obsvIndex] {
return nil, fmt.Errorf(
"BatchVAA Observation %v does not match supplied hash", obsvIndex)
@ -652,21 +652,42 @@ func (v *BatchVAA) signingBody() []byte {
return buf.Bytes()
}
// SigningMsg returns the hash of the signing body.
func SigningMsg(data []byte) common.Hash {
func doubleKeccak(bz []byte) common.Hash {
// In order to save space in the solana signature verification instruction, we hash twice so we only need to pass in
// the first hash (32 bytes) vs the full body data.
return crypto.Keccak256Hash(crypto.Keccak256Hash(data).Bytes())
return crypto.Keccak256Hash(crypto.Keccak256Hash(bz).Bytes())
}
// SigningMsg returns the hash of the signing body. This is used for signature generation and verification
func (v *VAA) SigningMsg() common.Hash {
return SigningMsg(v.signingBody())
// This is a temporary method to produce a vaa signing digest on raw bytes.
// It is error prone and we should use `v.SigningDigest()` instead.
// whenever possible.
// This will be removed in a subsequent release.
func DeprecatedSigningDigest(bz []byte) common.Hash {
return doubleKeccak(bz)
}
// SigningMsg returns the hash of the signing body. This is used for signature generation and verification
func (v *BatchVAA) SigningMsg() common.Hash {
return SigningMsg(v.signingBody())
// MessageSigningDigest returns the hash of the data prepended with it's signing prefix.
// This is intending to be used for signing messages of different types from VAA's.
// The message prefix help protect from message collisions.
func MessageSigningDigest(prefix []byte, data []byte) (common.Hash, error) {
if len(prefix) < 32 {
// Prefixes must be at least 32 bytes
// https://github.com/wormhole-foundation/wormhole/blob/main/whitepapers/0009_guardian_key.md
return common.Hash([32]byte{}), errors.New("prefix must be atleast 32 bytes")
}
return crypto.Keccak256Hash(prefix[:], data), nil
}
// SigningDigest returns the hash of the vaa hash to be signed directly.
// This is used for signature generation and verification
func (v *VAA) SigningDigest() common.Hash {
return doubleKeccak(v.signingBody())
}
// BatchSigningDigest returns the hash of the batch vaa hash to be signed directly.
// This is used for signature generation and verification
func (v *BatchVAA) SigningDigest() common.Hash {
return doubleKeccak(v.signingBody())
}
// ObsvHashArray creates an array of hashes of Observation.
@ -675,7 +696,7 @@ func (v *BatchVAA) ObsvHashArray() []common.Hash {
hashes := make([]common.Hash, len(v.Observations))
for _, msg := range v.Observations {
obsIndex := msg.Index
hashes[obsIndex] = msg.Observation.SigningMsg()
hashes[obsIndex] = msg.Observation.SigningDigest()
}
return hashes
@ -683,9 +704,10 @@ func (v *BatchVAA) ObsvHashArray() []common.Hash {
// Verify Signature checks that the provided address matches the address that created the signature for the provided digest
// Digest should be the output of SigningMsg(data).Bytes()
func VerifySignature(digest []byte, signature *Signature, address common.Address) bool {
// Should not be public as other message types should be verified using a message prefix.
func verifySignature(vaa_digest []byte, signature *Signature, address common.Address) bool {
// retrieve the address that signed the data
pubKey, err := crypto.Ecrecover(digest, signature.Signature[:])
pubKey, err := crypto.Ecrecover(vaa_digest, signature.Signature[:])
if err != nil {
return false
}
@ -696,7 +718,8 @@ func VerifySignature(digest []byte, signature *Signature, address common.Address
}
// Digest should be the output of SigningMsg(data).Bytes()
func VerifySignatures(digest []byte, signatures []*Signature, addresses []common.Address) bool {
// Should not be public as other message types should be verified using a message prefix.
func verifySignatures(vaa_digest []byte, signatures []*Signature, addresses []common.Address) bool {
if len(addresses) < len(signatures) {
return false
}
@ -717,7 +740,7 @@ func VerifySignatures(digest []byte, signatures []*Signature, addresses []common
// verify this signature
addr := addresses[sig.Index]
ok := VerifySignature(digest, sig, addr)
ok := verifySignature(vaa_digest, sig, addr)
if !ok {
return false
}
@ -734,16 +757,34 @@ func VerifySignatures(digest []byte, signatures []*Signature, addresses []common
return true
}
// Operating on bytes directly is error prone. We should use `vaa.VerifyingSignatures()` whenever possible.
// This function will be removed in a subsequent release.
func DeprecatedVerifySignatures(vaaBody []byte, signatures []*Signature, addresses []common.Address) bool {
vaaDigest := doubleKeccak(vaaBody)
return verifySignatures(vaaDigest[:], signatures, addresses)
}
func VerifyMessageSignature(prefix []byte, messageBody []byte, signatures *Signature, addresses common.Address) bool {
if len(prefix) < 32 {
return false
}
msgDigest, err := MessageSigningDigest(prefix, messageBody)
if err != nil {
return false
}
return verifySignature(msgDigest[:], signatures, addresses)
}
// VerifySignatures verifies the signature of the VAA given the signer addresses.
// Returns true if the signatures were verified successfully.
func (v *VAA) VerifySignatures(addresses []common.Address) bool {
return VerifySignatures(v.SigningMsg().Bytes(), v.Signatures, addresses)
return verifySignatures(v.SigningDigest().Bytes(), v.Signatures, addresses)
}
// VerifySignatures verifies the signature of the BatchVAA given the signer addresses.
// Returns true if the signatures were verified successfully.
func (v *BatchVAA) VerifySignatures(addresses []common.Address) bool {
return VerifySignatures(v.SigningMsg().Bytes(), v.Signatures, addresses)
return verifySignatures(v.SigningDigest().Bytes(), v.Signatures, addresses)
}
// Marshal returns the binary representation of the BatchVAA
@ -798,7 +839,7 @@ func (v *BatchVAA) serializeBody() []byte {
// - The signatures in the VAA is verified against the guardian set keys.
func (v *VAA) Verify(addresses []common.Address) error {
if addresses == nil {
return errors.New("No addresses were provided")
return errors.New("no addresses were provided")
}
// Check if VAA doesn't have any signatures
@ -906,12 +947,12 @@ func (v *BatchVAA) GetTransactionID() common.Hash {
// HexDigest returns the hex-encoded digest.
func (v *VAA) HexDigest() string {
return hex.EncodeToString(v.SigningMsg().Bytes())
return hex.EncodeToString(v.SigningDigest().Bytes())
}
// HexDigest returns the hex-encoded digest.
func (b *BatchVAA) HexDigest() string {
return hex.EncodeToString(b.SigningMsg().Bytes())
return hex.EncodeToString(b.SigningDigest().Bytes())
}
/*
@ -932,7 +973,7 @@ func (v *VAA) serializeBody() []byte {
}
func (v *VAA) AddSignature(key *ecdsa.PrivateKey, index uint8) {
sig, err := crypto.Sign(v.SigningMsg().Bytes(), key)
sig, err := crypto.Sign(v.SigningDigest().Bytes(), key)
if err != nil {
panic(err)
}
@ -948,7 +989,7 @@ func (v *VAA) AddSignature(key *ecdsa.PrivateKey, index uint8) {
// creates signature of BatchVAA.Hashes and adds it to BatchVAA.Signatures.
func (v *BatchVAA) AddSignature(key *ecdsa.PrivateKey, index uint8) {
sig, err := crypto.Sign(v.SigningMsg().Bytes(), key)
sig, err := crypto.Sign(v.SigningDigest().Bytes(), key)
if err != nil {
panic(err)
}

View File

@ -328,7 +328,7 @@ func TestSigningBody(t *testing.T) {
func TestSigningMsg(t *testing.T) {
vaa := getVaa()
expected := common.HexToHash("4fae136bb1fd782fe1b5180ba735cdc83bcece3f9b7fd0e5e35300a61c8acd8f")
assert.Equal(t, vaa.SigningMsg(), expected)
assert.Equal(t, vaa.SigningDigest(), expected)
}
func TestMessageID(t *testing.T) {

View File

@ -65,7 +65,7 @@ func TestVerifySignature(t *testing.T) {
Payload: []byte("abcd"),
}
data := v.SigningMsg()
data := v.SigningDigest()
key, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
require.NoError(t, err)

View File

@ -2,6 +2,7 @@ package keeper
import (
"encoding/json"
"fmt"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
@ -17,18 +18,35 @@ func NewCustomQueryHandler(keeper Keeper) *wasmkeeper.QueryPlugins {
}
type WormholeQuery struct {
VerifyQuorum *verifyQuorumParams `json:"verify_quorum,omitempty"`
VerifySignature *verifySignatureParams `json:"verify_signature,omitempty"`
// This is deprecated and will be removed in a subsequent release
// because it uses an error-prone verification interface.
VerifyQuorum *verifyQuorumParams `json:"verify_quorum,omitempty"`
// Verify the signatures on a VAA.
// Successor to `VerifyQuorum` as the verification uses a safe interface.
VerifyVaa *verifyVaaParams `json:"verify_vaa,omitempty"`
// Verify the signatures on a message with a given message prefix.
// The caller should take care not to allow outside sources to choose the prefix.
VerifyMessageSignature *verifyMessageSignatureParams `json:"verify_message_signature,omitempty"`
// Calculate the minimum number of participants required in quorum for the latest guardian set.
CalculateQuorum *calculateQuorumParams `json:"calculate_quorum,omitempty"`
}
// deprecated
type verifyQuorumParams struct {
Data []byte `json:"data"`
GuardianSetIndex uint32 `json:"guardian_set_index"`
Signatures []*vaa.Signature `json:"signatures"`
}
type verifySignatureParams struct {
type verifyVaaParams struct {
Vaa []byte
}
type verifyMessageSignatureParams struct {
Prefix []byte `json:"prefix"`
Data []byte `json:"data"`
GuardianSetIndex uint32 `json:"guardian_set_index"`
Signature *vaa.Signature `json:"signature"`
@ -45,20 +63,37 @@ func WormholeQuerier(keeper Keeper) func(ctx sdk.Context, data json.RawMessage)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
fmt.Println("-- WE GOT QUERIED")
if wormholeQuery.VerifyQuorum != nil {
// handle the verify quorum query
digest := vaa.SigningMsg(wormholeQuery.VerifyQuorum.Data).Bytes()
err := keeper.VerifyQuorum(ctx, digest, wormholeQuery.VerifyQuorum.GuardianSetIndex, wormholeQuery.VerifyQuorum.Signatures)
// verify vaa using deprecated method
err := keeper.DeprecatedVerifyVaa(ctx, wormholeQuery.VerifyQuorum.Data, wormholeQuery.VerifyQuorum.GuardianSetIndex, wormholeQuery.VerifyQuorum.Signatures)
if err != nil {
return nil, err
}
return []byte("{}"), nil
}
if wormholeQuery.VerifySignature != nil {
// handle the verify signature query
digest := vaa.SigningMsg(wormholeQuery.VerifySignature.Data).Bytes()
err := keeper.VerifySignature(ctx, digest, wormholeQuery.VerifySignature.GuardianSetIndex, wormholeQuery.VerifySignature.Signature)
if wormholeQuery.VerifyVaa != nil {
// verify vaa using recommended method
v, err := vaa.Unmarshal(wormholeQuery.VerifyVaa.Vaa)
if err != nil {
return nil, err
}
err = keeper.VerifyVAA(ctx, v)
if err != nil {
return nil, err
}
return []byte("{}"), nil
}
if wormholeQuery.VerifyMessageSignature != nil {
// handle the verify message signature query
err := keeper.VerifyMessageSignature(
ctx,
wormholeQuery.VerifyMessageSignature.Prefix,
wormholeQuery.VerifyMessageSignature.Data,
wormholeQuery.VerifyMessageSignature.GuardianSetIndex,
wormholeQuery.VerifyMessageSignature.Signature,
)
if err != nil {
return nil, err
}

View File

@ -40,7 +40,7 @@ func (k Keeper) CalculateQuorum(ctx sdk.Context, guardianSetIndex uint32) (int,
return CalculateQuorum(len(guardianSet.Keys)), &guardianSet, nil
}
func (k Keeper) VerifySignature(ctx sdk.Context, data []byte, guardianSetIndex uint32, signature *vaa.Signature) error {
func (k Keeper) VerifyMessageSignature(ctx sdk.Context, prefix []byte, data []byte, guardianSetIndex uint32, signature *vaa.Signature) error {
// Calculate quorum and retrieve guardian set
_, guardianSet, err := k.CalculateQuorum(ctx, guardianSetIndex)
if err != nil {
@ -53,7 +53,7 @@ func (k Keeper) VerifySignature(ctx sdk.Context, data []byte, guardianSetIndex u
return types.ErrGuardianIndexOutOfBounds
}
ok := vaa.VerifySignature(data, signature, addresses[signature.Index])
ok := vaa.VerifyMessageSignature(prefix, data, signature, addresses[signature.Index])
if !ok {
return types.ErrSignaturesInvalid
}
@ -61,7 +61,7 @@ func (k Keeper) VerifySignature(ctx sdk.Context, data []byte, guardianSetIndex u
return nil
}
func (k Keeper) VerifyQuorum(ctx sdk.Context, data []byte, guardianSetIndex uint32, signatures []*vaa.Signature) error {
func (k Keeper) DeprecatedVerifyVaa(ctx sdk.Context, vaaBody []byte, guardianSetIndex uint32, signatures []*vaa.Signature) error {
// Calculate quorum and retrieve guardian set
quorum, guardianSet, err := k.CalculateQuorum(ctx, guardianSetIndex)
if err != nil {
@ -72,7 +72,7 @@ func (k Keeper) VerifyQuorum(ctx sdk.Context, data []byte, guardianSetIndex uint
}
// Verify signatures
ok := vaa.VerifySignatures(data, signatures, guardianSet.KeysAsAddresses())
ok := vaa.DeprecatedVerifySignatures(vaaBody, signatures, guardianSet.KeysAsAddresses())
if !ok {
return types.ErrSignaturesInvalid
}
@ -80,8 +80,23 @@ func (k Keeper) VerifyQuorum(ctx sdk.Context, data []byte, guardianSetIndex uint
return nil
}
func (k Keeper) VerifyVAA(ctx sdk.Context, vaa *vaa.VAA) error {
return k.VerifyQuorum(ctx, vaa.SigningMsg().Bytes(), vaa.GuardianSetIndex, vaa.Signatures)
func (k Keeper) VerifyVAA(ctx sdk.Context, v *vaa.VAA) error {
// Calculate quorum and retrieve guardian set
quorum, guardianSet, err := k.CalculateQuorum(ctx, v.GuardianSetIndex)
if err != nil {
return err
}
if len(v.Signatures) < quorum {
return types.ErrNoQuorum
}
// Verify signatures
ok := v.VerifySignatures(guardianSet.KeysAsAddresses())
if !ok {
return types.ErrSignaturesInvalid
}
return nil
}
// Verify a governance VAA:

View File

@ -11,6 +11,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
keepertest "github.com/wormhole-foundation/wormchain/testutil/keeper"
"github.com/wormhole-foundation/wormchain/x/wormhole/keeper"
"github.com/wormhole-foundation/wormchain/x/wormhole/types"
@ -110,8 +111,11 @@ func sign(data common.Hash, key *ecdsa.PrivateKey, index uint8) *vaa.Signature {
}
}
func TestVerifySignature(t *testing.T) {
payload := vaa.SigningMsg([]byte{97, 97, 97, 97, 97, 97})
func TestVerifyMessageSignature(t *testing.T) {
prefix := [32]byte{}
payload := []byte{97, 97, 97, 97, 97, 97}
digest, err := vaa.MessageSigningDigest(prefix[:], payload)
require.NoError(t, err)
privKey1, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey2, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
@ -154,13 +158,85 @@ func TestVerifySignature(t *testing.T) {
keeper.AppendGuardianSet(ctx, tc.guardianSet)
// build the signature
signature := sign(payload, tc.signer, 0)
signature := sign(digest, tc.signer, 0)
if tc.setSigIndex {
signature.Index = tc.sigIndex
}
// verify the signature
err := keeper.VerifySignature(ctx, payload.Bytes(), tc.guardianSet.Index, signature)
err := keeper.VerifyMessageSignature(ctx, prefix[:], payload, tc.guardianSet.Index, signature)
if tc.willError == true {
assert.NotNil(t, err)
assert.Equal(t, err, tc.err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifyVaaSignature(t *testing.T) {
v := &vaa.VAA{
Version: 1,
GuardianSetIndex: 0,
Timestamp: time.Now(),
Nonce: 1,
Sequence: 1,
Payload: []byte{97, 97, 97, 97, 97, 97},
}
digest := v.SigningDigest()
privKey1, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey2, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
addr1 := crypto.PubkeyToAddress(privKey1.PublicKey)
addrsBytes := [][]byte{}
addrsBytes = append(addrsBytes, addr1.Bytes())
tests := []struct {
label string
guardianSet types.GuardianSet
signer *ecdsa.PrivateKey
setSigIndex bool
sigIndex uint8
willError bool
err error
}{
{label: "ValidSigner",
guardianSet: types.GuardianSet{Index: 0, Keys: addrsBytes, ExpirationTime: 0},
signer: privKey1,
willError: false},
{label: "IndexOutOfBounds",
guardianSet: types.GuardianSet{Index: 0, Keys: addrsBytes, ExpirationTime: 0},
signer: privKey1,
setSigIndex: true,
sigIndex: 1,
willError: true,
// this out of bounds issue will trigger invalid signature from sdk.
err: types.ErrSignaturesInvalid},
{label: "InvalidSigner",
guardianSet: types.GuardianSet{Index: 0, Keys: addrsBytes, ExpirationTime: 0},
signer: privKey2,
willError: true,
err: types.ErrSignaturesInvalid},
}
for _, tc := range tests {
t.Run(tc.label, func(t *testing.T) {
keeper, ctx := keepertest.WormholeKeeper(t)
keeper.AppendGuardianSet(ctx, tc.guardianSet)
// build the signature
signature := sign(digest, tc.signer, 0)
if tc.setSigIndex {
signature.Index = tc.sigIndex
}
v.Signatures = append(v.Signatures, signature)
// verify the signature
err := keeper.VerifyVAA(ctx, v)
if tc.willError == true {
assert.NotNil(t, err)