Skip to main content

tronz_primitives/
signature.rs

1//! Recoverable secp256k1 signature in TRON's `r || s || v` wire form.
2
3use core::{fmt, str::FromStr};
4
5use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
6
7use crate::{Address, B256, error::SignatureError};
8
9/// Length of the serialized signature: `r(32) || s(32) || v(1)`.
10pub const SIGNATURE_LEN: usize = 65;
11
12/// A secp256k1 ECDSA signature plus the recovery id needed to recover the
13/// signing public key.
14///
15/// Serializes to the 65-byte TRON wire format `r || s || v`, where `v` is the
16/// recovery id (`0` or `1`).
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
18pub struct RecoverableSignature {
19    r: [u8; 32],
20    s: [u8; 32],
21    v: u8,
22}
23
24impl RecoverableSignature {
25    /// Build from a non-recoverable [`Signature`] and its [`RecoveryId`].
26    pub fn from_signature(sig: &Signature, recovery_id: RecoveryId) -> Self {
27        let bytes = sig.to_bytes();
28        let mut r = [0u8; 32];
29        let mut s = [0u8; 32];
30        r.copy_from_slice(&bytes[..32]);
31        s.copy_from_slice(&bytes[32..]);
32        Self { r, s, v: recovery_id.to_byte() }
33    }
34
35    /// Parse the 65-byte `r || s || v` representation.
36    ///
37    /// Accepts a `v` of `0`/`1` or the Ethereum-style `27`/`28`, normalising to
38    /// `0`/`1`.
39    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignatureError> {
40        if bytes.len() != SIGNATURE_LEN {
41            return Err(SignatureError::BadLength(bytes.len()));
42        }
43        let v = match bytes[64] {
44            v @ (0 | 1) => v,
45            27 => 0,
46            28 => 1,
47            other => return Err(SignatureError::BadRecoveryId(other)),
48        };
49        let mut r = [0u8; 32];
50        let mut s = [0u8; 32];
51        r.copy_from_slice(&bytes[..32]);
52        s.copy_from_slice(&bytes[32..64]);
53        Ok(Self { r, s, v })
54    }
55
56    /// The 32-byte `r` scalar.
57    pub fn r(&self) -> &[u8; 32] {
58        &self.r
59    }
60
61    /// The 32-byte `s` scalar.
62    pub fn s(&self) -> &[u8; 32] {
63        &self.s
64    }
65
66    /// The recovery id (`0` or `1`).
67    pub fn v(&self) -> u8 {
68        self.v
69    }
70
71    /// Serialize to the 65-byte `r || s || v` wire form (`v` = `0`/`1`).
72    ///
73    /// TRON's native transaction encoding. For TronWeb/TronLink message
74    /// signatures (`v` = `27`/`28`) use [`to_legacy_bytes`](Self::to_legacy_bytes).
75    pub fn to_bytes(&self) -> [u8; SIGNATURE_LEN] {
76        let mut out = [0u8; SIGNATURE_LEN];
77        out[..32].copy_from_slice(&self.r);
78        out[32..64].copy_from_slice(&self.s);
79        out[64] = self.v;
80        out
81    }
82
83    /// Serialize to `r || s || v` using the legacy `27`/`28` recovery id.
84    ///
85    /// Matches TronWeb `signMessageV2` / TronLink output (ends in `1b`/`1c`).
86    /// The internal `v` stays `0`/`1`; only this encoding adds the `+27` offset.
87    pub fn to_legacy_bytes(&self) -> [u8; SIGNATURE_LEN] {
88        let mut out = self.to_bytes();
89        out[64] += 27;
90        out
91    }
92
93    /// Recover the TRON address that produced this signature over `prehash`.
94    pub fn recover_address_from_prehash(&self, prehash: B256) -> Result<Address, SignatureError> {
95        let (sig, recid) = self.split()?;
96        let vk = VerifyingKey::recover_from_prehash(prehash.as_slice(), &sig, recid)?;
97        Ok(Address::from_public_key(&vk))
98    }
99
100    /// Recover the non-recoverable [`Signature`] and [`RecoveryId`] components.
101    pub fn split(&self) -> Result<(Signature, RecoveryId), SignatureError> {
102        let mut rs = [0u8; 64];
103        rs[..32].copy_from_slice(&self.r);
104        rs[32..].copy_from_slice(&self.s);
105        let sig = Signature::from_slice(&rs)?;
106        let recid = RecoveryId::from_byte(self.v).ok_or(SignatureError::BadRecoveryId(self.v))?;
107        Ok((sig, recid))
108    }
109}
110
111impl TryFrom<&[u8]> for RecoverableSignature {
112    type Error = SignatureError;
113
114    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
115        Self::from_bytes(bytes)
116    }
117}
118
119impl From<RecoverableSignature> for [u8; SIGNATURE_LEN] {
120    fn from(signature: RecoverableSignature) -> Self {
121        signature.to_bytes()
122    }
123}
124
125impl From<&RecoverableSignature> for [u8; SIGNATURE_LEN] {
126    fn from(signature: &RecoverableSignature) -> Self {
127        signature.to_bytes()
128    }
129}
130
131impl FromStr for RecoverableSignature {
132    type Err = SignatureError;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        let bytes = hex::decode(s.strip_prefix("0x").unwrap_or(s))?;
136        Self::from_bytes(&bytes)
137    }
138}
139
140impl From<(Signature, RecoveryId)> for RecoverableSignature {
141    fn from((signature, recovery_id): (Signature, RecoveryId)) -> Self {
142        Self::from_signature(&signature, recovery_id)
143    }
144}
145
146impl TryFrom<RecoverableSignature> for (Signature, RecoveryId) {
147    type Error = SignatureError;
148
149    fn try_from(signature: RecoverableSignature) -> Result<Self, Self::Error> {
150        signature.split()
151    }
152}
153
154impl fmt::Debug for RecoverableSignature {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "RecoverableSignature(0x{})", hex::encode(self.to_bytes()))
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use k256::ecdsa::{SigningKey, signature::hazmat::PrehashSigner};
163
164    use super::*;
165
166    #[test]
167    fn bytes_roundtrip() {
168        let mut bytes = [7u8; SIGNATURE_LEN];
169        bytes[64] = 1;
170        let sig = RecoverableSignature::from_bytes(&bytes).unwrap();
171        assert_eq!(sig.to_bytes(), bytes);
172        assert_eq!(sig.v(), 1);
173    }
174
175    #[test]
176    fn normalises_eth_v() {
177        let mut bytes = [3u8; SIGNATURE_LEN];
178        bytes[64] = 28;
179        let sig = RecoverableSignature::from_bytes(&bytes).unwrap();
180        assert_eq!(sig.v(), 1);
181    }
182
183    #[test]
184    fn bad_length_and_recid() {
185        assert!(matches!(
186            RecoverableSignature::from_bytes(&[0u8; 10]),
187            Err(SignatureError::BadLength(10))
188        ));
189        let mut bytes = [0u8; SIGNATURE_LEN];
190        bytes[64] = 5;
191        assert!(matches!(
192            RecoverableSignature::from_bytes(&bytes),
193            Err(SignatureError::BadRecoveryId(5))
194        ));
195    }
196
197    #[test]
198    fn from_signature_and_split() {
199        let signing = SigningKey::from_bytes(&[1u8; 32].into()).unwrap();
200        let (sig, recid): (Signature, RecoveryId) = signing.sign_prehash(&[9u8; 32]).unwrap();
201        let rec = RecoverableSignature::from_signature(&sig, recid);
202        let (sig2, recid2) = rec.split().unwrap();
203        assert_eq!(sig, sig2);
204        assert_eq!(recid.to_byte(), recid2.to_byte());
205    }
206
207    #[test]
208    fn recover_address_round_trips() {
209        use k256::ecdsa::signature::hazmat::PrehashSigner;
210
211        let signing = SigningKey::from_bytes(&[1u8; 32].into()).unwrap();
212        let expected = crate::Address::from_public_key(signing.verifying_key());
213        let prehash = crate::B256::repeat_byte(0x42);
214        let (sig, recid): (Signature, RecoveryId) =
215            signing.sign_prehash(prehash.as_slice()).unwrap();
216        let rec = RecoverableSignature::from_signature(&sig, recid);
217        assert_eq!(rec.recover_address_from_prehash(prehash).unwrap(), expected);
218    }
219
220    #[test]
221    fn to_bytes_stays_0_1_and_legacy_is_27_28() {
222        // Guard: transaction signing relies on to_bytes() staying 0/1.
223        for v in [0u8, 1] {
224            let mut bytes = [3u8; SIGNATURE_LEN];
225            bytes[64] = v;
226            let sig = RecoverableSignature::from_bytes(&bytes).unwrap();
227            assert!(matches!(sig.to_bytes()[64], 0 | 1));
228            assert!(matches!(sig.to_legacy_bytes()[64], 27 | 28));
229            assert_eq!(sig.to_bytes()[..64], sig.to_legacy_bytes()[..64]);
230            assert_eq!(RecoverableSignature::from_bytes(&sig.to_legacy_bytes()).unwrap(), sig);
231        }
232    }
233
234    #[test]
235    fn standard_byte_and_string_conversions() {
236        let mut bytes = [3u8; SIGNATURE_LEN];
237        bytes[64] = 1;
238        let signature = RecoverableSignature::try_from(bytes.as_slice()).unwrap();
239
240        assert_eq!(<[u8; SIGNATURE_LEN]>::from(signature), bytes);
241        assert_eq!(<[u8; SIGNATURE_LEN]>::from(&signature), bytes);
242        assert_eq!(hex::encode(bytes).parse::<RecoverableSignature>().unwrap(), signature);
243        assert_eq!(
244            format!("0x{}", hex::encode(bytes)).parse::<RecoverableSignature>().unwrap(),
245            signature
246        );
247    }
248
249    #[test]
250    fn k256_tuple_conversions() {
251        let signing = SigningKey::from_bytes(&[1u8; 32].into()).unwrap();
252        let tuple: (Signature, RecoveryId) = signing.sign_prehash(&[9u8; 32]).unwrap();
253        let recoverable = RecoverableSignature::from(tuple);
254        let roundtrip: (Signature, RecoveryId) = recoverable.try_into().unwrap();
255
256        assert_eq!(roundtrip.0, tuple.0);
257        assert_eq!(roundtrip.1, tuple.1);
258    }
259}