Skip to content

Commit 3a9762b

Browse files
TEB1157claude
andcommitted
fix: use full 64-byte Ed25519 signatures in sign_transfer/parse_transfer
sign_transfer was truncating to 28 bytes (legacy format). Now appends the full 64-byte signature. parse_transfer accepts both 136-byte and 100-byte (legacy) formats. Bump to v0.2.2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 63001af commit 3a9762b

4 files changed

Lines changed: 25 additions & 23 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "zero-network"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
description = "Python SDK for the Zero Network — stablecoin microtransactions for AI agents"
55
readme = "README.md"
66
license = "MIT"

zero_network/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from .transaction import Transfer, build_transfer, parse_transfer, sign_transfer
4141
from .wallet import Wallet
4242

43-
__version__ = "0.2.1"
43+
__version__ = "0.2.2"
4444

4545
__all__ = [
4646
# Core

zero_network/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def send(
157157
to_hex: Hex-encoded recipient public key.
158158
amount_units: Amount in internal units.
159159
nonce: Sender nonce.
160-
signature_hex: Hex-encoded truncated signature (28 bytes).
160+
signature_hex: Hex-encoded Ed25519 signature (64 bytes).
161161
162162
Returns:
163163
API response dict.

zero_network/transaction.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""Transaction building and parsing for the Zero Network.
22
3-
Transaction format (100 bytes total):
4-
from_pubkey[32] + to_pubkey[32] + amount[4] + nonce[4] + signature[28]
3+
Transaction format (136 bytes total):
4+
from_pubkey[32] + to_pubkey[32] + amount[4] + nonce[4] + signature[64]
55
6-
The signature covers the first 72 bytes (from + to + amount + nonce) and is
7-
truncated to 28 bytes from the standard Ed25519 64-byte signature. This matches
8-
the wire format used by the Zero Network's Rust/tweetnacl implementations.
6+
The Ed25519 signature covers the first 72 bytes (from + to + amount + nonce).
7+
The full 64-byte signature is appended, matching the wire format required by
8+
the Zero Network validators.
99
"""
1010

1111
from __future__ import annotations
@@ -71,17 +71,17 @@ def build_transfer(
7171

7272

7373
def sign_transfer(unsigned_tx: bytes, signing_key: SigningKey) -> bytes:
74-
"""Sign an unsigned transaction and return the complete 100-byte tx.
74+
"""Sign an unsigned transaction and return the complete 136-byte tx.
7575
76-
The Ed25519 signature is computed over the 72-byte unsigned payload,
77-
then truncated to 28 bytes to fit the wire format.
76+
The Ed25519 signature is computed over the 72-byte unsigned payload.
77+
The full 64-byte signature is appended.
7878
7979
Args:
8080
unsigned_tx: 72-byte unsigned transaction from build_transfer().
8181
signing_key: Ed25519 signing key (nacl.signing.SigningKey).
8282
8383
Returns:
84-
100-byte signed transaction ready for submission.
84+
136-byte signed transaction ready for submission.
8585
8686
Raises:
8787
ValueError: If unsigned_tx is not 72 bytes.
@@ -90,33 +90,35 @@ def sign_transfer(unsigned_tx: bytes, signing_key: SigningKey) -> bytes:
9090
raise ValueError(f"unsigned_tx must be 72 bytes, got {len(unsigned_tx)}")
9191

9292
signed = signing_key.sign(unsigned_tx)
93-
# signed.signature is the 64-byte Ed25519 signature
94-
# Truncate to 28 bytes for the wire format
95-
sig_truncated = signed.signature[:28]
96-
97-
return unsigned_tx + sig_truncated
93+
# signed.signature is the full 64-byte Ed25519 signature
94+
return unsigned_tx + signed.signature
9895

9996

10097
def parse_transfer(tx_bytes: bytes) -> Transfer:
101-
"""Parse a 100-byte transaction into its components.
98+
"""Parse a signed transaction into its components.
99+
100+
Accepts 136-byte (full signature) or 100-byte (legacy) format.
102101
103102
Args:
104-
tx_bytes: 100-byte signed transaction.
103+
tx_bytes: Signed transaction bytes.
105104
106105
Returns:
107106
Transfer dataclass with parsed fields.
108107
109108
Raises:
110-
ValueError: If tx_bytes is not 100 bytes.
109+
ValueError: If tx_bytes is not 136 or 100 bytes.
111110
"""
112-
if len(tx_bytes) != TX_SIZE:
113-
raise ValueError(f"tx_bytes must be {TX_SIZE} bytes, got {len(tx_bytes)}")
111+
LEGACY_TX_SIZE = 100
112+
if len(tx_bytes) != TX_SIZE and len(tx_bytes) != LEGACY_TX_SIZE:
113+
raise ValueError(
114+
f"tx_bytes must be {TX_SIZE} or {LEGACY_TX_SIZE} bytes, got {len(tx_bytes)}"
115+
)
114116

115117
from_pubkey = tx_bytes[0:32]
116118
to_pubkey = tx_bytes[32:64]
117119
amount_units = struct.unpack("<I", tx_bytes[64:68])[0]
118120
nonce = struct.unpack("<I", tx_bytes[68:72])[0]
119-
signature = tx_bytes[72:100]
121+
signature = tx_bytes[72:]
120122

121123
return Transfer(
122124
from_pubkey=from_pubkey,

0 commit comments

Comments
 (0)