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
46 changes: 46 additions & 0 deletions .github/workflows/proto-gen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Generate Protobuf

on:
workflow_dispatch:

permissions:
contents: write

jobs:
generate-proto:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
fetch-depth: 0

- name: Generate Protobuf files
run: make proto-gen

- name: Check for changes
id: check_changes
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "changes=true" >> $GITHUB_OUTPUT
echo "Proto files have changed:"
git status --porcelain
else
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes detected in proto files"
fi

- name: Commit and push changes
if: steps.check_changes.outputs.changes == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "chore: regenerate protobuf files"
git push origin ${{ github.ref_name }}

- name: No changes summary
if: steps.check_changes.outputs.changes == 'false'
run: |
echo "::notice::No protobuf changes detected. Nothing to push."
174 changes: 173 additions & 1 deletion precompiles/ed25519/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,173 @@
https://hackmd.io/@046EkvuRTzieElv1kovjww/r1b1kD3Gge#
# ED25519 Signature Verification Precompile

## Overview

The ED25519 precompile provides native support for verifying ED25519 digital signatures within the EVM. This precompile enables efficient cryptographic signature verification using the ED25519 elliptic curve, which is widely used in blockchain and cryptographic applications.

## Contract Address

The ED25519 precompile is deployed at a fixed address:

```
0x00000000000000000000000000000000000008f3
```

## Interface

### Methods

#### `ed25519Verify`

Verifies an ED25519 signature against a public key and message.

```solidity
function ed25519Verify(
bytes32 publicKey,
bytes32[2] signature,
bytes message
) returns (bool isValid)
```

**Parameters:**
- `publicKey` (bytes32): The ED25519 public key (32 bytes)

Check failure on line 32 in precompiles/ed25519/README.md

View workflow job for this annotation

GitHub Actions / Run markdown-lint

Lists should be surrounded by blank lines [Context: "- `publicKey` (bytes32): The E..."]
- `signature` (bytes32[2]): The ED25519 signature split into two 32-byte parts:
- `signature[0]`: R component (first 32 bytes)
- `signature[1]`: S component (last 32 bytes)
- `message` (bytes): The message that was signed (variable length)

**Returns:**
- `isValid` (bool): `true` if the signature is valid, `false` otherwise

Check failure on line 39 in precompiles/ed25519/README.md

View workflow job for this annotation

GitHub Actions / Run markdown-lint

Lists should be surrounded by blank lines [Context: "- `isValid` (bool): `true` if ..."]

## Gas Costs

The gas cost for the `ed25519Verify` function is calculated dynamically based on the message length:

```
gas = ED25519_VERIFY_BASE_GAS + SHA512_BASE_GAS + SHA512_PER_WORD_GAS * ((msgLen + 31) / 32)
```

Where:
- `ED25519_VERIFY_BASE_GAS = 2000`: Base cost for ED25519 signature verification

Check failure on line 50 in precompiles/ed25519/README.md

View workflow job for this annotation

GitHub Actions / Run markdown-lint

Lists should be surrounded by blank lines [Context: "- `ED25519_VERIFY_BASE_GAS = 2..."]
- `SHA512_BASE_GAS = 60`: Base cost for SHA512 hashing
- `SHA512_PER_WORD_GAS = 12`: Cost per 32-byte word for SHA512 hashing
- `msgLen`: Length of the message (excluding the 36 bytes for method selector and signature.S)


Check failure on line 55 in precompiles/ed25519/README.md

View workflow job for this annotation

GitHub Actions / Run markdown-lint

Multiple consecutive blank lines [Expected: 1; Actual: 2]
## Usage Example

### Solidity

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IED25519 {
function ed25519Verify(
bytes32 publicKey,
bytes32[2] calldata signature,
bytes calldata message
) external returns (bool isValid);
}

contract ED25519Example {
IED25519 constant ED25519_PRECOMPILE = IED25519(0x00000000000000000000000000000000000008f3);

function verifySignature(
bytes32 publicKey,
bytes32[2] calldata signature,
bytes calldata message
) public returns (bool) {
return ED25519_PRECOMPILE.ed25519Verify(publicKey, signature, message);
}
}
```

### JavaScript/TypeScript (ethers.js)

```typescript
import { ethers } from 'ethers';

const ED25519_ADDRESS = '0x00000000000000000000000000000000000008f3';

const ed25519ABI = [
{
"inputs": [
{ "internalType": "bytes32", "name": "publicKey", "type": "bytes32" },
{ "internalType": "bytes32[2]", "name": "signature", "type": "bytes32[2]" },
{ "internalType": "bytes", "name": "message", "type": "bytes" }
],
"name": "ed25519Verify",
"outputs": [
{ "internalType": "bool", "name": "isValid", "type": "bool" }
],
"stateMutability": "nonpayable",
"type": "function"
}
];

async function verifyED25519Signature(
provider: ethers.Provider,
publicKey: string,
signature: [string, string],
message: string
): Promise<boolean> {
const ed25519Contract = new ethers.Contract(ED25519_ADDRESS, ed25519ABI, provider);
const isValid = await ed25519Contract.ed25519Verify(publicKey, signature, message);
return isValid;
}
```

## Technical Details

### ED25519 Algorithm

ED25519 is a public-key signature system that uses:
- Curve25519 elliptic curve

Check failure on line 125 in precompiles/ed25519/README.md

View workflow job for this annotation

GitHub Actions / Run markdown-lint

Lists should be surrounded by blank lines [Context: "- Curve25519 elliptic curve"]
- SHA-512 hash function
- Schnorr signature scheme

### Signature Format

The ED25519 signature is 64 bytes long and is split into two components:
- **R component** (32 bytes): The first half of the signature

Check failure on line 132 in precompiles/ed25519/README.md

View workflow job for this annotation

GitHub Actions / Run markdown-lint

Lists should be surrounded by blank lines [Context: "- **R component** (32 bytes): ..."]
- **S component** (32 bytes): The second half of the signature

### Implementation

The precompile uses Go's standard `crypto/ed25519` package for signature verification, ensuring compatibility with standard ED25519 implementations.

## Security Considerations

1. **Public Key Validation**: The precompile expects a valid 32-byte ED25519 public key. Invalid keys will result in verification failure.

2. **Signature Validation**: The signature must be exactly 64 bytes (provided as two 32-byte arrays). Invalid signature lengths or formats will result in an error.

3. **Message Integrity**: The message should be provided exactly as it was when the signature was created. Any modification will cause verification to fail.

4. **Gas Limits**: When verifying large messages, ensure sufficient gas is provided based on the dynamic gas calculation formula.

## Use Cases

- **Cross-chain Communication**: Verify signatures from chains that use ED25519 (e.g., Cosmos SDK chains, Solana)
- **Identity Verification**: Validate ED25519-based digital identities
- **Secure Messaging**: Verify signed messages in decentralized applications
- **Multi-signature Wallets**: Implement multi-sig wallets that support ED25519
- **Oracle Data Verification**: Verify signed data from oracles using ED25519 keys

## Testing

The precompile includes comprehensive test coverage. Run tests with:

```bash
go test ./precompiles/ed25519/...
```

## References

- [ED25519 Specification (RFC 8032)](https://datatracker.ietf.org/doc/html/rfc8032)
- [Go crypto/ed25519 Package](https://pkg.go.dev/crypto/ed25519)
- [Curve25519 and ED25519](https://ed25519.cr.yp.to/)

## License

This precompile is part of the Cosmos EVM project and is licensed under the project's license terms.
7 changes: 4 additions & 3 deletions precompiles/ed25519/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import (
//go:embed abi.json
var f embed.FS

const ED25519_VERIFY_BASE_GAS = 1500
const ED25519_VERIFY_BASE_GAS = 2000
const SHA512_BASE_GAS = 60
const SHA512_PER_WORD_GAS = 8
const SHA512_PER_WORD_GAS = 12

const ED25519VerifyMethod = "ed25519Verify"

Expand All @@ -46,7 +46,8 @@ func (Precompile) Address() common.Address {
func (p Precompile) RequiredGas(input []byte) uint64 {
// Challenge for ed25519 uses sha512 of sig.R, pubkey, msg
// So exclude 32 bytes of sig.Z from the length
msgLen := max(len(input)-32, 0)
// Also exclute 4 bytes of method selector
msgLen := max(len(input)-36, 0)
return ED25519_VERIFY_BASE_GAS + SHA512_BASE_GAS + SHA512_PER_WORD_GAS*((uint64(msgLen)+31)/32)
}

Expand Down
28 changes: 18 additions & 10 deletions precompiles/ed25519/ed25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func (s *PrecompileTestSuite) TestAddress() {
}

func (s *PrecompileTestSuite) TestRequiredGas() {
// Formula: msgLen = max(len(input) - 36, 0)
// gas = ED25519_VERIFY_BASE_GAS + SHA512_BASE_GAS + SHA512_PER_WORD_GAS * ((msgLen + 31) / 32)
// 36 = 4 (method selector) + 32 (sig.Z excluded from sha512 calculation)
testCases := []struct {
name string
input []byte
Expand All @@ -58,27 +61,32 @@ func (s *PrecompileTestSuite) TestRequiredGas() {
{
"empty input",
[]byte{},
// msgLen = 0, words = 0
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS,
},
{
"minimal input (96 bytes)",
make([]byte, 96),
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS,
// msgLen = 96-36 = 60, words = (60+31)/32 = 2
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS + edprecompile.SHA512_PER_WORD_GAS*2,
},
{
"input with 32 byte message",
make([]byte, 96+32),
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS + edprecompile.SHA512_PER_WORD_GAS,
"input with 32 byte message (128 bytes total)",
make([]byte, 128),
// msgLen = 128-36 = 92, words = (92+31)/32 = 3
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS + edprecompile.SHA512_PER_WORD_GAS*3,
},
{
"input with 64 byte message",
make([]byte, 96+64),
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS + edprecompile.SHA512_PER_WORD_GAS*2,
"input with 64 byte message (160 bytes total)",
make([]byte, 160),
// msgLen = 160-36 = 124, words = (124+31)/32 = 4
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS + edprecompile.SHA512_PER_WORD_GAS*4,
},
{
"input with 100 byte message",
make([]byte, 96+100),
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS + edprecompile.SHA512_PER_WORD_GAS*4,
"input with 100 byte message (196 bytes total)",
make([]byte, 196),
// msgLen = 196-36 = 160, words = (160+31)/32 = 5
edprecompile.ED25519_VERIFY_BASE_GAS + edprecompile.SHA512_BASE_GAS + edprecompile.SHA512_PER_WORD_GAS*5,
},
}

Expand Down
4 changes: 3 additions & 1 deletion x/vm/types/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ const LiquidStakePrecompileAddress = "0x000000000000000000000000000000000000
//
// NOTE: To be explicit, this list does not include the dynamically registered EVM extensions
// like the ERC-20 extensions.
// NOTE: This list MUST be sorted lexicographically by address to match the expected order
// after params are set (SetParams sorts the precompiles).
var AvailableStaticPrecompiles = []string{
P256PrecompileAddress,
Bech32PrecompileAddress,
Ed25519PrecompileAddress,
StakingPrecompileAddress,
DistributionPrecompileAddress,
ICS20PrecompileAddress,
Expand All @@ -35,5 +36,6 @@ var AvailableStaticPrecompiles = []string{
GovPrecompileAddress,
SlashingPrecompileAddress,
EvidencePrecompileAddress,
Ed25519PrecompileAddress,
LiquidStakePrecompileAddress,
}
2 changes: 1 addition & 1 deletion x/vm/types/scaling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestConvertEvmCoinFrom18Decimals(t *testing.T) {

if !tc.expErr {
require.NoError(t, err)
require.Equal(t, tc.expCoin, coinConverted, "expected a different coin")
require.True(t, tc.expCoin.IsEqual(coinConverted), "expected a different coin: got %s, want %s", coinConverted, tc.expCoin)
} else {
require.Error(t, err)
}
Expand Down
Loading