Skip to content

Commit 29aa526

Browse files
Herklosclaude
andcommitted
[Sync] Replace blockchain entitlement with JSON collection-based access control
Remove the entire chain abstraction layer (EvmChain, ChainRegistry, AbstractChain) and smart contract entitlement (ownerOf/hasAccess). Keep only standalone EIP-191 auth signature functions. Replace with two new collections: - entitlements (users/{identity}/entitlements): admin-managed paid features - members (products/{productId}/members): owner-managed strategy access The role enricher now reads from the object store with a TTL cache instead of querying blockchain contracts. Auth uses a VerifySignatureFn callable instead of the chain registry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 16bd746 commit 29aa526

18 files changed

Lines changed: 341 additions & 575 deletions

packages/sync/octobot_sync/app.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,23 @@
2525
from starfish_server.replica import ReplicaManager
2626

2727
import octobot_sync.auth as auth
28-
import octobot_sync.chain as chain
28+
import octobot_sync.chain.evm as evm
2929
import octobot_sync.constants as constants
3030
import octobot_sync.sync as sync
31+
from octobot_sync.sync.role_resolver import VerifySignatureFn
32+
33+
DEFAULT_SUPPORTED_CHAINS = frozenset({"evm:8453"})
34+
35+
36+
async def _verify_evm(canonical: str, signature: str, pubkey: str) -> bool:
37+
return evm.verify_evm(canonical, signature, pubkey)
3138

3239

3340
def create_app(
3441
nonce: auth.NonceStore,
3542
object_store: AbstractObjectStore,
36-
registry: chain.ChainRegistry,
43+
verify_signature: VerifySignatureFn = _verify_evm,
44+
supported_chains: set[str] | None = None,
3745
collections_path: str | None = None,
3846
primary_url: str | None = None,
3947
auth_provider: auth.StarfishAuthProvider | None = None,
@@ -42,6 +50,9 @@ def create_app(
4250
) -> FastAPI:
4351
app = FastAPI(title="OctoBot Sync — Signal Sync Server")
4452

53+
if supported_chains is None:
54+
supported_chains = DEFAULT_SUPPORTED_CHAINS
55+
4556
platform_pubkey = os.environ["PLATFORM_PUBKEY_EVM"]
4657
encryption_secret = os.environ["ENCRYPTION_SECRET"]
4758
platform_encryption_secret = os.environ["PLATFORM_ENCRYPTION_SECRET"]
@@ -66,14 +77,16 @@ def create_app(
6677
SyncRouterOptions(
6778
store=object_store,
6879
config=sync_config,
69-
role_resolver=sync.create_role_resolver(registry, nonce, platform_pubkey),
70-
role_enricher=sync.create_role_enricher(registry),
80+
role_resolver=sync.create_role_resolver(
81+
verify_signature, nonce, platform_pubkey, supported_chains,
82+
),
83+
role_enricher=sync.create_role_enricher(object_store),
7184
encryption_secret=encryption_secret,
7285
identity_encryption_info=constants.HKDF_INFO_USER_DATA,
7386
server_encryption_secret=platform_encryption_secret,
7487
server_identity=platform_pubkey,
7588
server_encryption_info=constants.HKDF_INFO_PLATFORM_DATA,
76-
signature_verifier=sync.create_signature_verifier(registry),
89+
signature_verifier=sync.create_signature_verifier(verify_signature),
7790
replica_manager=replica_manager,
7891
)
7992
)

packages/sync/octobot_sync/auth/provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def address(self) -> str:
3636
return self._address
3737

3838
async def sign_payload(self, data: str) -> str:
39-
msg_hash = evm._eip191_hash(data)
39+
msg_hash = evm.eip191_hash(data)
4040
signed = self._w3.eth.account._sign_hash(msg_hash, private_key=self._private_key)
4141
return signed.signature.hex()
4242

@@ -47,7 +47,7 @@ async def __call__(
4747
nonce = str(uuid.uuid4())
4848
body_hash = canonical.hash_body(body)
4949
msg = canonical.build_canonical(method, path, ts, nonce, body_hash)
50-
msg_hash = evm._eip191_hash(msg)
50+
msg_hash = evm.eip191_hash(msg)
5151
signed = self._w3.eth.account._sign_hash(msg_hash, private_key=self._private_key)
5252
return {
5353
constants.HEADER_PUBKEY: self._address,

packages/sync/octobot_sync/chain/__init__.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,18 @@
1414
# You should have received a copy of the GNU Lesser General Public
1515
# License along with this library.
1616

17-
from octobot_sync.chain import interface
18-
from octobot_sync.chain.interface import (
19-
AbstractChain,
20-
Item,
21-
Wallet,
22-
)
23-
24-
from octobot_sync.chain import evm
2517
from octobot_sync.chain.evm import (
26-
EvmChain,
18+
Wallet,
19+
eip191_hash,
2720
create_evm_wallet,
2821
address_from_evm_key,
2922
verify_evm,
3023
)
3124

32-
from octobot_sync.chain import registry
33-
from octobot_sync.chain.registry import (
34-
ChainRegistry,
35-
)
36-
3725
__all__ = [
38-
"AbstractChain",
39-
"Item",
4026
"Wallet",
41-
"EvmChain",
27+
"eip191_hash",
4228
"create_evm_wallet",
4329
"address_from_evm_key",
4430
"verify_evm",
45-
"ChainRegistry",
4631
]

packages/sync/octobot_sync/chain/evm.py

Lines changed: 10 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -14,67 +14,27 @@
1414
# You should have received a copy of the GNU General Public
1515
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
1616

17-
import functools
17+
from dataclasses import dataclass
1818

19-
from cachetools import TTLCache
20-
from web3 import Web3, AsyncWeb3
21-
from web3.providers import AsyncHTTPProvider
19+
from web3 import Web3
2220

23-
import octobot_sync.chain.interface as chain_interface
2421

25-
_SENTINEL = object()
22+
@dataclass
23+
class Wallet:
24+
private_key: str
25+
address: str
2626

2727

28-
def _async_ttl_cached(ttl_s: float, maxsize: int = 1024):
29-
def decorator(fn):
30-
cache = TTLCache(maxsize=maxsize, ttl=ttl_s)
31-
32-
@functools.wraps(fn)
33-
async def wrapper(*args, **kwargs):
34-
key = args[1:] # skip self
35-
result = cache.get(key, _SENTINEL)
36-
if result is not _SENTINEL:
37-
return result
38-
result = await fn(*args, **kwargs)
39-
cache[key] = result
40-
return result
41-
42-
wrapper.cache = cache
43-
return wrapper
44-
45-
return decorator
46-
47-
OCTOBOT_PRODUCT_ABI = [
48-
{
49-
"type": "function",
50-
"name": "ownerOf",
51-
"inputs": [{"name": "tokenId", "type": "uint256"}],
52-
"outputs": [{"name": "owner", "type": "address"}],
53-
"stateMutability": "view",
54-
},
55-
{
56-
"type": "function",
57-
"name": "hasAccess",
58-
"inputs": [
59-
{"name": "user", "type": "address"},
60-
{"name": "itemId", "type": "uint256"},
61-
],
62-
"outputs": [{"name": "", "type": "bool"}],
63-
"stateMutability": "view",
64-
},
65-
]
66-
67-
68-
def _eip191_hash(text: str) -> bytes:
28+
def eip191_hash(text: str) -> bytes:
6929
"""Compute the EIP-191 personal_sign message hash without eth_account."""
7030
msg_bytes = text.encode("utf-8")
7131
prefix = f"\x19Ethereum Signed Message:\n{len(msg_bytes)}".encode("utf-8")
7232
return Web3.keccak(prefix + msg_bytes)
7333

7434

75-
def create_evm_wallet() -> chain_interface.Wallet:
35+
def create_evm_wallet() -> Wallet:
7636
account = Web3().eth.account.create()
77-
return chain_interface.Wallet(
37+
return Wallet(
7838
private_key=account.key.hex(),
7939
address=account.address,
8040
)
@@ -87,72 +47,8 @@ def address_from_evm_key(private_key: str) -> str:
8747
def verify_evm(canonical: str, signature: str, address: str) -> bool:
8848
"""Verify an EIP-191 personal_sign signature via web3."""
8949
try:
90-
msg_hash = _eip191_hash(canonical)
50+
msg_hash = eip191_hash(canonical)
9151
recovered = Web3().eth.account._recover_hash(msg_hash, signature=signature)
9252
return recovered.lower() == address.lower()
9353
except Exception:
9454
return False
95-
96-
97-
98-
class EvmChain:
99-
def __init__(
100-
self,
101-
chain_id: str,
102-
rpc_url: str | None = None,
103-
contract_address: str | None = None,
104-
) -> None:
105-
self._id = chain_id
106-
if rpc_url and contract_address:
107-
self._contract_address = AsyncWeb3.to_checksum_address(contract_address)
108-
self._w3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
109-
self._contract = self._w3.eth.contract(
110-
address=self._contract_address, abi=OCTOBOT_PRODUCT_ABI
111-
)
112-
else:
113-
self._contract = None
114-
115-
@property
116-
def id(self) -> str:
117-
return self._id
118-
119-
@staticmethod
120-
def create_wallet() -> chain_interface.Wallet:
121-
return create_evm_wallet()
122-
123-
@staticmethod
124-
def address_from_key(private_key: str) -> str:
125-
return address_from_evm_key(private_key)
126-
127-
async def verify_signature(
128-
self, canonical: str, signature: str, pubkey_or_address: str
129-
) -> bool:
130-
return verify_evm(canonical, signature, pubkey_or_address)
131-
132-
def _require_contract(self) -> None:
133-
if self._contract is None:
134-
raise RuntimeError(
135-
f"Chain {self._id}: RPC not configured. "
136-
"Set EVM_BASE_RPC and EVM_CONTRACT_BASE environment variables."
137-
)
138-
139-
@_async_ttl_cached(ttl_s=30)
140-
async def get_item(self, item_id: str) -> chain_interface.Item | None:
141-
self._require_contract()
142-
try:
143-
owner = await self._contract.functions.ownerOf(int(item_id)).call()
144-
return chain_interface.Item(id=item_id, owner=owner)
145-
except Exception:
146-
return None
147-
148-
@_async_ttl_cached(ttl_s=365 * 86400)
149-
async def is_item_owner(self, item_id: str, pubkey_or_address: str) -> bool:
150-
item = await self.get_item(item_id)
151-
return item is not None and item.owner.lower() == pubkey_or_address.lower()
152-
153-
@_async_ttl_cached(ttl_s=60)
154-
async def has_access(self, item_id: str, user_address: str) -> bool:
155-
self._require_contract()
156-
return await self._contract.functions.hasAccess(
157-
AsyncWeb3.to_checksum_address(user_address), int(item_id)
158-
).call()

packages/sync/octobot_sync/chain/interface.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

packages/sync/octobot_sync/chain/registry.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)