Skip to content
Open
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
206 changes: 145 additions & 61 deletions tests/unit/test_chain_solana.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from dataclasses import asdict, dataclass
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any, Dict, List, Union

import base58
import pytest
from nacl.signing import VerifyKey

# Assuming these modules are part of the Aleph SDK
from aleph.sdk.chains.common import get_verification_buffer
from aleph.sdk.chains.solana import (
SOLAccount,
Expand All @@ -19,72 +21,113 @@

@dataclass
class Message:
"""A minimal message structure used for signing and verification tests."""
chain: str
sender: str
type: str
item_hash: str


def test_get_fallback_account():
def test_get_fallback_account() -> None:
"""
Tests the creation of a SOLAccount instance using the fallback mechanism,
which typically generates a new keypair and saves it to a temporary file.
"""
with NamedTemporaryFile() as private_key_file:
account: SOLAccount = get_fallback_account(path=Path(private_key_file.name))

# Assert fundamental characteristics of the generated account
assert account.CHAIN == "SOL"
assert account.CURVE == "curve25519"
# Check if the signing key has a verification key (i.e., it's a valid keypair)
assert account._signing_key.verify_key
assert isinstance(account.private_key, bytes)
# Check that the raw private key (seed) is 32 bytes long
assert len(account.private_key) == 32


@pytest.mark.asyncio
async def test_SOLAccount(solana_account):
message = asdict(
async def test_SOLAccount(solana_account: SOLAccount) -> None:
"""
Tests the signing capability of a SOLAccount and verifies the signature
using raw cryptography tools (PyNaCl).
"""
# 1. Arrange: Prepare message and signature process
message: Dict[str, Any] = asdict(
Message("SOL", solana_account.get_address(), "SomeType", "ItemHash")
)
initial_message = message.copy()

# 2. Act: Sign the message (modifies the message dict in place)
await solana_account.sign_message(message)
assert message["signature"]
assert "signature" in message

address = message["sender"]
assert address
# 3. Assert & Verification Checks
address: str = message["sender"]
assert isinstance(address, str)
# assert len(address) == 44 # can also be 43?
signature = json.loads(message["signature"])

pubkey = base58.b58decode(signature["publicKey"])
assert isinstance(pubkey, bytes)
assert len(pubkey) == 32

verify_key = VerifyKey(pubkey)
verification_buffer = get_verification_buffer(message)
# Parse the signature object which is stored as a JSON string in the message
signature_obj: Dict[str, str] = json.loads(message["signature"])

# Extract the public key from the signature object
pubkey_base58: str = signature_obj["publicKey"]
pubkey_bytes: bytes = base58.b58decode(pubkey_base58)
assert isinstance(pubkey_bytes, bytes)
assert len(pubkey_bytes) == 32

# Instantiate the verification key using raw PyNaCl/nacl
verify_key = VerifyKey(pubkey_bytes)

# Get the canonical buffer that was signed
verification_buffer: bytes = get_verification_buffer(message)
# Ensure the verification buffer is based on the unsigned content
assert get_verification_buffer(initial_message) == verification_buffer
verif = verify_key.verify(
verification_buffer, signature=base58.b58decode(signature["signature"])

# Verify the signature against the buffer
verified_buffer = verify_key.verify(
verification_buffer, signature=base58.b58decode(signature_obj["signature"])
)

assert verif == verification_buffer
assert message["sender"] == signature["publicKey"]
# If verification is successful, the returned value is the buffer itself
assert verified_buffer == verification_buffer

# Sanity check: the sender address in the message must match the public key in the signature
assert message["sender"] == signature_obj["publicKey"]

pubkey = solana_account.get_public_key()
assert isinstance(pubkey, str)
assert len(pubkey) == 64
# Check the format of the key returned by the account object (assumed to be hex)
pubkey_hex: str = solana_account.get_public_key()
assert isinstance(pubkey_hex, str)
# A 32-byte key is 64 characters in hexadecimal format
assert len(pubkey_hex) == 64


@pytest.mark.asyncio
async def test_decrypt_curve25516(solana_account):
async def test_decrypt_curve25516(solana_account: SOLAccount) -> None:
"""
Tests the encryption and decryption loop using the Ed25519/curve25519 properties.
"""
assert solana_account.CURVE == "curve25519"
content = b"SomeContent"

encrypted = await solana_account.encrypt(content)
# Encrypt
encrypted: bytes = await solana_account.encrypt(content)
assert isinstance(encrypted, bytes)
decrypted = await solana_account.decrypt(encrypted)

# Decrypt
decrypted: bytes = await solana_account.decrypt(encrypted)
assert isinstance(decrypted, bytes)

# Assert original content matches decrypted content
assert content == decrypted


@pytest.mark.asyncio
async def test_verify_signature(solana_account):
message = asdict(
async def test_verify_signature(solana_account: SOLAccount) -> None:
"""
Tests the utility verification function with both string (Base58) and raw byte inputs.
"""
# 1. Arrange: Sign the message
message: Dict[str, Any] = asdict(
Message(
"SOL",
solana_account.get_address(),
Expand All @@ -93,30 +136,48 @@ async def test_verify_signature(solana_account):
)
)
await solana_account.sign_message(message)
assert message["signature"]
raw_signature = json.loads(message["signature"])["signature"]
assert isinstance(raw_signature, str)

verify_signature(raw_signature, message["sender"], get_verification_buffer(message))

# as bytes
assert "signature" in message

raw_signature_b58: str = json.loads(message["signature"])["signature"]
assert isinstance(raw_signature_b58, str)
verification_buffer: bytes = get_verification_buffer(message)

# 2. Test 1: Verify using Base58 encoded string inputs (standard API usage)
verify_signature(raw_signature_b58, message["sender"], verification_buffer)

# 3. Test 2: Verify using raw bytes inputs (for stricter type checks)
# NOTE: The decode("utf-8") call from the original script is removed here,
# assuming the verification buffer must be passed as raw bytes.
verify_signature(
base58.b58decode(raw_signature),
base58.b58decode(message["sender"]),
get_verification_buffer(message).decode("utf-8"),
base58.b58decode(raw_signature_b58), # signature as bytes
base58.b58decode(message["sender"]), # sender (public key) as bytes
verification_buffer, # verification buffer as bytes
)


@pytest.mark.asyncio
async def test_verify_signature_with_processed_message(solana_account, json_messages):
message = json_messages[0]
signature = json.loads(message["signature"])["signature"]
async def test_verify_signature_with_processed_message(solana_account: SOLAccount, json_messages: List[Dict[str, Any]]) -> None:
"""
Tests signature verification using a pre-processed/fixture message structure.
"""
# Select the first fixture message
message: Dict[str, Any] = json_messages[0]

# Extract the Base58 signature string
signature: str = json.loads(message["signature"])["signature"]

# Perform verification
verify_signature(signature, message["sender"], get_verification_buffer(message))


@pytest.mark.asyncio
async def test_verify_signature_with_forged_signature(solana_account):
message = asdict(
async def test_verify_signature_with_forged_signature(solana_account: SOLAccount) -> None:
"""
Tests that the verification function raises BadSignatureError when provided
with a valid public key but a randomly forged 64-byte signature.
"""
# 1. Arrange: Create and sign a valid message
message: Dict[str, Any] = asdict(
Message(
"SOL",
solana_account.get_address(),
Expand All @@ -125,28 +186,40 @@ async def test_verify_signature_with_forged_signature(solana_account):
)
)
await solana_account.sign_message(message)
assert message["signature"]
# create forged 64 bit signature from random bytes
forged = base58.b58encode(bytes(64)).decode("utf-8")
assert "signature" in message

# 2. Act: Create a forged signature
# Solana signatures are 64 bytes. Create a base58 string from 64 null bytes.
forged: str = base58.b58encode(bytes(64)).decode("utf-8")

# 3. Assert: Verification must fail
with pytest.raises(BadSignatureError):
verify_signature(forged, message["sender"], get_verification_buffer(message))


@pytest.mark.asyncio
async def test_sign_raw(solana_account):
buffer = b"SomeBuffer"
signature = await solana_account.sign_raw(buffer)
assert signature
async def test_sign_raw(solana_account: SOLAccount) -> None:
"""
Tests the sign_raw utility for signing an arbitrary byte buffer.
"""
# 1. Arrange: Arbitrary buffer
buffer = b"SomeBufferToSign"

# 2. Act: Sign the raw buffer
signature: bytes = await solana_account.sign_raw(buffer)
assert isinstance(signature, bytes)


# 3. Assert: Verify the raw signature using the account's address (public key)
verify_signature(signature, solana_account.get_address(), buffer)


def test_parse_solana_private_key_bytes():
def test_parse_solana_private_key_bytes() -> None:
"""
Tests parsing a 32-byte raw private key (seed).
"""
# Valid 32-byte private key
private_key_bytes = bytes(range(32))
parsed_key = parse_private_key(private_key_bytes)
parsed_key: bytes = parse_private_key(private_key_bytes)
assert isinstance(parsed_key, bytes)
assert len(parsed_key) == 32
assert parsed_key == private_key_bytes
Expand All @@ -158,36 +231,47 @@ def test_parse_solana_private_key_bytes():
parse_private_key(bytes(range(31)))


def test_parse_solana_private_key_base58():
# Valid base58 private key (32 bytes)
base58_key = base58.b58encode(bytes(range(32))).decode("utf-8")
parsed_key = parse_private_key(base58_key)
def test_parse_solana_private_key_base58() -> None:
"""
Tests parsing a private key encoded in Base58 (either 32-byte seed or 64-byte keypair).
"""
# Valid 32-byte seed encoded in Base58
seed_bytes = bytes(range(32))
base58_key: str = base58.b58encode(seed_bytes).decode("utf-8")
parsed_key: bytes = parse_private_key(base58_key)
assert isinstance(parsed_key, bytes)
assert len(parsed_key) == 32
assert parsed_key == seed_bytes

# Invalid base58 key (not decodable)
with pytest.raises(ValueError, match="Invalid base58 encoded private key"):
parse_private_key("invalid_base58_key")

# Invalid base58 key (wrong length)
# Invalid base58 key (wrong length after decoding)
with pytest.raises(
ValueError,
match="The base58 decoded private key must be either 32 or 64 bytes long.",
):
# 31 bytes -> invalid length
parse_private_key(base58.b58encode(bytes(range(31))).decode("utf-8"))


def test_parse_solana_private_key_list():
# Valid list of uint8 integers (64 elements, but we only take the first 32 for private key)
uint8_list = list(range(64))
parsed_key = parse_private_key(uint8_list)
def test_parse_solana_private_key_list() -> None:
"""
Tests parsing a private key supplied as a list of integers (Solana's JSON keypair format).
The function should extract only the first 32 bytes (the private key/seed).
"""
# Valid list of uint8 integers (64 elements: 32 bytes private key + 32 bytes public key)
uint8_list: List[int] = list(range(64))
parsed_key: bytes = parse_private_key(uint8_list)
assert isinstance(parsed_key, bytes)
assert len(parsed_key) == 32
# Ensure only the first 32 elements are taken
assert parsed_key == bytes(range(32))

# Invalid list (contains non-integers)
with pytest.raises(ValueError, match="Invalid uint8 array"):
parse_private_key([1, 2, "not an int", 4]) # type: ignore # Ignore type check for string
parse_private_key([1, 2, "not an int", 4]) # type: ignore

# Invalid list (less than 32 elements)
with pytest.raises(
Expand Down