Page MenuHomePhabricator

No OneTemporary

diff --git a/modules/ecash-lib-wasm/src/ecc.rs b/modules/ecash-lib-wasm/src/ecc.rs
index 3c9a39545..cffb85f05 100644
--- a/modules/ecash-lib-wasm/src/ecc.rs
+++ b/modules/ecash-lib-wasm/src/ecc.rs
@@ -1,101 +1,164 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//! Module for [`Ecc`] for signing secp256k1 signatures.
-use ecash_secp256k1::{All, Message, PublicKey, Secp256k1, SecretKey};
+use ecash_secp256k1::{All, Message, PublicKey, Scalar, Secp256k1, SecretKey};
use thiserror::Error;
use wasm_bindgen::prelude::*;
use crate::ecc::EccError::*;
/// ECC signatures with libsecp256k1.
#[derive(Debug)]
#[wasm_bindgen]
pub struct Ecc {
curve: Secp256k1<All>,
}
/// Errors indicating something went wrong with [`Ecc`].
#[derive(Error, Debug, PartialEq, Eq)]
pub enum EccError {
/// Invalid secret key size
#[error("Invalid secret key size, expected 32 bytes but got {0}")]
InvalidSeckeySize(usize),
/// Invalid secret key for signing
#[error("Secret key not valid for secp256k1")]
InvalidSeckey,
+ /// Invalid public key size
+ #[error("Invalid public key size, expected 33 bytes but got {0}")]
+ InvalidPubkeySize(usize),
+
+ /// Invalid pubkey key for signing
+ #[error("Pubkey key not valid for secp256k1")]
+ InvalidPubkey,
+
+ /// Invalid scalar size
+ #[error("Invalid scalar size, expected 32 bytes but got {0}")]
+ InvalidScalarSize(usize),
+
+ /// Invalid scalar in range
+ #[error("Scalar not valid for secp256k1")]
+ InvalidScalar,
+
/// Invalid message size
#[error("Invalid message size, expected 32 bytes but got {0}")]
InvalidMessageSize(usize),
}
impl From<EccError> for String {
fn from(ecc: EccError) -> Self {
ecc.to_string()
}
}
fn parse_secret_key(seckey: &[u8]) -> Result<SecretKey, String> {
Ok(SecretKey::from_byte_array(
&seckey
.try_into()
.map_err(|_| InvalidSeckeySize(seckey.len()))?,
)
.map_err(|_| InvalidSeckey)?)
}
+fn parse_public_key(pubkey: &[u8]) -> Result<PublicKey, String> {
+ Ok(PublicKey::from_byte_array_compressed(
+ pubkey
+ .try_into()
+ .map_err(|_| InvalidPubkeySize(pubkey.len()))?,
+ )
+ .map_err(|_| InvalidPubkey)?)
+}
+
+fn parse_scalar(scalar: &[u8]) -> Result<Scalar, String> {
+ Ok(Scalar::from_be_bytes(
+ scalar
+ .try_into()
+ .map_err(|_| InvalidScalarSize(scalar.len()))?,
+ )
+ .map_err(|_| InvalidScalar)?)
+}
+
fn parse_msg(msg: &[u8]) -> Result<Message, String> {
Ok(Message::from_digest(
msg.try_into().map_err(|_| InvalidMessageSize(msg.len()))?,
))
}
#[wasm_bindgen]
impl Ecc {
/// Create a new Ecc instance.
#[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Ecc {
curve: Secp256k1::default(),
}
}
/// Derive a public key from secret key.
#[wasm_bindgen(js_name = derivePubkey)]
pub fn derive_pubkey(&self, seckey: &[u8]) -> Result<Vec<u8>, String> {
let seckey = parse_secret_key(seckey)?;
let pubkey = PublicKey::from_secret_key(&self.curve, &seckey);
Ok(pubkey.serialize().to_vec())
}
/// Sign an ECDSA signature.
#[wasm_bindgen(js_name = ecdsaSign)]
pub fn ecdsa_sign(
&self,
seckey: &[u8],
msg: &[u8],
) -> Result<Vec<u8>, String> {
let seckey = parse_secret_key(seckey)?;
let msg = parse_msg(msg)?;
let sig = self.curve.sign_ecdsa(&msg, &seckey);
Ok(sig.serialize_der().to_vec())
}
/// Sign a Schnorr signature.
#[wasm_bindgen(js_name = schnorrSign)]
pub fn schnorr_sign(
&self,
seckey: &[u8],
msg: &[u8],
) -> Result<Vec<u8>, String> {
let seckey = parse_secret_key(seckey)?;
let msg = parse_msg(msg)?;
let sig = self.curve.sign_schnorrabc_no_aux_rand(&msg, &seckey);
Ok(sig.as_ref().to_vec())
}
+
+ /// Return whether the given secret key is valid, i.e. whether is of correct
+ /// length (32 bytes) and is on the curve.
+ #[wasm_bindgen(js_name = isValidSeckey)]
+ pub fn is_valid_seckey(&self, seckey: &[u8]) -> bool {
+ parse_secret_key(seckey).is_ok()
+ }
+
+ /// Add a scalar to a secret key.
+ #[wasm_bindgen(js_name = seckeyAdd)]
+ pub fn seckey_add(&self, a: &[u8], b: &[u8]) -> Result<Vec<u8>, String> {
+ let a = parse_secret_key(a)?;
+ let b = parse_scalar(b)?;
+ Ok(a.add_tweak(&b)
+ .map_err(|_| InvalidSeckey)?
+ .secret_bytes()
+ .to_vec())
+ }
+
+ /// Add a scalar to a public key (adding G*b).
+ #[wasm_bindgen(js_name = pubkeyAdd)]
+ pub fn pubkey_add(&self, a: &[u8], b: &[u8]) -> Result<Vec<u8>, String> {
+ let a = parse_public_key(a)?;
+ let b = parse_scalar(b)?;
+ Ok(a.add_exp_tweak(&self.curve, &b)
+ .map_err(|_| InvalidPubkey)?
+ .serialize()
+ .to_vec())
+ }
}
diff --git a/modules/ecash-lib/src/ecc.ts b/modules/ecash-lib/src/ecc.ts
index d533f3461..0ed94edea 100644
--- a/modules/ecash-lib/src/ecc.ts
+++ b/modules/ecash-lib/src/ecc.ts
@@ -1,37 +1,61 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
/** Interface to abstract over Elliptic Curve Cryptography */
export interface Ecc {
/** Derive a public key from secret key. */
derivePubkey(seckey: Uint8Array): Uint8Array;
/** Sign an ECDSA signature. msg needs to be a 32-byte hash */
ecdsaSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array;
/** Sign a Schnorr signature. msg needs to be a 32-byte hash */
schnorrSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array;
+
+ /**
+ * Return whether the given secret key is valid, i.e. whether is of correct
+ * length (32 bytes) and is on the curve.
+ */
+ isValidSeckey(seckey: Uint8Array): boolean;
+
+ /** Add a scalar to a secret key */
+ seckeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array;
+
+ /** Add a scalar to a public key (adding G*b) */
+ pubkeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array;
}
/** Ecc implementation using WebAssembly */
export let Ecc: { new (): Ecc };
/** Dummy Ecc impl that always returns 0, useful for measuring tx size */
export class EccDummy implements Ecc {
derivePubkey(_seckey: Uint8Array): Uint8Array {
return new Uint8Array(33);
}
ecdsaSign(_seckey: Uint8Array, _msg: Uint8Array): Uint8Array {
return new Uint8Array(73);
}
schnorrSign(_seckey: Uint8Array, _msg: Uint8Array): Uint8Array {
return new Uint8Array(64);
}
+
+ isValidSeckey(_seckey: Uint8Array): boolean {
+ return false;
+ }
+
+ seckeyAdd(_a: Uint8Array, _b: Uint8Array): Uint8Array {
+ return new Uint8Array(32);
+ }
+
+ pubkeyAdd(_a: Uint8Array, _b: Uint8Array): Uint8Array {
+ return new Uint8Array(32);
+ }
}
export function __setEcc(ecc: { new (): Ecc }) {
Ecc = ecc;
}
diff --git a/modules/ecash-lib/src/hdwallet.test.ts b/modules/ecash-lib/src/hdwallet.test.ts
new file mode 100644
index 000000000..f436f6b13
--- /dev/null
+++ b/modules/ecash-lib/src/hdwallet.test.ts
@@ -0,0 +1,236 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { expect } from 'chai';
+
+import { fromHex, toHex } from './io/hex.js';
+import { initWasm } from './initNodeJs.js';
+import { HdNode } from './hdwallet.js';
+import { Ecc } from './ecc.js';
+
+// Tests are based on https://github.com/bitcoinjs/bip32/blob/master/test/fixtures/index.json
+
+describe('hdwallet', async () => {
+ await initWasm();
+ const ecc = new Ecc();
+
+ it('hdwallet 000102030405060708090a0b0c0d0e0f', () => {
+ const seed = fromHex('000102030405060708090a0b0c0d0e0f');
+ const master = HdNode.fromSeed(ecc, seed);
+ expect(toHex(master.seckey()!)).to.equal(
+ 'e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35',
+ );
+ expect(toHex(master.pubkey())).to.equal(
+ '0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2',
+ );
+ expect(toHex(master.pkh())).to.equal(
+ '3442193e1bb70916e914552172cd4e2dbc9df811',
+ );
+ expect(toHex(master.fingerprint())).to.equal('3442193e');
+ expect(toHex(master.chainCode())).to.equal(
+ '873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508',
+ );
+ expect(master.index()).to.equal(0);
+ expect(master.depth()).to.equal(0);
+
+ const child0 = master.deriveHardened(0);
+ expect(toHex(child0.seckey()!)).to.equal(
+ 'edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea',
+ );
+ expect(toHex(child0.pubkey())).to.equal(
+ '035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56',
+ );
+ expect(toHex(child0.pkh())).to.equal(
+ '5c1bd648ed23aa5fd50ba52b2457c11e9e80a6a7',
+ );
+ expect(toHex(child0.fingerprint())).to.equal('5c1bd648');
+ expect(toHex(child0.chainCode())).to.equal(
+ '47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141',
+ );
+ expect(child0.index()).to.equal(0x80000000);
+ expect(child0.depth()).to.equal(1);
+
+ const child01 = child0.derive(1);
+ expect(toHex(child01.seckey()!)).to.equal(
+ '3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368',
+ );
+ expect(toHex(child01.pubkey())).to.equal(
+ '03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c',
+ );
+ expect(toHex(child01.pkh())).to.equal(
+ 'bef5a2f9a56a94aab12459f72ad9cf8cf19c7bbe',
+ );
+ expect(toHex(child01.fingerprint())).to.equal('bef5a2f9');
+ expect(toHex(child01.chainCode())).to.equal(
+ '2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19',
+ );
+ expect(child01.index()).to.equal(1);
+ expect(child01.depth()).to.equal(2);
+
+ expect(toHex(master.derivePath("m/0'").seckey()!)).to.equal(
+ toHex(child0.seckey()!),
+ );
+ expect(toHex(master.derivePath("m/0'/1").seckey()!)).to.equal(
+ toHex(child01.seckey()!),
+ );
+
+ const child012 = master.derivePath("m/0'/1/2'");
+ expect(toHex(child012.seckey()!)).to.equal(
+ 'cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca',
+ );
+ expect(toHex(child012.pubkey())).to.equal(
+ '0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2',
+ );
+ expect(toHex(child012.pkh())).to.equal(
+ 'ee7ab90cde56a8c0e2bb086ac49748b8db9dce72',
+ );
+ expect(toHex(child012.fingerprint())).to.equal('ee7ab90c');
+ expect(toHex(child012.chainCode())).to.equal(
+ '04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f',
+ );
+ expect(child012.index()).to.equal(0x80000002);
+ expect(child012.depth()).to.equal(3);
+
+ const child0122 = master.derivePath("m/0'/1/2'/2");
+ expect(toHex(child0122.seckey()!)).to.equal(
+ '0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4',
+ );
+ expect(toHex(child0122.pubkey())).to.equal(
+ '02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29',
+ );
+ expect(toHex(child0122.pkh())).to.equal(
+ 'd880d7d893848509a62d8fb74e32148dac68412f',
+ );
+ expect(toHex(child0122.fingerprint())).to.equal('d880d7d8');
+ expect(toHex(child0122.chainCode())).to.equal(
+ 'cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd',
+ );
+ expect(child0122.index()).to.equal(2);
+ expect(child0122.depth()).to.equal(4);
+
+ const child = master.derivePath("m/0'/1/2'/2/1000000000");
+ expect(toHex(child.seckey()!)).to.equal(
+ '471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8',
+ );
+ expect(toHex(child.pubkey())).to.equal(
+ '022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011',
+ );
+ expect(toHex(child.pkh())).to.equal(
+ 'd69aa102255fed74378278c7812701ea641fdf32',
+ );
+ expect(toHex(child.fingerprint())).to.equal('d69aa102');
+ expect(toHex(child.chainCode())).to.equal(
+ 'c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e',
+ );
+ expect(child.index()).to.equal(1000000000);
+ expect(child.depth()).to.equal(5);
+ });
+
+ it('hdwallet fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9...', () => {
+ const seed = fromHex(
+ 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542',
+ );
+ const master = HdNode.fromSeed(ecc, seed);
+ expect(toHex(master.seckey()!)).to.equal(
+ '4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e',
+ );
+ expect(toHex(master.pubkey())).to.equal(
+ '03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7',
+ );
+ expect(toHex(master.pkh())).to.equal(
+ 'bd16bee53961a47d6ad888e29545434a89bdfe95',
+ );
+ expect(toHex(master.fingerprint())).to.equal('bd16bee5');
+ expect(toHex(master.chainCode())).to.equal(
+ '60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689',
+ );
+ expect(master.index()).to.equal(0);
+ expect(master.depth()).to.equal(0);
+
+ const child0 = master.derivePath('m/0');
+ expect(toHex(child0.seckey()!)).to.equal(
+ 'abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e',
+ );
+ expect(toHex(child0.pubkey())).to.equal(
+ '02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea',
+ );
+ expect(toHex(child0.pkh())).to.equal(
+ '5a61ff8eb7aaca3010db97ebda76121610b78096',
+ );
+ expect(toHex(child0.fingerprint())).to.equal('5a61ff8e');
+ expect(toHex(child0.chainCode())).to.equal(
+ 'f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c',
+ );
+ expect(child0.index()).to.equal(0);
+ expect(child0.depth()).to.equal(1);
+
+ const child1 = master.derivePath("m/0/2147483647'");
+ expect(toHex(child1.seckey()!)).to.equal(
+ '877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93',
+ );
+ expect(toHex(child1.pubkey())).to.equal(
+ '03c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b',
+ );
+ expect(toHex(child1.pkh())).to.equal(
+ 'd8ab493736da02f11ed682f88339e720fb0379d1',
+ );
+ expect(toHex(child1.fingerprint())).to.equal('d8ab4937');
+ expect(toHex(child1.chainCode())).to.equal(
+ 'be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9',
+ );
+ expect(child1.index()).to.equal(0xffffffff);
+ expect(child1.depth()).to.equal(2);
+
+ const child2 = master.derivePath("m/0/2147483647'/1");
+ expect(toHex(child2.seckey()!)).to.equal(
+ '704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7',
+ );
+ expect(toHex(child2.pubkey())).to.equal(
+ '03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9',
+ );
+ expect(toHex(child2.pkh())).to.equal(
+ '78412e3a2296a40de124307b6485bd19833e2e34',
+ );
+ expect(toHex(child2.fingerprint())).to.equal('78412e3a');
+ expect(toHex(child2.chainCode())).to.equal(
+ 'f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb',
+ );
+ expect(child2.index()).to.equal(1);
+ expect(child2.depth()).to.equal(3);
+
+ const child3 = master.derivePath("m/0/2147483647'/1/2147483646'");
+ expect(toHex(child3.seckey()!)).to.equal(
+ 'f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d',
+ );
+ expect(toHex(child3.pubkey())).to.equal(
+ '02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0',
+ );
+ expect(toHex(child3.pkh())).to.equal(
+ '31a507b815593dfc51ffc7245ae7e5aee304246e',
+ );
+ expect(toHex(child3.fingerprint())).to.equal('31a507b8');
+ expect(toHex(child3.chainCode())).to.equal(
+ '637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29',
+ );
+ expect(child3.index()).to.equal(0xfffffffe);
+ expect(child3.depth()).to.equal(4);
+
+ const child4 = master.derivePath("m/0/2147483647'/1/2147483646'/2");
+ expect(toHex(child4.seckey()!)).to.equal(
+ 'bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23',
+ );
+ expect(toHex(child4.pubkey())).to.equal(
+ '024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c',
+ );
+ expect(toHex(child4.pkh())).to.equal(
+ '26132fdbe7bf89cbc64cf8dafa3f9f88b8666220',
+ );
+ expect(toHex(child4.fingerprint())).to.equal('26132fdb');
+ expect(toHex(child4.chainCode())).to.equal(
+ '9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271',
+ );
+ expect(child4.index()).to.equal(2);
+ expect(child4.depth()).to.equal(5);
+ });
+});
diff --git a/modules/ecash-lib/src/hdwallet.ts b/modules/ecash-lib/src/hdwallet.ts
new file mode 100644
index 000000000..0f9005b29
--- /dev/null
+++ b/modules/ecash-lib/src/hdwallet.ts
@@ -0,0 +1,181 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { Ecc } from './ecc.js';
+import { hmacSha512 } from './hmac.js';
+import { shaRmd160 } from './hash.js';
+import { Bytes } from './io/bytes.js';
+import { strToBytes } from './io/str.js';
+import { WriterBytes } from './io/writerbytes.js';
+
+const HIGHEST_BIT = 0x80000000;
+
+export class HdNode {
+ private _ecc: Ecc;
+ private _seckey: Uint8Array | undefined;
+ private _pubkey: Uint8Array;
+ private _chainCode: Uint8Array;
+ private _depth: number;
+ private _index: number;
+ private _parentFingerprint: number;
+
+ public constructor(params: {
+ ecc: Ecc;
+ seckey: Uint8Array | undefined;
+ pubkey: Uint8Array;
+ chainCode: Uint8Array;
+ depth: number;
+ index: number;
+ parentFingerprint: number;
+ }) {
+ this._ecc = params.ecc;
+ this._seckey = params.seckey;
+ this._pubkey = params.pubkey;
+ this._chainCode = params.chainCode;
+ this._depth = params.depth;
+ this._index = params.index;
+ this._parentFingerprint = params.parentFingerprint;
+ }
+
+ public seckey(): Uint8Array | undefined {
+ return this._seckey;
+ }
+
+ public pubkey(): Uint8Array {
+ return this._pubkey;
+ }
+
+ public pkh(): Uint8Array {
+ return shaRmd160(this._pubkey);
+ }
+
+ public fingerprint(): Uint8Array {
+ return this.pkh().slice(0, 4);
+ }
+
+ public index(): number {
+ return this._index;
+ }
+
+ public depth(): number {
+ return this._depth;
+ }
+
+ public parentFingerprint(): number {
+ return this._parentFingerprint;
+ }
+
+ public chainCode(): Uint8Array {
+ return this._chainCode;
+ }
+
+ public derive(index: number): HdNode {
+ const isHardened = index >= HIGHEST_BIT;
+ const data = new WriterBytes(1 + 32 + 4);
+ if (isHardened) {
+ if (this._seckey === undefined) {
+ throw new Error('Missing private key for hardened child key');
+ }
+ data.putU8(0);
+ data.putBytes(this._seckey);
+ } else {
+ data.putBytes(this._pubkey);
+ }
+ data.putU32(index, 'BE');
+ const hashed = hmacSha512(this._chainCode, data.data);
+ const hashedLeft = hashed.slice(0, 32);
+ const hashedRight = hashed.slice(32);
+
+ // In case the secret key doesn't lie on the curve, we proceed with the
+ // next index. This is astronomically unlikely but part of the specification.
+ if (!this._ecc.isValidSeckey(hashedLeft)) {
+ return this.derive(index + 1);
+ }
+
+ let seckey: Uint8Array | undefined;
+ let pubkey: Uint8Array;
+ if (this._seckey !== undefined) {
+ try {
+ seckey = this._ecc.seckeyAdd(this._seckey, hashedLeft);
+ } catch (ex) {
+ console.log('Skipping index', index, ':', ex);
+ return this.derive(index + 1);
+ }
+ pubkey = this._ecc.derivePubkey(seckey);
+ } else {
+ try {
+ pubkey = this._ecc.pubkeyAdd(this._pubkey, hashedLeft);
+ } catch (ex) {
+ console.log('Skipping index', index, ':', ex);
+ return this.derive(index + 1);
+ }
+ seckey = undefined;
+ }
+ return new HdNode({
+ ecc: this._ecc,
+ seckey: seckey,
+ pubkey: pubkey,
+ chainCode: hashedRight,
+ depth: this._depth + 1,
+ index,
+ parentFingerprint: new Bytes(this.fingerprint()).readU32('BE'),
+ });
+ }
+
+ public deriveHardened(index: number): HdNode {
+ if (index < 0 || index >= HIGHEST_BIT) {
+ throw new TypeError(
+ `index must be between 0 and ${HIGHEST_BIT}, got ${index}`,
+ );
+ }
+ return this.derive(index + HIGHEST_BIT);
+ }
+
+ public derivePath(path: string): HdNode {
+ let splitPath = path.split('/');
+ if (splitPath[0] === 'm') {
+ if (this._parentFingerprint) {
+ throw new TypeError('Expected master, got child');
+ }
+ splitPath = splitPath.slice(1);
+ }
+
+ let hd: HdNode = this;
+ for (const step of splitPath) {
+ if (step.slice(-1) === `'`) {
+ hd = hd.deriveHardened(parseInt(step.slice(0, -1), 10));
+ } else {
+ hd = hd.derive(parseInt(step, 10));
+ }
+ }
+
+ return hd;
+ }
+
+ public static fromPrivateKey(
+ ecc: Ecc,
+ seckey: Uint8Array,
+ chainCode: Uint8Array,
+ ): HdNode {
+ return new HdNode({
+ ecc,
+ seckey: seckey,
+ pubkey: ecc.derivePubkey(seckey),
+ chainCode,
+ depth: 0,
+ index: 0,
+ parentFingerprint: 0,
+ });
+ }
+
+ public static fromSeed(ecc: Ecc, seed: Uint8Array): HdNode {
+ if (seed.length < 16 || seed.length > 64) {
+ throw new TypeError('Seed must be between 16 and 64 bytes long');
+ }
+ const hashed = hmacSha512(strToBytes('Bitcoin seed'), seed);
+ const hashedLeft = hashed.slice(0, 32);
+ const hashedRight = hashed.slice(32);
+ return HdNode.fromPrivateKey(ecc, hashedLeft, hashedRight);
+ }
+}
diff --git a/modules/ecash-lib/src/io/bytes.ts b/modules/ecash-lib/src/io/bytes.ts
index 3b40fd3b1..c395e9cfa 100644
--- a/modules/ecash-lib/src/io/bytes.ts
+++ b/modules/ecash-lib/src/io/bytes.ts
@@ -1,70 +1,80 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+export type Endian = 'LE' | 'BE';
+
+export function endianToBool(endian?: Endian): boolean {
+ if (!endian) {
+ // By default, little endian
+ return true;
+ }
+ return endian === 'LE';
+}
+
/** Reads ints/bytes from a Uint8Array. All integers are little-endian. */
export class Bytes {
public data: Uint8Array;
public view: DataView;
public idx: number;
/** Create a new Bytes that reads from the given data */
public constructor(data: Uint8Array) {
this.data = data;
this.view = new DataView(
this.data.buffer,
this.data.byteOffset,
this.data.byteLength,
);
this.idx = 0;
}
/** Read a single byte */
public readU8(): number {
this.ensureSize(1);
const result = this.data[this.idx];
this.idx++;
return result;
}
/** Read 2-byte little-endian integer (uint16_t) */
- public readU16(): number {
+ public readU16(endian?: Endian): number {
this.ensureSize(2);
- const result = this.view.getUint16(this.idx, true);
+ const result = this.view.getUint16(this.idx, endianToBool(endian));
this.idx += 2;
return result;
}
/** Read 4-byte little-endian integer (uint32_t) */
- public readU32(): number {
+ public readU32(endian?: Endian): number {
this.ensureSize(4);
- const result = this.view.getUint32(this.idx, true);
+ const result = this.view.getUint32(this.idx, endianToBool(endian));
this.idx += 4;
return result;
}
/** Read 8-byte little-endian integer (uint64_t) */
- public readU64(): bigint {
+ public readU64(endian?: Endian): bigint {
this.ensureSize(8);
- const result = this.view.getBigUint64(this.idx, true);
+ const result = this.view.getBigUint64(this.idx, endianToBool(endian));
this.idx += 8;
return result;
}
/** Read the given number of bytes as array */
public readBytes(numBytes: number): Uint8Array {
this.ensureSize(numBytes);
const result = this.data.slice(this.idx, this.idx + numBytes);
this.idx += numBytes;
return result;
}
private ensureSize(extraBytes: number) {
if (this.data.length < this.idx + extraBytes) {
throw (
`Not enough bytes: Tried reading ${extraBytes} byte(s), but ` +
`there are only ${this.data.length - this.idx} byte(s) left`
);
}
}
}
diff --git a/modules/ecash-lib/src/io/writer.ts b/modules/ecash-lib/src/io/writer.ts
index 78b229c7c..af247ad59 100644
--- a/modules/ecash-lib/src/io/writer.ts
+++ b/modules/ecash-lib/src/io/writer.ts
@@ -1,19 +1,20 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+import { Endian } from './bytes.js';
import { Int } from './int.js';
/** Writer interface to abstract over writing Bitcoin objects */
export interface Writer {
/** Write a single byte */
putU8(value: Int): void;
/** Write a 2-byte little-endian integer (uint16_t) */
- putU16(value: Int): void;
+ putU16(value: Int, endian?: Endian): void;
/** Write a 4-byte little-endian integer (uint32_t) */
- putU32(value: Int): void;
+ putU32(value: Int, endian?: Endian): void;
/** Write an 8-byte little-endian integer (uint64_t) */
- putU64(value: Int): void;
+ putU64(value: Int, endian?: Endian): void;
/** Write the given bytes */
putBytes(bytes: Uint8Array): void;
}
diff --git a/modules/ecash-lib/src/io/writerbytes.test.ts b/modules/ecash-lib/src/io/writerbytes.test.ts
index 8af05eb76..5574d905d 100644
--- a/modules/ecash-lib/src/io/writerbytes.test.ts
+++ b/modules/ecash-lib/src/io/writerbytes.test.ts
@@ -1,64 +1,75 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
import { expect } from 'chai';
import { fromHex } from './hex.js';
import { WriterBytes } from './writerbytes.js';
const wrote = (size: number, fn: (writer: WriterBytes) => void) => {
const writer = new WriterBytes(size);
fn(writer);
return writer.data;
};
describe('WriterBytes', () => {
it('WriterBytes.put* one', () => {
expect(wrote(1, writer => writer.putU8(0xde))).to.deep.equal(
fromHex('de'),
);
expect(wrote(2, writer => writer.putU16(0xdead))).to.deep.equal(
fromHex('adde'),
);
expect(wrote(4, writer => writer.putU32(0xdeadbeef))).to.deep.equal(
fromHex('efbeadde'),
);
expect(
wrote(8, writer => writer.putU64(0xdeadbeef0badcafen)),
).to.deep.equal(fromHex('fecaad0befbeadde'));
expect(
wrote(3, writer => writer.putBytes(fromHex('abcdef'))),
).to.deep.equal(fromHex('abcdef'));
});
it('WriterBytes.put* multiple', () => {
const writer = new WriterBytes(17);
writer.putU8(0x44);
writer.putU16(0x0201);
writer.putU32(0x06050403);
writer.putU64(0x0e0d0c0b0a090807n);
writer.putBytes(fromHex('0f10'));
expect(writer.data).to.deep.equal(
fromHex('440102030405060708090a0b0c0d0e0f10'),
);
});
+ it('WriterBytes.put* multiple big-endian', () => {
+ const writer = new WriterBytes(17);
+ writer.putU8(0x44);
+ writer.putU16(0x0201, 'BE');
+ writer.putU32(0x06050403, 'BE');
+ writer.putU64(0x0e0d0c0b0a090807n, 'BE');
+ writer.putBytes(fromHex('0f10'));
+ expect(writer.data).to.deep.equal(
+ fromHex('440201060504030e0d0c0b0a0908070f10'),
+ );
+ });
it('WriterBytes.put* failure', () => {
expect(() => wrote(0, writer => writer.putU8(0))).to.throw(
'Not enough bytes: Tried writing 1 byte(s), but only 0 byte(s) have been pre-allocated',
);
expect(() => wrote(1, writer => writer.putU16(0))).to.throw(
'Not enough bytes: Tried writing 2 byte(s), but only 1 byte(s) have been pre-allocated',
);
expect(() => wrote(3, writer => writer.putU32(0))).to.throw(
'Not enough bytes: Tried writing 4 byte(s), but only 3 byte(s) have been pre-allocated',
);
expect(() => wrote(7, writer => writer.putU64(0n))).to.throw(
'Not enough bytes: Tried writing 8 byte(s), but only 7 byte(s) have been pre-allocated',
);
expect(() =>
wrote(2, writer => writer.putBytes(new Uint8Array(3))),
).to.throw(
'Not enough bytes: Tried writing 3 byte(s), but only 2 byte(s) have been pre-allocated',
);
});
});
diff --git a/modules/ecash-lib/src/io/writerbytes.ts b/modules/ecash-lib/src/io/writerbytes.ts
index 95e94bfb8..fb231be66 100644
--- a/modules/ecash-lib/src/io/writerbytes.ts
+++ b/modules/ecash-lib/src/io/writerbytes.ts
@@ -1,86 +1,87 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+import { Endian, endianToBool } from './bytes.js';
import { Int } from './int.js';
import { Writer } from './writer.js';
/**
* Implementation of `Writer` which writes to an array of pre-allocated size.
* It's intended to be used in unison with `WriterLength`, which first finds
* out the length of the serialized object and then the actual data is written
* using this class.
**/
export class WriterBytes implements Writer {
public data: Uint8Array;
public view: DataView;
public idx: number;
/** Create a new WriterBytes with the given pre-allocated size */
public constructor(length: number) {
this.data = new Uint8Array(length);
this.view = new DataView(
this.data.buffer,
this.data.byteOffset,
this.data.byteLength,
);
this.idx = 0;
}
/** Write a single byte */
public putU8(value: Int): void {
if (value < 0 || value > 0xff) {
throw new Error(`Cannot fit ${value} into a u8`);
}
this.ensureSize(1);
this.data[this.idx] = Number(value);
this.idx++;
}
/** Write a 2-byte little-endian integer (uint16_t) */
- public putU16(value: Int): void {
+ public putU16(value: Int, endian?: Endian): void {
if (value < 0 || value > 0xffff) {
throw new Error(`Cannot fit ${value} into a u16`);
}
this.ensureSize(2);
- this.view.setUint16(this.idx, Number(value), true);
+ this.view.setUint16(this.idx, Number(value), endianToBool(endian));
this.idx += 2;
}
/** Write a 4-byte little-endian integer (uint32_t) */
- public putU32(value: Int): void {
+ public putU32(value: Int, endian?: Endian): void {
if (value < 0 || value > 0xffffffff) {
throw new Error(`Cannot fit ${value} into a u32`);
}
this.ensureSize(4);
- this.view.setUint32(this.idx, Number(value), true);
+ this.view.setUint32(this.idx, Number(value), endianToBool(endian));
this.idx += 4;
}
/** Write an 8-byte little-endian integer (uint64_t) */
- public putU64(value: Int): void {
+ public putU64(value: Int, endian?: Endian): void {
if (value < 0 || value > 0xffffffffffffffffn) {
throw new Error(`Cannot fit ${value} into a u64`);
}
this.ensureSize(8);
- this.view.setBigUint64(this.idx, BigInt(value), true);
+ this.view.setBigUint64(this.idx, BigInt(value), endianToBool(endian));
this.idx += 8;
}
/** Write the given bytes */
public putBytes(bytes: Uint8Array): void {
this.ensureSize(bytes.length);
this.data.set(bytes, this.idx);
this.idx += bytes.length;
}
private ensureSize(extraBytes: number) {
if (this.data.length < this.idx + extraBytes) {
throw new Error(
`Not enough bytes: Tried writing ${extraBytes} byte(s), but ` +
`only ${this.data.length - this.idx} byte(s) have been ` +
`pre-allocated`,
);
}
}
}
diff --git a/modules/ecash-lib/src/io/writerlength.ts b/modules/ecash-lib/src/io/writerlength.ts
index 164be05f6..b328ab38e 100644
--- a/modules/ecash-lib/src/io/writerlength.ts
+++ b/modules/ecash-lib/src/io/writerlength.ts
@@ -1,43 +1,44 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+import { Endian } from './bytes.js';
import { Int } from './int.js';
import { Writer } from './writer.js';
/**
* Writer implementation which only measures the length of the serialized
* output but doesn't actually store any byte data.
**/
export class WriterLength implements Writer {
public length: number;
public constructor() {
this.length = 0;
}
/** Write a single byte */
public putU8(_value: Int): void {
this.length++;
}
/** Write a 2-byte little-endian integer (uint16_t) */
- public putU16(_value: Int): void {
+ public putU16(_value: Int, _endian?: Endian): void {
this.length += 2;
}
/** Write a 4-byte little-endian integer (uint32_t) */
- public putU32(_value: Int): void {
+ public putU32(_value: Int, _endian?: Endian): void {
this.length += 4;
}
/** Write an 8-byte little-endian integer (uint64_t) */
- public putU64(_value: Int): void {
+ public putU64(_value: Int, _endian?: Endian): void {
this.length += 8;
}
/** Write the given bytes */
public putBytes(bytes: Uint8Array): void {
this.length += bytes.length;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 21, 17:39 (1 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865607
Default Alt Text
(35 KB)

Event Timeline