Skip to content

feat: expose the recover() function #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ crate-type = ["cdylib"]
[dependencies]
# `[patch.crates-io]` is not working :(
# This commit is where secp256k1-sys version changed to 0.4.1
secp256k1-sys = { version = "0.4.1", default-features = false, git = "https://github.com/rust-bitcoin/rust-secp256k1", rev = "455ee57ba4051bb2cfea5f5f675378170fb42c7f" }
secp256k1-sys = { version = "0.4.1", default-features = false, features=["recovery"], git = "https://github.com/rust-bitcoin/rust-secp256k1", rev = "455ee57ba4051bb2cfea5f5f675378170fb42c7f" }

[profile.release]
lto = true
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ Returns `null` if result is equal to `0`.
- `Expected Private` if `!isPrivate(d)`
- `Expected Tweak` if `tweak` is not in `[0...order - 1]`

### privateNegate (d)

```haskell
privateNegate :: Buffer -> Buffer
```

Returns the negation of d on the order n (`n - d`)

##### Throws:

- `Expected Private` if `!isPrivate(d)`

### xOnlyPointAddTweak (p, tweak)

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

### signRecoverable (h, d[, e])

```haskell
signRecoverable :: Buffer -> Buffer [-> Buffer] -> { recoveryId: 0 | 1 | 2 | 3; signature: Buffer; }
```

Returns normalized signatures and recovery Id, each of (r, s) values are guaranteed to less than `order / 2`.
Uses RFC6979.
Adds `e` as Added Entropy to the deterministic k generation.

##### Throws:

- `Expected Private` if `!isPrivate(d)`
- `Expected Scalar` if `h` is not 256-bit
- `Expected Extra Data (32 bytes)` if `e` is not 256-bit

### signSchnorr (h, d[, e])

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

### recover (h, signature, recoveryId[, compressed = false])

```haskell
verify :: Buffer -> Buffer -> Number [-> Bool] -> Maybe Buffer
```

Returns the ECDSA public key from a signature if it can be recovered, `null` otherwise.


##### Throws:

- `Expected Signature` if `signature` has any (r, s) values not in range `(0...order - 1]`
- `Bad Recovery Id` if `recid & 2 !== 0` and `signature` has any r value not in range `(0...P - N - 1]`
- `Expected Hash` if `h` is not 256-bit


### verifySchnorr (h, Q, signature)

```haskell
Expand Down
12 changes: 12 additions & 0 deletions benches/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,24 @@ const benchmarks = [
secp256k1.sign(f.m, f.d)
),
},
{
name: "signRecoverable",
bench: createBenchmarkFn(fecdsa, (secp256k1, f) =>
secp256k1.signRecoverable(f.m, f.d)
),
},
{
name: "verify",
bench: createBenchmarkFn(fecdsa, (secp256k1, f) =>
secp256k1.verify(f.m, f.Q, f.signature)
),
},
{
name: "recover",
bench: createBenchmarkFn(fecdsa, (secp256k1, f) =>
secp256k1.recover(f.m, f.signature, f.recoveryId)
),
},
{
name: "signSchnorr",
bench: createBenchmarkFn(fschnorrSign, (secp256k1, f) =>
Expand Down
136 changes: 136 additions & 0 deletions examples/react-app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,15 @@ const App = withStyles(useStyles)(
signature={this.state.data?.signature}
/>
</Box>
<Box className={this.props.classes.methodBox}>
<ApiRecover
classes={this.props.classes}
hash={this.state.data?.hash}
signature={this.state.data?.signature}
recoveryId={this.state.data?.recoveryId}
compressed={this.state.data?.compressed}
/>
</Box>
<Box className={this.props.classes.methodBox}>
<ApiSignSchnorr
classes={this.props.classes}
Expand Down Expand Up @@ -1593,6 +1602,133 @@ const ApiVerify = withStyles(useStyles)(
}
);

const ApiRecover = withStyles(useStyles)(
class extends Component {
constructor(props) {
super(props);
this.state = {
hash: "",
hash_valid: undefined,
signature: "",
signature_valid: undefined,
recoveryId: 0,
recoveryId_valid: undefined,
compressed: false,
result: undefined,
};
}

componentDidUpdate(prevProps, prevState) {
if (
prevProps.hash !== this.props.hash ||
prevProps.signature !== this.props.signature ||
prevProps.recoveryId !== this.props.recoveryId ||
prevProps.compressed !== this.props.compressed
) {
this.setState({
hash: this.props.hash,
signature: this.props.signature,
recoveryId: this.props.recoveryId,
compressed: this.props.compressed,
});
}

if (
prevState.hash !== this.state.hash ||
prevState.signature !== this.state.signature ||
prevState.recoveryId !== this.state.recoveryId ||
prevState.compressed !== this.state.compressed
) {
const { hash, signature, recoveryId, compressed } = this.state;
const hash_valid = hash === "" ? undefined : validate.isHash(hash);
const recoveryId_valid =
recoveryId === "" ? undefined : 0 <= +recoveryId <= 3;
const signature_valid =
signature === "" ? undefined : validate.isSignature(signature);
const result =
hash === "" && recoveryId === "" && signature === ""
? undefined
: secp256k1.recover(hash, signature, recoveryId, compressed);
this.setState({
hash_valid,
signature_valid,
recoveryId_valid,
result,
});
}
}

render() {
return (
<>
<Typography variant="h6">
recover(h: Uint8Array, signature: Uint8Array, recoveryId: number,
compressed?: boolean) =&gt; Uint8Array | null
</Typography>
<TextField
label="Hash as HEX string"
onChange={createInputChange(this, "hash")}
value={this.state.hash}
fullWidth
margin="normal"
variant="outlined"
InputProps={getInputProps(
this.state.hash_valid,
this.props.classes
)}
/>
<TextField
label="Signature as HEX string"
onChange={createInputChange(this, "signature")}
value={this.state.signature}
fullWidth
margin="normal"
variant="outlined"
InputProps={getInputProps(
this.state.signature_valid,
this.props.classes
)}
/>
<TextField
label="Recovery Id (0, 1, 2 or 3)"
type="number"
onChange={createInputChange(this, "recoveryId")}
value={this.state.recoveryId}
fullWidth
margin="normal"
variant="outlined"
InputProps={getInputProps(
this.state.recoveryId_valid,
this.props.classes
)}
/>
<FormControlLabel
control={
<CompressedCheckbox
onChange={createCheckedChange(this, "compressed")}
checked={this.state.compressed}
/>
}
label="Compressed"
/>
<TextField
label="Output, Public Key as HEX string"
value={
this.state.result === undefined
? ""
: this.state.result || "Invalid result"
}
fullWidth
margin="normal"
variant="outlined"
InputProps={getInputProps(this.state.result, this.props.classes)}
/>
</>
);
}
}
);

const ApiSignSchnorr = withStyles(useStyles)(
class extends Component {
constructor(props) {
Expand Down
89 changes: 84 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ use secp256k1_sys::{
SECP256K1_START_VERIFY,
};

use secp256k1_sys::recovery::{
secp256k1_ecdsa_recover, secp256k1_ecdsa_recoverable_signature_parse_compact,
secp256k1_ecdsa_recoverable_signature_serialize_compact, secp256k1_ecdsa_sign_recoverable,
RecoverableSignature,
};

#[link(wasm_import_module = "./validate_error.js")]
extern "C" {
#[link_name = "throwError"]
Expand Down Expand Up @@ -102,7 +108,7 @@ fn initialize_context_seed() {
unsafe {
for offset in (0..8).map(|v| v * 4) {
let value = generate_int32();
let bytes: [u8; 4] = core::mem::transmute(value);
let bytes: [u8; 4] = value.to_ne_bytes();
CONTEXT_SEED[offset..offset + 4].copy_from_slice(&bytes);
}
}
Expand Down Expand Up @@ -412,16 +418,28 @@ pub extern "C" fn private_sub() -> i32 {
}
}

#[allow(clippy::missing_panics_doc)]
#[no_mangle]
#[export_name = "privateNegate"]
pub extern "C" fn private_negate() {
unsafe {
assert_eq!(
secp256k1_ec_seckey_negate(secp256k1_context_no_precomp, PRIVATE_INPUT.as_mut_ptr()),
1
);
}
}

#[allow(clippy::missing_panics_doc)]
#[no_mangle]
pub extern "C" fn sign(extra_data: i32) {
unsafe {
let mut sig = Signature::new();
let noncedata = (if extra_data == 0 {
let noncedata = if extra_data == 0 {
core::ptr::null()
} else {
EXTRA_DATA_INPUT.as_ptr()
})
}
.cast::<c_void>();

assert_eq!(
Expand All @@ -447,17 +465,53 @@ pub extern "C" fn sign(extra_data: i32) {
}
}

#[allow(clippy::missing_panics_doc)]
#[no_mangle]
#[export_name = "signRecoverable"]
pub extern "C" fn sign_recoverable(extra_data: i32) -> i32 {
unsafe {
let mut sig = RecoverableSignature::new();
let noncedata = if extra_data == 0 {
core::ptr::null()
} else {
EXTRA_DATA_INPUT.as_ptr()
}
.cast::<c_void>();

assert_eq!(
secp256k1_ecdsa_sign_recoverable(
get_context(),
&mut sig,
HASH_INPUT.as_ptr(),
PRIVATE_INPUT.as_ptr(),
secp256k1_nonce_function_rfc6979,
noncedata
),
1
);

let mut recid: i32 = 0;
secp256k1_ecdsa_recoverable_signature_serialize_compact(
secp256k1_context_no_precomp,
SIGNATURE_INPUT.as_mut_ptr(),
&mut recid,
&sig,
);
recid
}
}

#[allow(clippy::missing_panics_doc)]
#[no_mangle]
#[export_name = "signSchnorr"]
pub extern "C" fn sign_schnorr(extra_data: i32) {
unsafe {
let mut keypair = KeyPair::new();
let noncedata = (if extra_data == 0 {
let noncedata = if extra_data == 0 {
core::ptr::null()
} else {
EXTRA_DATA_INPUT.as_ptr()
})
}
.cast::<c_void>();

assert_eq!(
Expand Down Expand Up @@ -511,6 +565,31 @@ pub extern "C" fn verify(inputlen: usize, strict: i32) -> i32 {
}
}

#[no_mangle]
pub extern "C" fn recover(outputlen: usize, recid: i32) -> i32 {
unsafe {
let mut signature = RecoverableSignature::new();
if secp256k1_ecdsa_recoverable_signature_parse_compact(
secp256k1_context_no_precomp,
&mut signature,
SIGNATURE_INPUT.as_ptr(),
recid,
) == 0
{
throw_error(ERROR_BAD_SIGNATURE);
return 0;
}

let mut pk = PublicKey::new();
if secp256k1_ecdsa_recover(get_context(), &mut pk, &signature, HASH_INPUT.as_ptr()) == 1 {
pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen);
1
} else {
0
}
}
}

#[no_mangle]
#[export_name = "verifySchnorr"]
pub extern "C" fn verify_schnorr() -> i32 {
Expand Down
Loading