Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions util/ethereum.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package util

import (
"crypto/ecdsa"
"fmt"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
Expand All @@ -23,22 +24,37 @@ func RecoverAddressFromSignature(msg []byte, sig []byte) (*common.Address, error
if len(sig) != crypto.SignatureLength {
return nil, secp256k1.ErrInvalidSignatureLen
}

v := &sig[crypto.RecoveryIDOffset]

// According to the Ethereum Yellow Paper, the signature format must be
// [R || S || V], and V (the Recovery ID) must be 27 or 28. This was apparently
// inherited from Bitcoin.
// Internally, V is 0 or 1, so we subtract 27 to get the actual recovery ID.
// References:
// - https://github.com/ethereum/go-ethereum/issues/19751#issuecomment-504900739
// - https://ethereum.github.io/yellowpaper/paper.pdf, page 22, Appendix E., (213)
sig[crypto.RecoveryIDOffset] -= 27
//
// Additionally, we've seen some wallets produce signatures with V=0 or 1.
// In those cases, we don't need to tweak the recovery ID before recovering the public key.
switch *v {
case 27, 28:
// Subtract 27 to get the actual recovery ID.
*v -= 27
// Restore V to its original value at the end of the function.
defer func() {
*v += 27
}()
case 0, 1:
// Do nothing.
default:
return nil, fmt.Errorf("invalid recovery ID: %d", *v)
}

// Recover the public key from the signature.
hash := accounts.TextHash(msg)
pubKey, err := crypto.SigToPub(hash, sig)

// Restore V to its original value.
sig[crypto.RecoveryIDOffset] += 27

if err != nil {
return nil, err
}
Expand Down
32 changes: 32 additions & 0 deletions util/ethereum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,35 @@ func TestAccountsTextHash(t *testing.T) {
})
}
}

func TestRecoverAddressFromSignature(t *testing.T) {
tests := []struct {
name string
node_id string
msg string
signature string
}{
{
name: "Example signature",
node_id: "0xEA28d002042fd9898D0Db016be9758eeAFE35C1E",
msg: "Rescue Node 1753055877",
signature: "3b1ffab4b818e917b81a878265061660c05a5d9eb55bda623b184d1d8f0af9af58c366d487652a7017d994999ad344f72ea85f2c40ff6da7e0810f8a4a6e7b341c",
},
{
name: "Example signature with low v",
node_id: "0xEA28d002042fd9898D0Db016be9758eeAFE35C1E",
msg: "Rescue Node 1753055877",
signature: "3b1ffab4b818e917b81a878265061660c05a5d9eb55bda623b184d1d8f0af9af58c366d487652a7017d994999ad344f72ea85f2c40ff6da7e0810f8a4a6e7b3401",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
signature, err := hex.DecodeString(tt.signature)
assert.NoError(t, err)
address, err := RecoverAddressFromSignature([]byte(tt.msg), signature)
assert.NoError(t, err)
assert.Equal(t, common.HexToAddress(tt.node_id), *address)
})
}
}
Loading