Skip to content
Closed
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
19 changes: 19 additions & 0 deletions gittensor/validator/issue_competitions/contract_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,19 @@ def _exec_contract_raw(
bt.logging.error(err)
return None, err

@staticmethod
def _encode_compact_len(length: int) -> bytes:
"""SCALE compact-encode a non-negative length (used as the Vec<u8>/str prefix)."""
if length < 0:
raise ValueError(f'Length must be non-negative, got {length}')
if length <= 0b0011_1111: # single-byte mode
return bytes([length << 2])
if length <= 0b0011_1111_1111_1111: # two-byte mode
return struct.pack('<H', (length << 2) | 0b01)
if length <= 0x3FFF_FFFF: # four-byte mode
return struct.pack('<I', (length << 2) | 0b10)
raise ValueError(f'Length too large to SCALE compact-encode: {length}')

def _encode_args(self, method_name: str, args: dict) -> bytes:
"""SCALE-encode method arguments using hardcoded type definitions."""
arg_types = CONTRACT_ARG_TYPES.get(method_name, [])
Expand All @@ -568,6 +581,12 @@ def _encode_args(self, method_name: str, args: dict) -> bytes:
encoded += struct.pack('<Q', value)
elif type_def == 'u128':
encoded += struct.pack('<QQ', value & 0xFFFFFFFFFFFFFFFF, value >> 64)
elif type_def == 'str':
# SCALE encodes str as a Vec<u8>: compact length prefix + UTF-8 bytes.
if not isinstance(value, str):
raise ValueError(f'Expected str for arg {arg_name}, got {type(value)}')
utf8 = value.encode('utf-8')
encoded += self._encode_compact_len(len(utf8)) + utf8
elif type_def == 'AccountId':
if isinstance(value, str):
encoded += bytes.fromhex(self.subtensor.substrate.ss58_decode(value))
Expand Down
48 changes: 48 additions & 0 deletions tests/validator/test_contract_client_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""Tests for IssueCompetitionContractClient transaction methods."""

import hashlib
import struct
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

Expand Down Expand Up @@ -104,6 +105,53 @@ def test_exception_returns_false(client, wallet, method, kwargs_fn, _cm, _ea, _h
assert getattr(client, method)(**kwargs_fn(wallet)) is False


def test_register_issue_encodes_str_args(client):
"""register_issue declares github_url/repository_full_name as `str`; encoding
must SCALE-encode them (compact length prefix + UTF-8) rather than raising.

Regression test for the `Unsupported type: str` bug (#1375).
"""
github_url = 'https://github.com/owner/repo/issues/1'
repo = 'owner/repo'

encoded = client._encode_args(
'register_issue',
{
'github_url': github_url,
'repository_full_name': repo,
'issue_number': 1,
'target_bounty': 10_000_000_000,
},
)

url_bytes = github_url.encode('utf-8')
repo_bytes = repo.encode('utf-8')
expected = (
IssueCompetitionContractClient._encode_compact_len(len(url_bytes))
+ url_bytes
+ IssueCompetitionContractClient._encode_compact_len(len(repo_bytes))
+ repo_bytes
+ struct.pack('<I', 1) # issue_number: u32
+ struct.pack('<QQ', 10_000_000_000, 0) # target_bounty: u128
)
assert encoded == expected


@pytest.mark.parametrize(
'length, expected',
[
(0, b'\x00'),
(1, b'\x04'),
(63, b'\xfc'), # single-byte mode upper bound
(64, b'\x01\x01'), # two-byte mode lower bound
(16383, b'\xfd\xff'), # two-byte mode upper bound
(16384, b'\x02\x00\x01\x00'), # four-byte mode lower bound
],
)
def test_encode_compact_len_modes(length, expected):
assert IssueCompetitionContractClient._encode_compact_len(length) == expected


def _packed_treasury_storage():
return SimpleNamespace(owner=b'\x01' * 32, treasury_hotkey=b'\x02' * 32, netuid=42)

Expand Down