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; } }