Skip to content

Commit 8de5a16

Browse files
thomas-nguyclaude
andauthored
feat: support unblockable list (#2100)
* feat: support unblock able list (#2095) * docs: add unblockable list design spec Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: unblockable list support * remove docs superpower * fix changelog * chore: apply black formatting to e2ee test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix some encoding inconsistency * add testing addresses * patch nix --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * update changelog * revert testenv.nix fetchCargoVendor change Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix changelog --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6385c1c commit 8de5a16

6 files changed

Lines changed: 175 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Improvements
66

7+
* [#2100](https://github.com/crypto-org-chain/cronos/pull/2100) feat: support unblockable addresses
78
* [#1977](https://github.com/crypto-org-chain/cronos/pull/1977) Upgrade rocksdb to v10.9.1 and add tuning profiles.
89
* [#1970](https://github.com/crypto-org-chain/cronos/pull/1970) feat(testground): refine testground test process in local
910
* [#1971](https://github.com/crypto-org-chain/cronos/pull/1971) Upgrade cosmos-sdk to v0.53.4.

app/app.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,9 @@ func (app *App) setAnteHandler(txConfig client.TxConfig, mempoolMaxTxs int, blac
10911091
if err != nil {
10921092
return fmt.Errorf("invalid bech32 address: %s, err: %w", str, err)
10931093
}
1094+
if IsUnblockable(addr) {
1095+
continue
1096+
}
10941097

10951098
blockedMap[addr.String()] = struct{}{}
10961099
}

app/proposal.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ func (h *ProposalHandler) SetBlockList(blob []byte) error {
104104
if err != nil {
105105
return fmt.Errorf("invalid bech32 address: %s, err: %w", s, err)
106106
}
107+
if IsUnblockable(addr) {
108+
continue
109+
}
107110
encoded, err := h.addressCodec.BytesToString(addr)
108111
if err != nil {
109112
return fmt.Errorf("invalid bech32 address: %s, err: %w", s, err)
@@ -167,8 +170,12 @@ func (h *ProposalHandler) ValidateTransaction(tx sdk.Tx, txBz []byte) error {
167170
for _, auth := range ethTx.SetCodeAuthorizations() {
168171
addr, err := auth.Authority()
169172
if err == nil {
170-
if _, ok := h.blocklist[sdk.AccAddress(addr.Bytes()).String()]; ok {
171-
return fmt.Errorf("signer is blocked: %s", addr.String())
173+
encoded, err := h.addressCodec.BytesToString(addr.Bytes())
174+
if err != nil {
175+
return fmt.Errorf("invalid bech32 address: %s, err: %w", addr, err)
176+
}
177+
if _, ok := h.blocklist[encoded]; ok {
178+
return fmt.Errorf("signer is blocked: %s", encoded)
172179
}
173180
}
174181
// check the target address

app/unblockable.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package app
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
)
8+
9+
// unblockableHexAddresses is the hardcoded set of EVM addresses that must never
10+
// be blocked by either the mempool or the consensus blocklist. Entries here are
11+
// filtered out when building both blocklist lookup maps, so they can never take
12+
// effect regardless of node config or on-chain MsgStoreBlockList contents. This
13+
// list is intentionally not configurable.
14+
var unblockableHexAddresses = []string{
15+
"0x007F588ca3FFe53F20cb03553Ca38bb13542FF89",
16+
"0x4356e8c6Ddca1964b22ECd35cb74A74BDdeDe2a3",
17+
"0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D",
18+
"0xC543052518F7787936522926242f86BADD39Cb46",
19+
"0x405fCcd57dA8ffbd0F2C38D57B1DA933b00B7bC6",
20+
"0x31f58b04f03d791c56de058211f0c767af96b464",
21+
"0xA6dE01a2d62C6B5f3525d768f34d276652C554c8",
22+
"0x192E362B2810f604e0618B5033d26F3b85E05AF9",
23+
"0xA4EC772557A0E72985EA2532B72f363fA5379C11",
24+
"0xeae603121f38d43e801254d172fd0bee959918b6",
25+
"0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
26+
"0x81D40F21F12A8F0E3252Bccb954D722d4c464B64",
27+
"0xfd78EE919681417d192449715b2594ab58f5D002",
28+
"0x1CcaFdffBC1b7B5C499c97322F961B7d929a41b4",
29+
"0x01fB02b8209c8A5c271a4fCB700Bfb9C80b5B614",
30+
"0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78",
31+
"0xda95b41655EA94d93241d97432DAfb6B27148289",
32+
"0x3812789185aF19B2002c0DfAcC3C7926eCbA674D",
33+
"0xc375fe4b88c5858bD5521917D0C3418856Ac1FB1",
34+
"0x69F762B2f1706e15eF77F7F8C5b07Fda66844d67",
35+
"0x7ea46aDC49Eb1228350f76327c94b9F06A032bd9",
36+
"0x4E6B78bF26881E38FfB939945116Dd8d4DD48551",
37+
"0xBE3866F2Cdddc6A5dE252e50EFD9429BD3495007",
38+
"0x26132C4bCceFa08bBEa4Ca85E3dBB797Ba8C1f09",
39+
"0x1a061EDeA58DA99c2d09FdD1f9e6BA9DaB1413ff",
40+
"0xa64915eaf58b245b2d2bbe7a7dc8c69956ac8670",
41+
// test
42+
"0xCC5d9bF5C3662D8A86A45ed23B300bc06ab36644",
43+
"0xDaB2C01b1eBdf1D33eCF6Aff3a29b977a1EFba41",
44+
}
45+
46+
// unblockableSet maps the raw 20-byte account-address form of each unblockable
47+
// address to an empty struct, for O(1) lookup.
48+
var unblockableSet = buildUnblockableSet(unblockableHexAddresses)
49+
50+
// buildUnblockableSet validates and indexes the hardcoded addresses. It panics
51+
// on any malformed entry, since that is a programming error in a hardcoded list
52+
// and should fail fast at startup.
53+
func buildUnblockableSet(hexAddrs []string) map[string]struct{} {
54+
m := make(map[string]struct{}, len(hexAddrs))
55+
for _, h := range hexAddrs {
56+
if !common.IsHexAddress(h) {
57+
panic(fmt.Sprintf("invalid unblockable address: %s", h))
58+
}
59+
m[string(common.HexToAddress(h).Bytes())] = struct{}{}
60+
}
61+
return m
62+
}
63+
64+
// IsUnblockable reports whether the given account-address bytes are in the
65+
// hardcoded unblockable list.
66+
func IsUnblockable(addr []byte) bool {
67+
_, ok := unblockableSet[string(addr)]
68+
return ok
69+
}

app/unblockable_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package app
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestIsUnblockable(t *testing.T) {
11+
// a listed address (raw 20-byte form) is unblockable
12+
listed := common.HexToAddress("0x007F588ca3FFe53F20cb03553Ca38bb13542FF89")
13+
require.True(t, IsUnblockable(listed.Bytes()))
14+
15+
// case-insensitive: the same address in a different case is still matched,
16+
// since matching is on raw bytes
17+
listedLower := common.HexToAddress("0x007f588ca3ffe53f20cb03553ca38bb13542ff89")
18+
require.True(t, IsUnblockable(listedLower.Bytes()))
19+
20+
// an address not in the list is blockable
21+
other := common.HexToAddress("0x0000000000000000000000000000000000000001")
22+
require.False(t, IsUnblockable(other.Bytes()))
23+
24+
// empty / wrong-length input does not match
25+
require.False(t, IsUnblockable(nil))
26+
require.False(t, IsUnblockable([]byte{0x01, 0x02}))
27+
}
28+
29+
func TestBuildUnblockableSet(t *testing.T) {
30+
// every hardcoded entry is indexed and looked up by its raw bytes
31+
require.Len(t, unblockableSet, len(unblockableHexAddresses))
32+
for _, h := range unblockableHexAddresses {
33+
require.True(t, IsUnblockable(common.HexToAddress(h).Bytes()), h)
34+
}
35+
36+
// a malformed entry fails fast at construction time
37+
require.Panics(t, func() {
38+
buildUnblockableSet([]string{"not-an-address"})
39+
})
40+
}

integration_tests/test_e2ee.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
from .network import Cronos
99
from .utils import (
1010
ADDRS,
11+
KEYS,
1112
bech32_to_eth,
1213
derive_new_account,
14+
eth_to_bech32,
1315
fund_acc,
16+
get_account_nonce,
17+
send_transaction,
18+
sign_transaction,
1419
wait_for_new_blocks,
1520
wait_for_port,
1621
)
@@ -240,3 +245,51 @@ def test_block_list_contract(cronos):
240245
wait_for_new_blocks(cli, 1)
241246
assert nonce + 1 == get_nonce(cli, user)
242247
assert w3.eth.get_filter_changes(flt.filter_id) == []
248+
249+
250+
def test_block_list_unblockable(cronos):
251+
"""Addresses in the hardcoded unblockable list (app/unblockable.go) are
252+
filtered out of any submitted blocklist, so they can never be blocked."""
253+
gen_validator_identity(cronos)
254+
cli = cronos.cosmos_cli()
255+
w3 = cronos.w3
256+
257+
# one of the hardcoded unblockable addresses; eth_to_bech32 is how it would
258+
# be expressed when submitted in a blocklist
259+
unblockable_eth = to_checksum_address("0x007F588ca3FFe53F20cb03553Ca38bb13542FF89")
260+
unblockable = eth_to_bech32(unblockable_eth)
261+
262+
# control: a normal address that must stay blocked as a destination
263+
control = cli.address("signer1")
264+
265+
# block both the control and the unblockable address at the same time
266+
encrypt_to_validators(cli, {"addresses": [control, unblockable]})
267+
268+
base_port = cronos.base_port(0)
269+
wait_for_port(ports.evmrpc_ws_port(base_port))
270+
271+
# 1) a tx whose destination is the control address must be filtered out of
272+
# blocks: send it without waiting for a receipt, then confirm below that
273+
# the sender's nonce does not advance while the blocklist is active.
274+
blocked_nonce = get_account_nonce(w3, KEYS["community"])
275+
signed = sign_transaction(
276+
w3,
277+
{"to": to_checksum_address(bech32_to_eth(control)), "value": 1},
278+
KEYS["community"],
279+
)
280+
w3.eth.send_raw_transaction(signed.raw_transaction)
281+
282+
# 2) a tx whose destination is the unblockable address IS included, even
283+
# though that address is present in the submitted blocklist -> it was
284+
# filtered out, and the filtering is selective (the control stays blocked).
285+
receipt = send_transaction(w3, {"to": unblockable_eth, "value": 1}, KEYS["signer2"])
286+
assert receipt.status == 1
287+
288+
# the control tx is still stuck: its sender nonce has not advanced
289+
wait_for_new_blocks(cli, 1)
290+
assert get_account_nonce(w3, KEYS["community"]) == blocked_nonce
291+
292+
# clearing the blocklist lets the previously blocked tx through
293+
encrypt_to_validators(cli, {})
294+
wait_for_new_blocks(cli, 2)
295+
assert get_account_nonce(w3, KEYS["community"]) == blocked_nonce + 1

0 commit comments

Comments
 (0)