diff --git a/.github/workflows/proto-gen.yml b/.github/workflows/proto-gen.yml new file mode 100644 index 000000000..3bdd1a600 --- /dev/null +++ b/.github/workflows/proto-gen.yml @@ -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." diff --git a/precompiles/ed25519/README.md b/precompiles/ed25519/README.md index 28032c926..d6354c9f7 100644 --- a/precompiles/ed25519/README.md +++ b/precompiles/ed25519/README.md @@ -1 +1,173 @@ -https://hackmd.io/@046EkvuRTzieElv1kovjww/r1b1kD3Gge# \ No newline at end of file +# 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) +- `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 + +## 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 +- `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) + + +## 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 { + 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 +- 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 +- **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. diff --git a/precompiles/ed25519/ed25519.go b/precompiles/ed25519/ed25519.go index 20362a1b5..3908c833a 100644 --- a/precompiles/ed25519/ed25519.go +++ b/precompiles/ed25519/ed25519.go @@ -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" @@ -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) } diff --git a/precompiles/ed25519/ed25519_test.go b/precompiles/ed25519/ed25519_test.go index d83a8635b..77060892a 100644 --- a/precompiles/ed25519/ed25519_test.go +++ b/precompiles/ed25519/ed25519_test.go @@ -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 @@ -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, }, } diff --git a/x/vm/types/precompiles.go b/x/vm/types/precompiles.go index ce687740e..c0e621089 100644 --- a/x/vm/types/precompiles.go +++ b/x/vm/types/precompiles.go @@ -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, @@ -35,5 +36,6 @@ var AvailableStaticPrecompiles = []string{ GovPrecompileAddress, SlashingPrecompileAddress, EvidencePrecompileAddress, + Ed25519PrecompileAddress, LiquidStakePrecompileAddress, } diff --git a/x/vm/types/scaling_test.go b/x/vm/types/scaling_test.go index 354290c08..8e80d9d5f 100644 --- a/x/vm/types/scaling_test.go +++ b/x/vm/types/scaling_test.go @@ -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) }