Skip to content

Commit 7a7f620

Browse files
committed
megred post audit contract changes
2 parents de51911 + d5d4510 commit 7a7f620

58 files changed

Lines changed: 3626 additions & 604 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/uregistry/v1/query.pulsar.go

Lines changed: 714 additions & 151 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/utss/v1/types.pulsar.go

Lines changed: 119 additions & 45 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

precompiles/usigverifier/README.md

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,44 +30,56 @@ address constant USigVerifier_PRECOMPILE_ADDRESS = 0x000000000000000000000000
3030
address constant USigVerifier_PRECOMPILE_ADDRESS_V2 = 0xEC00000000000000000000000000000000000001;
3131
3232
interface IUSigVerifier {
33-
/// @notice Verifies an Ed25519 signature.
34-
/// @param pubKey The 32-byte Ed25519 public key (Solana address bytes).
35-
/// @param msg The message digest that was signed (bytes32).
36-
/// @param signature The 64-byte Ed25519 signature.
37-
/// @return isValid True iff the signature is valid for (pubKey, msg).
38-
function verifyEd25519(
39-
bytes calldata pubKey,
40-
bytes32 msg,
41-
bytes calldata signature
42-
) external view returns (bool);
33+
/// Verifies signature over `"0x" + hex(msgDigest)` (66 ASCII bytes).
34+
/// Used by UEA_SVM. Solana wallets render the hex string in their sign-message UI.
35+
function verifyEd25519(bytes calldata pubKey, bytes32 msgDigest, bytes calldata signature)
36+
external view returns (bool);
37+
38+
/// Verifies signature over the raw message bytes (standard Ed25519 semantics).
39+
/// Use this if your signer uses the conventional `ed25519.Sign(privKey, rawBytes)` API.
40+
function verifyEd25519RawMessage(bytes calldata pubKey, bytes calldata message, bytes calldata signature)
41+
external view returns (bool);
4342
}
4443
```
4544

46-
| Property | Value |
47-
|---|---|
48-
| Method | `verifyEd25519(bytes,bytes32,bytes)` |
49-
| State mutability | `view` (no on-chain state is touched) |
50-
| Gas cost | `4000` per call (`VerifyEd25519Gas` in `usigverifier.go`) |
45+
| Method | Signed bytes | Gas | Use when |
46+
|---|---|---|---|
47+
| `verifyEd25519(bytes,bytes32,bytes)` | `"0x" + hex(msgDigest)` (66 ASCII bytes) | 4000 | UEA_SVM / Solana-wallet flows where the user signs a hex string in Phantom/Solflare |
48+
| `verifyEd25519RawMessage(bytes,bytes,bytes)` | Raw `message` bytes | 4000 | New integrations / relayers using standard `ed25519.Sign(privKey, rawBytes)` |
49+
50+
Both methods are `view` and touch no chain state.
5151

5252
## Verification Semantics
5353

54-
The precompile is intentionally narrow. It accepts:
54+
Two methods, two distinct signing conventions. **A signature produced for one method will not verify under the other** — the test vectors in `query_test.go` lock this in.
5555

56-
- `pubKey` — 32 raw Ed25519 public key bytes (a Solana address is exactly this)
57-
- `msg` — a single `bytes32` digest
58-
- `signature` — 64 raw Ed25519 signature bytes
56+
### `verifyEd25519` — hex-ASCII convention (legacy / wallet-friendly)
5957

60-
Internally (`query.go:VerifyEd25519`), the `bytes32` digest is **rendered as a 0x-prefixed hex string** before being passed to `ed25519.Verify`:
58+
Internally (`query.go:VerifyEd25519`), the `bytes32` `msgDigest` is rendered as a 0x-prefixed hex string before being passed to `ed25519.Verify`:
6159

6260
```go
6361
msgStr := "0x" + hex.EncodeToString(msg) // 66 ASCII bytes
6462
msgBytes := []byte(msgStr)
6563
ok = ed25519.Verify(pubKeyBytes, msgBytes, signature)
6664
```
6765

68-
In other words, the signed message that the off-chain signer must sign is the **66-byte ASCII string** `0x...` of the digest, not the raw 32 bytes. This matches the convention used by Solana wallets when signing arbitrary messages — they prefix-encode the payload — so a normal Solana wallet signature over a Push Chain message hash will verify here without any extra work on the wallet side.
66+
The off-chain signer must sign the **66-byte ASCII string** `"0x"+hex(digest)`, not the raw 32 bytes. This is what UEA_SVM uses so that a Solana wallet (Phantom, Solflare) shows the user a copy-pasteable hex string in its sign-message prompt rather than opaque bytes.
67+
68+
### `verifyEd25519RawMessage` — raw-bytes convention (standard)
69+
70+
Standard Ed25519 verification — signature is checked against the raw `message` bytes:
71+
72+
```go
73+
ok = ed25519.Verify(pubKeyBytes, message, signature)
74+
```
75+
76+
Use this when your signer uses `ed25519.Sign(privKey, rawBytes)` (default in every Solana SDK / nacl library). `message` may be any length, not just 32 bytes.
77+
78+
### Common rules
6979

70-
If `pubKey` is not 32 bytes or `signature` is not 64 bytes, the precompile reverts with `invalid params`. Unknown method IDs revert with the standard `unknown method` error.
80+
- `pubKey` must be exactly 32 bytes; `signature` must be exactly 64 bytes — otherwise the precompile reverts with `invalid params`.
81+
- Unknown method IDs revert with the standard `unknown method` error.
82+
- Both methods cost `4000` gas.
7183

7284
## Generating the ABI
7385

precompiles/usigverifier/USigVerifier.sol

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,26 @@ IUSigVerifier constant USigVerifier_CONTRACT_V2 = IUSigVerifier(USigVerifier_PRE
1515

1616
/// @dev The IUSigVerifier contract's interface.
1717
interface IUSigVerifier {
18-
/// @notice Verifies a signature using Ed25519
19-
/// @param pubKey The base58-encoded public key (Solana address)
20-
/// @param msg The message that was signed
21-
/// @param signature The signature to verify
22-
/// @return isValid True if the signature is valid
23-
function verifyEd25519(bytes calldata pubKey, bytes32 msg, bytes calldata signature) external view returns (bool);
18+
/// @notice Verifies an Ed25519 signature over the ASCII hex form of msgDigest.
19+
/// @dev The signature MUST be produced over the 66-byte UTF-8 sequence
20+
/// `"0x" + hex(msgDigest)`, NOT over the raw 32 bytes of msgDigest.
21+
/// This convention exists so Solana wallets (Phantom, Solflare, etc.)
22+
/// display a human-readable hex string in their sign-message prompt.
23+
/// For raw-bytes semantics, use {verifyEd25519RawMessage}.
24+
/// @param pubKey 32-byte Ed25519 public key (a Solana address is exactly this).
25+
/// @param msgDigest The 32-byte digest. Off-chain signer must sign `"0x" + hex(msgDigest)` (66 bytes).
26+
/// @param signature 64-byte Ed25519 signature.
27+
/// @return isValid True iff signature is valid for (pubKey, "0x"+hex(msgDigest)).
28+
function verifyEd25519(bytes calldata pubKey, bytes32 msgDigest, bytes calldata signature) external view returns (bool);
29+
30+
/// @notice Verifies an Ed25519 signature over raw message bytes.
31+
/// @dev Standard Ed25519 verification: signature is checked against the raw
32+
/// bytes of `message`. Use this when your off-chain signer uses the
33+
/// conventional `ed25519.Sign(privKey, rawBytes)` API (the default in
34+
/// every Solana SDK / nacl library).
35+
/// @param pubKey 32-byte Ed25519 public key.
36+
/// @param message Raw message bytes that were signed (any length).
37+
/// @param signature 64-byte Ed25519 signature.
38+
/// @return isValid True iff signature is valid for (pubKey, message).
39+
function verifyEd25519RawMessage(bytes calldata pubKey, bytes calldata message, bytes calldata signature) external view returns (bool);
2440
}

precompiles/usigverifier/abi.json

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
{
1818
"internalType": "bytes32",
19-
"name": "msg",
19+
"name": "msgDigest",
2020
"type": "bytes32"
2121
},
2222
{
@@ -35,6 +35,35 @@
3535
],
3636
"stateMutability": "view",
3737
"type": "function"
38+
},
39+
{
40+
"inputs": [
41+
{
42+
"internalType": "bytes",
43+
"name": "pubKey",
44+
"type": "bytes"
45+
},
46+
{
47+
"internalType": "bytes",
48+
"name": "message",
49+
"type": "bytes"
50+
},
51+
{
52+
"internalType": "bytes",
53+
"name": "signature",
54+
"type": "bytes"
55+
}
56+
],
57+
"name": "verifyEd25519RawMessage",
58+
"outputs": [
59+
{
60+
"internalType": "bool",
61+
"name": "",
62+
"type": "bool"
63+
}
64+
],
65+
"stateMutability": "view",
66+
"type": "function"
3867
}
3968
]
4069
}

precompiles/usigverifier/query.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import (
88
"github.com/ethereum/go-ethereum/accounts/abi"
99
)
1010

11-
const VerifyEd25519Method = "verifyEd25519"
11+
const (
12+
VerifyEd25519Method = "verifyEd25519"
13+
VerifyEd25519RawMessageMethod = "verifyEd25519RawMessage"
14+
)
1215

16+
// VerifyEd25519 verifies a signature over the ASCII bytes of "0x"+hex(msgDigest).
17+
// This is the legacy / Solana-wallet-friendly form used by UEA_SVM.
1318
func (p Precompile) VerifyEd25519(
1419
method *abi.Method,
1520
args []interface{},
@@ -50,6 +55,43 @@ func (p Precompile) VerifyEd25519(
5055
return method.Outputs.Pack(ok)
5156
}
5257

58+
// VerifyEd25519RawMessage verifies a signature over raw message bytes —
59+
// standard Ed25519 semantics. Use this when the signer used the conventional
60+
// ed25519.Sign(privKey, rawBytes) API.
61+
func (p Precompile) VerifyEd25519RawMessage(
62+
method *abi.Method,
63+
args []interface{},
64+
) ([]byte, error) {
65+
66+
pubKey, ok := args[0].([]byte)
67+
if !ok {
68+
return nil, fmt.Errorf("invalid pubKey type")
69+
}
70+
71+
message, ok := args[1].([]byte)
72+
if !ok {
73+
return nil, fmt.Errorf("invalid message type")
74+
}
75+
76+
signature, ok := args[2].([]byte)
77+
if !ok {
78+
return nil, fmt.Errorf("invalid signature type")
79+
}
80+
81+
pubKeyBytes, err := getSolanaPubKeyFromAddress(pubKey)
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to parse pubKey: %w", err)
84+
}
85+
86+
if len(pubKeyBytes) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize {
87+
return nil, fmt.Errorf("invalid params")
88+
}
89+
90+
ok = ed25519.Verify(pubKeyBytes, message, signature)
91+
92+
return method.Outputs.Pack(ok)
93+
}
94+
5395
func getSolanaPubKeyFromAddress(pubKey []byte) (ed25519.PublicKey, error) {
5496
return ed25519.PublicKey(pubKey), nil
5597
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package usigverifier
2+
3+
import (
4+
"crypto/ed25519"
5+
"encoding/hex"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// Fixed test vectors locking in the two distinct signing conventions exposed
12+
// by the precompile (F-2026-17043 remediation).
13+
//
14+
// - verifyEd25519: signature must be over `"0x" + hex(msgDigest)` (66 ASCII bytes)
15+
// - verifyEd25519RawMessage: signature must be over the raw message bytes
16+
//
17+
// A signature produced for one convention MUST NOT verify under the other.
18+
19+
// Deterministic seed so the test vectors below are reproducible and inspectable.
20+
// Anyone can re-derive these by running ed25519.NewKeyFromSeed on this 32-byte seed.
21+
var testSeed = mustHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
22+
23+
// A 32-byte digest used as the input to both methods. Same input, different
24+
// signing semantics — that's the whole point of the two methods.
25+
var testDigest32 = mustHex("deadbeef00112233445566778899aabbccddeeff0123456789abcdef00ff00ff")
26+
27+
func TestVerifyEd25519_AcceptsHexAsciiSignature(t *testing.T) {
28+
priv := ed25519.NewKeyFromSeed(testSeed)
29+
pub := priv.Public().(ed25519.PublicKey)
30+
31+
// What verifyEd25519 expects the signer to have signed.
32+
hexAsciiBytes := []byte("0x" + hex.EncodeToString(testDigest32))
33+
require.Len(t, hexAsciiBytes, 66, "ASCII hex form must be 66 bytes (0x + 64 hex chars)")
34+
35+
sig := ed25519.Sign(priv, hexAsciiBytes)
36+
require.True(t, ed25519.Verify(pub, hexAsciiBytes, sig),
37+
"sanity: signature must verify against the bytes that were signed")
38+
39+
// What verifyEd25519 actually verifies internally:
40+
verified := ed25519.Verify(pub, []byte("0x"+hex.EncodeToString(testDigest32)), sig)
41+
require.True(t, verified, "verifyEd25519 must accept signature over hex-ASCII form of digest")
42+
}
43+
44+
func TestVerifyEd25519_RejectsRawDigestSignature(t *testing.T) {
45+
priv := ed25519.NewKeyFromSeed(testSeed)
46+
pub := priv.Public().(ed25519.PublicKey)
47+
48+
// Signer mistakenly signs the raw 32-byte digest (the "natural" thing).
49+
rawDigestSig := ed25519.Sign(priv, testDigest32)
50+
51+
// What verifyEd25519 actually verifies internally:
52+
verified := ed25519.Verify(pub, []byte("0x"+hex.EncodeToString(testDigest32)), rawDigestSig)
53+
require.False(t, verified, "verifyEd25519 must reject signature over raw digest bytes")
54+
}
55+
56+
func TestVerifyEd25519RawMessage_AcceptsRawSignature(t *testing.T) {
57+
priv := ed25519.NewKeyFromSeed(testSeed)
58+
pub := priv.Public().(ed25519.PublicKey)
59+
60+
// What verifyEd25519RawMessage expects: signature over the raw message bytes.
61+
rawSig := ed25519.Sign(priv, testDigest32)
62+
63+
// What verifyEd25519RawMessage actually verifies internally:
64+
verified := ed25519.Verify(pub, testDigest32, rawSig)
65+
require.True(t, verified, "verifyEd25519RawMessage must accept signature over raw bytes")
66+
}
67+
68+
func TestVerifyEd25519RawMessage_RejectsHexAsciiSignature(t *testing.T) {
69+
priv := ed25519.NewKeyFromSeed(testSeed)
70+
pub := priv.Public().(ed25519.PublicKey)
71+
72+
// Signer (using legacy convention) signs the hex-ASCII form.
73+
hexAsciiSig := ed25519.Sign(priv, []byte("0x"+hex.EncodeToString(testDigest32)))
74+
75+
// What verifyEd25519RawMessage actually verifies internally:
76+
verified := ed25519.Verify(pub, testDigest32, hexAsciiSig)
77+
require.False(t, verified, "verifyEd25519RawMessage must reject signature over hex-ASCII form")
78+
}
79+
80+
// TestVerifyEd25519RawMessage_ArbitraryMessageLength sanity-checks that the raw
81+
// method works for messages other than 32-byte digests (its whole point —
82+
// no implicit assumption that the message is a digest).
83+
func TestVerifyEd25519RawMessage_ArbitraryMessageLength(t *testing.T) {
84+
priv := ed25519.NewKeyFromSeed(testSeed)
85+
pub := priv.Public().(ed25519.PublicKey)
86+
87+
for _, msg := range [][]byte{
88+
[]byte("hello"),
89+
make([]byte, 0), // empty
90+
make([]byte, 1024), // 1 KiB
91+
[]byte{0x00, 0x01, 0x02, 0x03, 0xff}, // arbitrary short
92+
} {
93+
sig := ed25519.Sign(priv, msg)
94+
require.True(t, ed25519.Verify(pub, msg, sig),
95+
"verifyEd25519RawMessage must work for messages of any length (len=%d)", len(msg))
96+
}
97+
}
98+
99+
func mustHex(s string) []byte {
100+
b, err := hex.DecodeString(s)
101+
if err != nil {
102+
panic(err)
103+
}
104+
return b
105+
}

precompiles/usigverifier/usigverifier.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const (
1616
USigVerifierPrecompileAddress = "0xEC00000000000000000000000000000000000001"
1717
// VerifyEd25519Gas is the gas cost for verifying an Ed25519 signature.
1818
VerifyEd25519Gas uint64 = 4000
19+
// VerifyEd25519RawMessageGas matches VerifyEd25519Gas — same Ed25519
20+
// verification cost, only the message-prep step differs (no hex encoding).
21+
VerifyEd25519RawMessageGas uint64 = 4000
1922
)
2023

2124
var _ vm.PrecompiledContract = &Precompile{}
@@ -70,6 +73,8 @@ func (p Precompile) RequiredGas(input []byte) uint64 {
7073
switch method.Name {
7174
case VerifyEd25519Method:
7275
return VerifyEd25519Gas
76+
case VerifyEd25519RawMessageMethod:
77+
return VerifyEd25519RawMessageGas
7378
default:
7479
return p.Precompile.RequiredGas(input, p.IsTransaction(method))
7580
}
@@ -97,6 +102,8 @@ func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [
97102
switch method.Name {
98103
case VerifyEd25519Method:
99104
bz, err = p.VerifyEd25519(method, args)
105+
case VerifyEd25519RawMessageMethod:
106+
bz, err = p.VerifyEd25519RawMessage(method, args)
100107
default:
101108
return nil, fmt.Errorf(cmn.ErrUnknownMethod, method.Name)
102109
}

proto/uregistry/v1/query.proto

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
syntax = "proto3";
22
package uregistry.v1;
33

4+
import "cosmos/base/query/v1beta1/pagination.proto";
45
import "google/api/annotations.proto";
56
import "uregistry/v1/genesis.proto";
67
import "uregistry/v1/types.proto";
@@ -61,11 +62,14 @@ message QueryChainConfigResponse {
6162
}
6263

6364
// QueryAllChainConfigsRequest is the request type for the Query/AllChainConfigs RPC method.
64-
message QueryAllChainConfigsRequest {}
65+
message QueryAllChainConfigsRequest {
66+
cosmos.base.query.v1beta1.PageRequest pagination = 1;
67+
}
6568

6669
// QueryAllChainConfigsResponse is the response type for the Query/AllChainConfigs RPC method.
6770
message QueryAllChainConfigsResponse {
6871
repeated ChainConfig configs = 1;
72+
cosmos.base.query.v1beta1.PageResponse pagination = 2;
6973
}
7074

7175
// TokenConfig
@@ -81,19 +85,24 @@ message QueryTokenConfigResponse {
8185
}
8286

8387
// QueryAllTokenConfigsRequest is the request type for the Query/AllTokenConfigs RPC method.
84-
message QueryAllTokenConfigsRequest {}
88+
message QueryAllTokenConfigsRequest {
89+
cosmos.base.query.v1beta1.PageRequest pagination = 1;
90+
}
8591

8692
// QueryAllTokenConfigsResponse is the response type for the Query/AllTokenConfigs RPC method.
8793
message QueryAllTokenConfigsResponse {
8894
repeated TokenConfig configs = 1;
95+
cosmos.base.query.v1beta1.PageResponse pagination = 2;
8996
}
9097

9198
// QueryTokenConfigsByChainRequest is the request type for the Query/TokenConfigsByChain RPC method.
9299
message QueryTokenConfigsByChainRequest {
93100
string chain = 1;
101+
cosmos.base.query.v1beta1.PageRequest pagination = 2;
94102
}
95103

96104
// QueryTokenConfigsByChainResponse is the response type for the Query/TokenConfigsByChain RPC method.
97105
message QueryTokenConfigsByChainResponse {
98106
repeated TokenConfig configs = 1;
107+
cosmos.base.query.v1beta1.PageResponse pagination = 2;
99108
}

0 commit comments

Comments
 (0)