ethereum / go-ethereum

Go implementation of the Ethereum protocol
https://geth.ethereum.org
GNU Lesser General Public License v3.0
47.49k stars 20.1k forks source link

Insufficient Funds Error When Sending Transaction Despite Positive Balance #30702

Open GiovanniBraconi opened 22 hours ago

GiovanniBraconi commented 22 hours ago

I'm encountering an issue where BalanceAt() correctly shows a balance of 5 * 10^16 wei for a wallet on the Sepolia testnet. However, when calling SendTransaction() to send 1 wei to a second wallet , I get an error indicating the balance is 0:

Output

Balance wallet 1 in wei: 50000000000000000
Balance wallet 2 in wei: 0
2024/10/30 09:08:04 Failed to send transaction: insufficient funds for gas * price + value: balance 0, tx cost 258363331065001, overshot 258363331065001

Code (main.go)

package main

import (
    "context"
    "encoding/asn1"
    "fmt"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/kms"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethclient"
    "golang.org/x/crypto/sha3"
    "log"
    "math/big"
)

var kmsClient *kms.KMS
var ethClient *ethclient.Client

type AlgorithmIdentifier struct {
    Algorithm  asn1.ObjectIdentifier
    Parameters asn1.RawValue
}

type SubjectPublicKeyInfo struct {
    Algorithm        AlgorithmIdentifier
    SubjectPublicKey asn1.BitString
}

type EcdsaSigValue struct {
    R *big.Int
    S *big.Int
}

func main() {
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("eu-west-1"),
    })
    if err != nil {
        log.Fatalf("Failed to create AWS session: %v", err)
    }

    kmsClient = kms.New(sess)

    input := &kms.ListKeysInput{}
    result, err := kmsClient.ListKeys(input)

    if err != nil {
        log.Fatalf("failed to list KMS keys: %v", err)
    }

    var sepoliaTestnet = "https://sepolia.infura.io/v3/my-infura-key"

    ethClient, err = ethclient.Dial(sepoliaTestnet)
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum client: %v", err)
    }
    defer ethClient.Close()

    keyId1 := *result.Keys[len(result.Keys)-2].KeyId
    keyId2 := *result.Keys[len(result.Keys)-3].KeyId

    rawPublicKey1 := getPublicKey(keyId1)
    rawPublicKey2 := getPublicKey(keyId2)

    ethAddress1 := getEthereumAddress(rawPublicKey1)
    ethAddress2 := getEthereumAddress(rawPublicKey2)

    ethBalance1 := getWeiBalance(ethAddress1)
    ethBalance2 := getWeiBalance(ethAddress2)

    fmt.Println("Balance wallet 1 in wei:", ethBalance1)
    fmt.Println("Balance wallet 2 in wei:", ethBalance2)

    nonce, err := ethClient.PendingNonceAt(context.Background(), ethAddress1)
    if err != nil {
        log.Fatalf("Failed to get nonce: %v", err)
    }

    amount := big.NewInt(1)
    gasLimit := uint64(21000)
    gasPrice, err := ethClient.SuggestGasPrice(context.Background())

    if err != nil {
        log.Fatalf("Failed to suggest gas price: %v", err)
    }

    tx := types.NewTx(&types.LegacyTx{
        Nonce:    nonce,
        To:       &ethAddress2,
        Value:    amount,
        Gas:      gasLimit,
        GasPrice: gasPrice,
        Data:     nil,
    })

    chainID, err := ethClient.NetworkID(context.Background())
    if err != nil {
        log.Fatalf("Failed to get chain ID: %v", err)
    }

    signInput := &kms.SignInput{
        KeyId:            aws.String(keyId1),
        Message:          tx.Hash().Bytes(),
        MessageType:      aws.String("DIGEST"),
        SigningAlgorithm: aws.String("ECDSA_SHA_256"),
    }

    signOutput, err := kmsClient.Sign(signInput)

    if err != nil {
        log.Fatalf("Failed to sign transaction: %v", err)
    }

    var ecdsaSigValue EcdsaSigValue
    _, err = asn1.Unmarshal(signOutput.Signature, &ecdsaSigValue)

    if err != nil {
        log.Fatalf("failed to parse signature: %v", err)
    }

    r := ecdsaSigValue.R
    s := ecdsaSigValue.S

    secp256k1N := new(big.Int)
    secp256k1N.SetString("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16)
    secp256k1halfN := new(big.Int).Div(secp256k1N, big.NewInt(2))

    if s.Cmp(secp256k1halfN) == 1 {
        s.Sub(secp256k1N, s)
    }

    v := new(big.Int).Mul(chainID, big.NewInt(2))

    v1 := new(big.Int).Add(v, big.NewInt(35))

    signedTx := types.NewTx(&types.LegacyTx{
        Nonce:    nonce,
        To:       &ethAddress2,
        Value:    amount,
        Gas:      gasLimit,
        GasPrice: gasPrice,
        Data:     nil,
        V:        v1,
        R:        r,
        S:        s,
    })

    err = ethClient.SendTransaction(context.Background(), signedTx)

    if err != nil {
        log.Fatalf("Failed to send transaction: %v", err)
    }
}

func getPublicKey(privateKeyId string) []byte {
    derPubKey, err := getDerPublicKey(privateKeyId)
    if err != nil {
        log.Fatalf("failed to get public key: %v", err)
    }

    publicKeyInfo, err := parsePublicKey(derPubKey)
    if err != nil {
        log.Fatalf("failed to parse public key: %v", err)
    }

    publicKeyBytes := publicKeyInfo.SubjectPublicKey.Bytes
    return publicKeyBytes[1:]
}

func getDerPublicKey(keyId string) ([]byte, error) {
    input := &kms.GetPublicKeyInput{
        KeyId: aws.String(keyId),
    }

    result, err := kmsClient.GetPublicKey(input)
    if err != nil {
        return nil, fmt.Errorf("failed to get public key: %v", err)
    }

    if result.PublicKey == nil {
        return nil, fmt.Errorf("AWSKMS: PublicKey is undefined")
    }

    return result.PublicKey, nil
}

func parsePublicKey(encodedPublicKey []byte) (SubjectPublicKeyInfo, error) {
    var key SubjectPublicKeyInfo
    _, err := asn1.Unmarshal(encodedPublicKey, &key)
    if err != nil {
        log.Fatalf("failed to parse public key: %v", err)
    }
    return key, err
}

func getEthereumAddress(rawPublicKey []byte) common.Address {
    hash := sha3.NewLegacyKeccak256()
    hash.Write(rawPublicKey)
    hashed := hash.Sum(nil)

    address := hashed[len(hashed)-20:]
    ethAddress := fmt.Sprintf("0x%x", address)
    return common.HexToAddress(ethAddress)
}

func getWeiBalance(address common.Address) *big.Int {
    weiBalance, err := ethClient.BalanceAt(context.Background(), address, nil)
    if err != nil {
        log.Fatalf("Failed to get weiBalance: %v", err)
    }
    return weiBalance
}
hadv commented 17 hours ago

why don't use this method to sign the tnx?

// SignTx signs the transaction using the given signer and private key.
func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) {
    h := s.Hash(tx)
    sig, err := crypto.Sign(h[:], prv)
    if err != nil {
        return nil, err
    }
    return tx.WithSignature(s, sig)
}

https://github.com/ethereum/go-ethereum/blob/25bc07749ce21376e1023a6e16ec173fa3fc4e43/core/types/transaction_signing.go#L105

GiovanniBraconi commented 10 hours ago

Hi @hadv 😃, thanks for the help!

In my setup, I'm creating private keys directly within AWS KMS. Due to security restrictions in KMS, I can't access the private keys directly; I can only obtain the associated public keys.

I've been following this approach: https://jonathanokz.medium.com/secure-an-ethereum-wallet-with-a-kms-provider-2914bd1e4341

jwasinger commented 10 hours ago

As a sanity check, can you recover the address from the signed tx and verify that it matches an account with funds?