Skip to content

Commit 905acdf

Browse files
authored
Merge pull request #69 from motorina0/motorina0/expose-recover
feat: expose the recover() function
2 parents 45b0cc2 + f0fe02c commit 905acdf

File tree

13 files changed

+4843
-2029
lines changed

13 files changed

+4843
-2029
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
1414
[dependencies]
1515
# `[patch.crates-io]` is not working :(
1616
# This commit is where secp256k1-sys version changed to 0.4.1
17-
secp256k1-sys = { version = "0.4.1", default-features = false, git = "https://github.com/rust-bitcoin/rust-secp256k1", rev = "455ee57ba4051bb2cfea5f5f675378170fb42c7f" }
17+
secp256k1-sys = { version = "0.4.1", default-features = false, features=["recovery"], git = "https://github.com/rust-bitcoin/rust-secp256k1", rev = "455ee57ba4051bb2cfea5f5f675378170fb42c7f" }
1818

1919
[profile.release]
2020
lto = true

README.md

+44
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@ Returns `null` if result is equal to `0`.
212212
- `Expected Private` if `!isPrivate(d)`
213213
- `Expected Tweak` if `tweak` is not in `[0...order - 1]`
214214

215+
### privateNegate (d)
216+
217+
```haskell
218+
privateNegate :: Buffer -> Buffer
219+
```
220+
221+
Returns the negation of d on the order n (`n - d`)
222+
223+
##### Throws:
224+
225+
- `Expected Private` if `!isPrivate(d)`
226+
215227
### xOnlyPointAddTweak (p, tweak)
216228

217229
```haskell
@@ -258,6 +270,22 @@ Adds `e` as Added Entropy to the deterministic k generation.
258270
- `Expected Scalar` if `h` is not 256-bit
259271
- `Expected Extra Data (32 bytes)` if `e` is not 256-bit
260272

273+
### signRecoverable (h, d[, e])
274+
275+
```haskell
276+
signRecoverable :: Buffer -> Buffer [-> Buffer] -> { recoveryId: 0 | 1 | 2 | 3; signature: Buffer; }
277+
```
278+
279+
Returns normalized signatures and recovery Id, each of (r, s) values are guaranteed to less than `order / 2`.
280+
Uses RFC6979.
281+
Adds `e` as Added Entropy to the deterministic k generation.
282+
283+
##### Throws:
284+
285+
- `Expected Private` if `!isPrivate(d)`
286+
- `Expected Scalar` if `h` is not 256-bit
287+
- `Expected Extra Data (32 bytes)` if `e` is not 256-bit
288+
261289
### signSchnorr (h, d[, e])
262290

263291
```haskell
@@ -290,6 +318,22 @@ If `strict` is `true`, valid signatures with any of (r, s) values greater than `
290318
- `Expected Signature` if `signature` has any (r, s) values not in range `[0...order - 1]`
291319
- `Expected Scalar` if `h` is not 256-bit
292320

321+
### recover (h, signature, recoveryId[, compressed = false])
322+
323+
```haskell
324+
verify :: Buffer -> Buffer -> Number [-> Bool] -> Maybe Buffer
325+
```
326+
327+
Returns the ECDSA public key from a signature if it can be recovered, `null` otherwise.
328+
329+
330+
##### Throws:
331+
332+
- `Expected Signature` if `signature` has any (r, s) values not in range `(0...order - 1]`
333+
- `Bad Recovery Id` if `recid & 2 !== 0` and `signature` has any r value not in range `(0...P - N - 1]`
334+
- `Expected Hash` if `h` is not 256-bit
335+
336+
293337
### verifySchnorr (h, Q, signature)
294338

295339
```haskell

benches/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,24 @@ const benchmarks = [
110110
secp256k1.sign(f.m, f.d)
111111
),
112112
},
113+
{
114+
name: "signRecoverable",
115+
bench: createBenchmarkFn(fecdsa, (secp256k1, f) =>
116+
secp256k1.signRecoverable(f.m, f.d)
117+
),
118+
},
113119
{
114120
name: "verify",
115121
bench: createBenchmarkFn(fecdsa, (secp256k1, f) =>
116122
secp256k1.verify(f.m, f.Q, f.signature)
117123
),
118124
},
125+
{
126+
name: "recover",
127+
bench: createBenchmarkFn(fecdsa, (secp256k1, f) =>
128+
secp256k1.recover(f.m, f.signature, f.recoveryId)
129+
),
130+
},
119131
{
120132
name: "signSchnorr",
121133
bench: createBenchmarkFn(fschnorrSign, (secp256k1, f) =>

examples/react-app/index.js

+136
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,15 @@ const App = withStyles(useStyles)(
292292
signature={this.state.data?.signature}
293293
/>
294294
</Box>
295+
<Box className={this.props.classes.methodBox}>
296+
<ApiRecover
297+
classes={this.props.classes}
298+
hash={this.state.data?.hash}
299+
signature={this.state.data?.signature}
300+
recoveryId={this.state.data?.recoveryId}
301+
compressed={this.state.data?.compressed}
302+
/>
303+
</Box>
295304
<Box className={this.props.classes.methodBox}>
296305
<ApiSignSchnorr
297306
classes={this.props.classes}
@@ -1593,6 +1602,133 @@ const ApiVerify = withStyles(useStyles)(
15931602
}
15941603
);
15951604

1605+
const ApiRecover = withStyles(useStyles)(
1606+
class extends Component {
1607+
constructor(props) {
1608+
super(props);
1609+
this.state = {
1610+
hash: "",
1611+
hash_valid: undefined,
1612+
signature: "",
1613+
signature_valid: undefined,
1614+
recoveryId: 0,
1615+
recoveryId_valid: undefined,
1616+
compressed: false,
1617+
result: undefined,
1618+
};
1619+
}
1620+
1621+
componentDidUpdate(prevProps, prevState) {
1622+
if (
1623+
prevProps.hash !== this.props.hash ||
1624+
prevProps.signature !== this.props.signature ||
1625+
prevProps.recoveryId !== this.props.recoveryId ||
1626+
prevProps.compressed !== this.props.compressed
1627+
) {
1628+
this.setState({
1629+
hash: this.props.hash,
1630+
signature: this.props.signature,
1631+
recoveryId: this.props.recoveryId,
1632+
compressed: this.props.compressed,
1633+
});
1634+
}
1635+
1636+
if (
1637+
prevState.hash !== this.state.hash ||
1638+
prevState.signature !== this.state.signature ||
1639+
prevState.recoveryId !== this.state.recoveryId ||
1640+
prevState.compressed !== this.state.compressed
1641+
) {
1642+
const { hash, signature, recoveryId, compressed } = this.state;
1643+
const hash_valid = hash === "" ? undefined : validate.isHash(hash);
1644+
const recoveryId_valid =
1645+
recoveryId === "" ? undefined : 0 <= +recoveryId <= 3;
1646+
const signature_valid =
1647+
signature === "" ? undefined : validate.isSignature(signature);
1648+
const result =
1649+
hash === "" && recoveryId === "" && signature === ""
1650+
? undefined
1651+
: secp256k1.recover(hash, signature, recoveryId, compressed);
1652+
this.setState({
1653+
hash_valid,
1654+
signature_valid,
1655+
recoveryId_valid,
1656+
result,
1657+
});
1658+
}
1659+
}
1660+
1661+
render() {
1662+
return (
1663+
<>
1664+
<Typography variant="h6">
1665+
recover(h: Uint8Array, signature: Uint8Array, recoveryId: number,
1666+
compressed?: boolean) =&gt; Uint8Array | null
1667+
</Typography>
1668+
<TextField
1669+
label="Hash as HEX string"
1670+
onChange={createInputChange(this, "hash")}
1671+
value={this.state.hash}
1672+
fullWidth
1673+
margin="normal"
1674+
variant="outlined"
1675+
InputProps={getInputProps(
1676+
this.state.hash_valid,
1677+
this.props.classes
1678+
)}
1679+
/>
1680+
<TextField
1681+
label="Signature as HEX string"
1682+
onChange={createInputChange(this, "signature")}
1683+
value={this.state.signature}
1684+
fullWidth
1685+
margin="normal"
1686+
variant="outlined"
1687+
InputProps={getInputProps(
1688+
this.state.signature_valid,
1689+
this.props.classes
1690+
)}
1691+
/>
1692+
<TextField
1693+
label="Recovery Id (0, 1, 2 or 3)"
1694+
type="number"
1695+
onChange={createInputChange(this, "recoveryId")}
1696+
value={this.state.recoveryId}
1697+
fullWidth
1698+
margin="normal"
1699+
variant="outlined"
1700+
InputProps={getInputProps(
1701+
this.state.recoveryId_valid,
1702+
this.props.classes
1703+
)}
1704+
/>
1705+
<FormControlLabel
1706+
control={
1707+
<CompressedCheckbox
1708+
onChange={createCheckedChange(this, "compressed")}
1709+
checked={this.state.compressed}
1710+
/>
1711+
}
1712+
label="Compressed"
1713+
/>
1714+
<TextField
1715+
label="Output, Public Key as HEX string"
1716+
value={
1717+
this.state.result === undefined
1718+
? ""
1719+
: this.state.result || "Invalid result"
1720+
}
1721+
fullWidth
1722+
margin="normal"
1723+
variant="outlined"
1724+
InputProps={getInputProps(this.state.result, this.props.classes)}
1725+
/>
1726+
</>
1727+
);
1728+
}
1729+
}
1730+
);
1731+
15961732
const ApiSignSchnorr = withStyles(useStyles)(
15971733
class extends Component {
15981734
constructor(props) {

src/lib.rs

+84-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ use secp256k1_sys::{
2727
SECP256K1_START_VERIFY,
2828
};
2929

30+
use secp256k1_sys::recovery::{
31+
secp256k1_ecdsa_recover, secp256k1_ecdsa_recoverable_signature_parse_compact,
32+
secp256k1_ecdsa_recoverable_signature_serialize_compact, secp256k1_ecdsa_sign_recoverable,
33+
RecoverableSignature,
34+
};
35+
3036
#[link(wasm_import_module = "./validate_error.js")]
3137
extern "C" {
3238
#[link_name = "throwError"]
@@ -102,7 +108,7 @@ fn initialize_context_seed() {
102108
unsafe {
103109
for offset in (0..8).map(|v| v * 4) {
104110
let value = generate_int32();
105-
let bytes: [u8; 4] = core::mem::transmute(value);
111+
let bytes: [u8; 4] = value.to_ne_bytes();
106112
CONTEXT_SEED[offset..offset + 4].copy_from_slice(&bytes);
107113
}
108114
}
@@ -412,16 +418,28 @@ pub extern "C" fn private_sub() -> i32 {
412418
}
413419
}
414420

421+
#[allow(clippy::missing_panics_doc)]
422+
#[no_mangle]
423+
#[export_name = "privateNegate"]
424+
pub extern "C" fn private_negate() {
425+
unsafe {
426+
assert_eq!(
427+
secp256k1_ec_seckey_negate(secp256k1_context_no_precomp, PRIVATE_INPUT.as_mut_ptr()),
428+
1
429+
);
430+
}
431+
}
432+
415433
#[allow(clippy::missing_panics_doc)]
416434
#[no_mangle]
417435
pub extern "C" fn sign(extra_data: i32) {
418436
unsafe {
419437
let mut sig = Signature::new();
420-
let noncedata = (if extra_data == 0 {
438+
let noncedata = if extra_data == 0 {
421439
core::ptr::null()
422440
} else {
423441
EXTRA_DATA_INPUT.as_ptr()
424-
})
442+
}
425443
.cast::<c_void>();
426444

427445
assert_eq!(
@@ -447,17 +465,53 @@ pub extern "C" fn sign(extra_data: i32) {
447465
}
448466
}
449467

468+
#[allow(clippy::missing_panics_doc)]
469+
#[no_mangle]
470+
#[export_name = "signRecoverable"]
471+
pub extern "C" fn sign_recoverable(extra_data: i32) -> i32 {
472+
unsafe {
473+
let mut sig = RecoverableSignature::new();
474+
let noncedata = if extra_data == 0 {
475+
core::ptr::null()
476+
} else {
477+
EXTRA_DATA_INPUT.as_ptr()
478+
}
479+
.cast::<c_void>();
480+
481+
assert_eq!(
482+
secp256k1_ecdsa_sign_recoverable(
483+
get_context(),
484+
&mut sig,
485+
HASH_INPUT.as_ptr(),
486+
PRIVATE_INPUT.as_ptr(),
487+
secp256k1_nonce_function_rfc6979,
488+
noncedata
489+
),
490+
1
491+
);
492+
493+
let mut recid: i32 = 0;
494+
secp256k1_ecdsa_recoverable_signature_serialize_compact(
495+
secp256k1_context_no_precomp,
496+
SIGNATURE_INPUT.as_mut_ptr(),
497+
&mut recid,
498+
&sig,
499+
);
500+
recid
501+
}
502+
}
503+
450504
#[allow(clippy::missing_panics_doc)]
451505
#[no_mangle]
452506
#[export_name = "signSchnorr"]
453507
pub extern "C" fn sign_schnorr(extra_data: i32) {
454508
unsafe {
455509
let mut keypair = KeyPair::new();
456-
let noncedata = (if extra_data == 0 {
510+
let noncedata = if extra_data == 0 {
457511
core::ptr::null()
458512
} else {
459513
EXTRA_DATA_INPUT.as_ptr()
460-
})
514+
}
461515
.cast::<c_void>();
462516

463517
assert_eq!(
@@ -511,6 +565,31 @@ pub extern "C" fn verify(inputlen: usize, strict: i32) -> i32 {
511565
}
512566
}
513567

568+
#[no_mangle]
569+
pub extern "C" fn recover(outputlen: usize, recid: i32) -> i32 {
570+
unsafe {
571+
let mut signature = RecoverableSignature::new();
572+
if secp256k1_ecdsa_recoverable_signature_parse_compact(
573+
secp256k1_context_no_precomp,
574+
&mut signature,
575+
SIGNATURE_INPUT.as_ptr(),
576+
recid,
577+
) == 0
578+
{
579+
throw_error(ERROR_BAD_SIGNATURE);
580+
return 0;
581+
}
582+
583+
let mut pk = PublicKey::new();
584+
if secp256k1_ecdsa_recover(get_context(), &mut pk, &signature, HASH_INPUT.as_ptr()) == 1 {
585+
pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen);
586+
1
587+
} else {
588+
0
589+
}
590+
}
591+
}
592+
514593
#[no_mangle]
515594
#[export_name = "verifySchnorr"]
516595
pub extern "C" fn verify_schnorr() -> i32 {

0 commit comments

Comments
 (0)