keyFromPrivate($privateKeyHex); $sig = $key->sign($hash, ['canonical' => true]); // Encode r/s as canonical big-endian integers (minimal bytes, no leading // zero bytes). Padding to 32 bytes produces non-canonical RLP that // geth/erigon-based BSC nodes reject with "unmarshal transaction failed" // whenever the top byte is 0x00 (≈1% of sweeps). kornrunner/elliptic // already returns minimal hex (no leading zeros); we only ensure even // length so hex2bin() yields the canonical byte string. $r = $sig->r->toString(16); $s = $sig->s->toString(16); if (strlen($r) % 2 !== 0) { $r = '0'.$r; } if (strlen($s) % 2 !== 0) { $s = '0'.$s; } $recovery = (int) ($sig->recoveryParam ?? 0); $v = (string) ($recovery + 35 + $chainId * 2); $signed = EthRlp::encodeList([ EthRlp::intToBin($tx['nonce']), EthRlp::intToBin($tx['gasPrice']), EthRlp::intToBin($tx['gas']), hex2bin($to) ?: '', EthRlp::intToBin($tx['value']), $data === '' ? '' : (hex2bin($data) ?: ''), EthRlp::intToBin($v), hex2bin($r) ?: '', hex2bin($s) ?: '', ]); return '0x'.bin2hex($signed); } public static function publicAddress(string $privateKeyHex): string { $privateKeyHex = strtolower($privateKeyHex); if (str_starts_with($privateKeyHex, '0x')) { $privateKeyHex = substr($privateKeyHex, 2); } $ec = new EC('secp256k1'); $pub = $ec->keyFromPrivate($privateKeyHex)->getPublic(false, 'hex'); return EthAddress::fromUncompressedPublicKey($pub); } }