Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864167
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
35 KB
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
rABC Bitcoin ABC
Event Timeline
Log In to Comment