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

(WIP) Add pre-compile: verification for ed25519 sig made via GPG #2

Open
wants to merge 11 commits into
base: optimism
Choose a base branch
from
122 changes: 112 additions & 10 deletions core/vm/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import (
"maps"
"math/big"

pgpcrypto "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/consensys/gnark-crypto/ecc"
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fp"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/tracing"
Expand Down Expand Up @@ -104,16 +106,17 @@ var PrecompiledContractsBerlin = PrecompiledContracts{
// PrecompiledContractsCancun contains the default set of pre-compiled Ethereum
// contracts used in the Cancun release.
var PrecompiledContractsCancun = PrecompiledContracts{
common.BytesToAddress([]byte{0x1}): &ecrecover{},
common.BytesToAddress([]byte{0x2}): &sha256hash{},
common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
common.BytesToAddress([]byte{0x4}): &dataCopy{},
common.BytesToAddress([]byte{0x5}): &bigModExp{eip2565: true},
common.BytesToAddress([]byte{0x6}): &bn256AddIstanbul{},
common.BytesToAddress([]byte{0x7}): &bn256ScalarMulIstanbul{},
common.BytesToAddress([]byte{0x8}): &bn256PairingIstanbul{},
common.BytesToAddress([]byte{0x9}): &blake2F{},
common.BytesToAddress([]byte{0xa}): &kzgPointEvaluation{},
common.BytesToAddress([]byte{0x1}): &ecrecover{},
common.BytesToAddress([]byte{0x2}): &sha256hash{},
common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
common.BytesToAddress([]byte{0x4}): &dataCopy{},
common.BytesToAddress([]byte{0x5}): &bigModExp{eip2565: true},
common.BytesToAddress([]byte{0x6}): &bn256AddIstanbul{},
common.BytesToAddress([]byte{0x7}): &bn256ScalarMulIstanbul{},
common.BytesToAddress([]byte{0x8}): &bn256PairingIstanbul{},
common.BytesToAddress([]byte{0x9}): &blake2F{},
common.BytesToAddress([]byte{0xa}): &kzgPointEvaluation{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},
}

// PrecompiledContractsPrague contains the set of pre-compiled Ethereum
Expand All @@ -138,6 +141,7 @@ var PrecompiledContractsPrague = PrecompiledContracts{
common.BytesToAddress([]byte{0x11}): &bls12381Pairing{},
common.BytesToAddress([]byte{0x12}): &bls12381MapG1{},
common.BytesToAddress([]byte{0x13}): &bls12381MapG2{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},
}

var PrecompiledContractsBLS = PrecompiledContractsPrague
Expand Down Expand Up @@ -173,6 +177,7 @@ var PrecompiledContractsGranite = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{8}): &bn256PairingGranite{},
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},
common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{},
}

Expand Down Expand Up @@ -1353,3 +1358,100 @@ func (c *p256Verify) Run(input []byte) ([]byte, error) {
return nil, nil
}
}

// gpgEd25519Verify implements native verification for ed25519 signatures produced via gpg
type gpgEd25519Verify struct{}

var (
errDecodingFailed = errors.New("failed to decode input")
errInvalidPublicKey = errors.New("invalid public key")
)

// RequiredGas returns the gas required to execute the pre-compiled contract
func (c *gpgEd25519Verify) RequiredGas(input []byte) uint64 {
// You can adjust this value based on your needs
return params.GpgEd25519VerifyGas
}

// Run performs ed25519 signature verification
func (c *gpgEd25519Verify) Run(input []byte) ([]byte, error) {
// Input should be: abi.encode(bytes32 message, bytes publicKey, bytes signature)
message, pubKey, signature, err := decodeGPGEd25519VerifyInput(input)
if err != nil {
return nil, errDecodingFailed
}

// Create message object
messageObj := pgpcrypto.NewPlainMessage(message[:])

// Create public key object
pubKeyObj, err := pgpcrypto.NewKey(pubKey)
if err != nil {
return nil, errInvalidPublicKey
}

// Create public keyring
pubKeyRing, err := pgpcrypto.NewKeyRing(pubKeyObj)
if err != nil {
return nil, errInvalidPublicKey
}

// Create signature object
signatureObj := pgpcrypto.NewPGPSignature(signature)

// Verify signature
err = pubKeyRing.VerifyDetached(messageObj, signatureObj, 0)
if err != nil {
// Return 32 bytes: 0 for failure
return common.LeftPadBytes([]byte{0}, 32), nil
}

// Return 32 bytes: 1 for success
return common.LeftPadBytes([]byte{1}, 32), nil
}

func decodeGPGEd25519VerifyInput(input []byte) ([32]byte, []byte, []byte, error) {
// Define ABI types
bytesType, err := abi.NewType("bytes", "", nil)
if err != nil {
return [32]byte{}, nil, nil, fmt.Errorf("failed to create bytes type: %v", err)
}
bytes32Type, err := abi.NewType("bytes32", "", nil)
if err != nil {
return [32]byte{}, nil, nil, fmt.Errorf("failed to create bytes32 type: %v", err)
}

// Create ABI arguments
arguments := abi.Arguments{
{Type: bytes32Type},
{Type: bytesType},
{Type: bytesType},
}

// Unpack the encoded data
unpacked, err := arguments.Unpack(input)
if err != nil {
return [32]byte{}, nil, nil, fmt.Errorf("failed to unpack data: %v", err)
}

// Ensure we have the correct number of elements
if len(unpacked) != 3 {
return [32]byte{}, nil, nil, fmt.Errorf("unexpected number of decoded arguments: got %d, want 3", len(unpacked))
}

// Extract each value
message, ok := unpacked[0].([32]byte)
if !ok {
return [32]byte{}, nil, nil, fmt.Errorf("failed to cast message to [32]byte")
}
publicKey, ok := unpacked[1].([]byte)
if !ok {
return [32]byte{}, nil, nil, fmt.Errorf("failed to cast publicKey to []byte")
}
signature, ok := unpacked[2].([]byte)
if !ok {
return [32]byte{}, nil, nil, fmt.Errorf("failed to cast signature to []byte")
}

return message, publicKey, signature, nil
}
38 changes: 38 additions & 0 deletions core/vm/contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ var allPrecompiles = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{8}): &bn256PairingGranite{},
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},

common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{},

Expand Down Expand Up @@ -421,3 +422,40 @@ func BenchmarkPrecompiledP256Verify(bench *testing.B) {
}

func TestPrecompiledP256Verify(t *testing.T) { testJson("p256Verify", "100", t) }

// Tests the GPG Ed25519 signature verification. Input format: abi.encodePacked(message.len, message, pubKey.len, pubKey, sig.len, sig)
func TestPrecompiledGpgEd25519Verify(t *testing.T) { testJson("gpgEd25519Verify", "ed", t) }

func BenchmarkPrecompiledGpgEd25519Verify(b *testing.B) {
t := precompiledTest{
Input: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000001ab983304676a5d0e16092b06010401da470f010107405d10ab19030f8a3edcace13d0e166367d295f1e114ef87c8f3009cda931a0eebb42b4b72697368616e67204e61646761756461203c6b72697368616e672e6e6f746540676d61696c2e636f6d3e88930413160a003b162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b03050b0908070202220206150a09080b020416020301021e07021780000a0910967672f8454443b8224200ff75c455a94fe4c82e6c516f523aec19d42857f17a74e1c7402401540f9ed70c1d00fe2a2e4e3f5a136084bcb5aa5f68e8995a75bf8d0a93cc024509e4156443b35004b83804676a5d0e120a2b060104019755010501010740151340eabbaaf90a7dda8562b821742d068d1a41681d7e152eefbf73042ca32e0301080788780418160a0020162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b0c000a0910967672f8454443b8d9480100fda1c358442e155612fe0af757f2eeb94fea65d9f90ddaa57bab37683890c19f00fc09cf6e07b238f81361edb9cc119fc876b95f68aa001d8308d53d8f3f99dcfc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007788750400160a001d162104c0c2dcd8a10746e47c75b181967672f8454443b80502676e9c52000a0910967672f8454443b8c10000fe22cd5d6fd453b2bad46641bc41ffb3cffa834a2c74da517e8fb644e1631d23b50100828ee409948c1ea870d2192c3a76cbeb46556454f20d132f52dfe835dfb9ad04000000000000000000",
Expected: "0000000000000000000000000000000000000000000000000000000000000001",
Name: "verify_gpg_ed25519_success",
}
benchmarkPrecompiled("ed", t, b)
}

// Tests GPG Ed25519 verification with malformed inputs
var gpgEd25519MalformedInputTests = []precompiledFailureTest{
{
Input: "",
ExpectedError: "failed to decode input",
Name: "empty input",
},
{
Input: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024",
ExpectedError: "failed to decode input",
Name: "input less than 96 bytes",
},
{
Input: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000001ab984404676a5d0e16092b06010401da470f010107405d10ab19030f8a3edcace13d0e166367d295f1e114ef87c8f3009cda931a0eebb42b4b72697368616e67204e61646761756461203c6b72697368616e672e6e6f746540676d61696c2e636f6d3e88930413160a003b162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b03050b0908070202220206150a09080b020416020301021e07021780000a0910967672f8454443b8224200ff75c455a94fe4c82e6c516f523aec19d42857f17a74e1c7402401540f9ed70c1d00fe2a2e4e3f5a136084bcb5aa5f68e8995a75bf8d0a93cc024509e4156443b35004b83804676a5d0e120a2b060104019755010501010740151340eabbaaf90a7dda8562b821742d068d1a41681d7e152eefbf73042ca32e0301080788780418160a0020162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b0c000a0910967672f8454443b8d9480100fda1c358442e155612fe0af757f2eeb94fea65d9f90ddaa57bab37683890c19f00fc09cf6e07b238f81361edb9cc119fc876b95f68aa001d8308d53d8f3f99dcfc22000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007788750400160a001d162104c0c2dcd8a10746e47c75b181967672f8454443b80502676e9c52000a0910967672f8454443b8c10000fe22cd5d6fd453b2bad46641bc41ffb3cffa834a2c74da517e8fb644e1631d23b50100828ee409948c1ea870d2192c3a76cbeb46556454f20d132f52dfe835dfb9ad04000000000000000000",
ExpectedError: "invalid public key",
Name: "invalid public key",
},
}

func TestPrecompiledGpgEd25519VerifyMalformedInput(t *testing.T) {
for _, test := range gpgEd25519MalformedInputTests {
testPrecompiledFailure("ed", test, t)
}
}
16 changes: 16 additions & 0 deletions core/vm/testdata/precompiles/gpgEd25519Verify.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"Input": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000001ab983304676a5d0e16092b06010401da470f010107405d10ab19030f8a3edcace13d0e166367d295f1e114ef87c8f3009cda931a0eebb42b4b72697368616e67204e61646761756461203c6b72697368616e672e6e6f746540676d61696c2e636f6d3e88930413160a003b162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b03050b0908070202220206150a09080b020416020301021e07021780000a0910967672f8454443b8224200ff75c455a94fe4c82e6c516f523aec19d42857f17a74e1c7402401540f9ed70c1d00fe2a2e4e3f5a136084bcb5aa5f68e8995a75bf8d0a93cc024509e4156443b35004b83804676a5d0e120a2b060104019755010501010740151340eabbaaf90a7dda8562b821742d068d1a41681d7e152eefbf73042ca32e0301080788780418160a0020162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b0c000a0910967672f8454443b8d9480100fda1c358442e155612fe0af757f2eeb94fea65d9f90ddaa57bab37683890c19f00fc09cf6e07b238f81361edb9cc119fc876b95f68aa001d8308d53d8f3f99dcfc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007788750400160a001d162104c0c2dcd8a10746e47c75b181967672f8454443b80502676e9c52000a0910967672f8454443b8c10000fe22cd5d6fd453b2bad46641bc41ffb3cffa834a2c74da517e8fb644e1631d23b50100828ee409948c1ea870d2192c3a76cbeb46556454f20d132f52dfe835dfb9ad04000000000000000000",
"Expected": "0000000000000000000000000000000000000000000000000000000000000001",
"Gas": 2000,
"Name": "verify_gpg_ed25519_success",
"NoBenchmark": false
},
{
"Input": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000001ab983304676a5d0e16092b06010401da470f010107405d10ab19030f8a3edcace13d0e166367d295f1e114ef87c8f3009cda931a0eebb42b4b72697368616e67204e61646761756461203c6b72697368616e672e6e6f746540676d61696c2e636f6d3e88930413160a003b162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b03050b0908070202220206150a09080b020416020301021e07021780000a0910967672f8454443b8224200ff75c455a94fe4c82e6c516f523aec19d42857f17a74e1c7402401540f9ed70c1d00fe2a2e4e3f5a136084bcb5aa5f68e8995a75bf8d0a93cc024509e4156443b35004b83804676a5d0e120a2b060104019755010501010740151340eabbaaf90a7dda8562b821742d068d1a41681d7e152eefbf73042ca32e0301080788780418160a0020162104c0c2dcd8a10746e47c75b181967672f8454443b80502676a5d0e021b0c000a0910967672f8454443b8d9480100fda1c358442e155612fe0af757f2eeb94fea65d9f90ddaa57bab37683890c19f00fc09cf6e07b238f81361edb9cc119fc876b95f68aa001d8308d53d8f3f99dcfc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007788750400160a001d162104c0c2dcd8a10746e47c75b181967672f8454443b80502676e9c52000a0910967672f8454443b8c10000fe22cd5d6fd453b2bad46641bc41ffb3cffa834a2c74da517e8fb644e1631d23b50100828ee409948c1ea870d2192c3a76cbeb46556454f20d132f52dfe835dfb9aa04000000000000000000",
"Expected": "0000000000000000000000000000000000000000000000000000000000000000",
"Gas": 2000,
"Name": "verify_gpg_ed25519_failure",
"NoBenchmark": false
}
]
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.22.7
require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
github.com/Microsoft/go-winio v0.6.2
github.com/ProtonMail/gopenpgp/v2 v2.8.1
github.com/VictoriaMetrics/fastcache v1.12.2
github.com/aws/aws-sdk-go-v2 v1.21.2
github.com/aws/aws-sdk-go-v2/config v1.18.45
Expand Down Expand Up @@ -84,6 +85,8 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect
Expand All @@ -97,6 +100,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
Expand Down
Loading