Skip to content
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

Insufficient Funds Error When Sending Transaction Despite Positive Balance #30702

Open
GiovanniBraconi opened this issue Oct 30, 2024 · 7 comments
Assignees
Labels

Comments

@GiovanniBraconi
Copy link

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
Copy link
Contributor

hadv commented Oct 30, 2024

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)
}

func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) {

@GiovanniBraconi
Copy link
Author

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
Copy link
Contributor

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

@GiovanniBraconi
Copy link
Author

GiovanniBraconi commented Nov 1, 2024

@jwasinger I tried something like this, but the address I obtained is different from the real sender and has 0 balance.

signer := types.NewEIP155Signer(chainID)
sender, err := types.Sender(signer, signedTx)
fmt.Printf("Sender Address %v - Balance: %v\n", ethAddress1, getWeiBalance(ethAddress1))
fmt.Printf("Sender Address from signature %v - Balance: %v\n", sender, getWeiBalance(sender))

Output

Sender Address: 0x8A9b2870A09Adbb9b8d1Cb9158E6CD1eA34B5891 - Balance: 50000000000000000
Sender Address from signature: 0xC53B85344E8164AE9908DacF48f9F7FB17C0dAFE - Balance: 0

@jwasinger
Copy link
Contributor

Yeah, that's what I expected. You are somehow signing it incorrectly. I don't really have an answer right now, but I can try and look into this.

@jwasinger jwasinger self-assigned this Nov 1, 2024
@hadv
Copy link
Contributor

hadv commented Nov 1, 2024

I think that this one is not always correct v1 := new(big.Int).Add(v, big.NewInt(35)), you should check condition to decide to add 35 or 36.
`

@GiovanniBraconi
Copy link
Author

GiovanniBraconi commented Nov 1, 2024

@hadv you're right, I still need to add the check but since I tried several times with both values I don't think that is the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants