diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3b62fa76d..4061b0f14 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -33,9 +33,9 @@ jobs: - name: Download and install bitcoind run: | if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - wget https://bitcoin.org/bin/bitcoin-core-27.0/bitcoin-27.0-x86_64-linux-gnu.tar.gz - tar -xzf bitcoin-27.0-x86_64-linux-gnu.tar.gz - sudo mv bitcoin-27.0/bin/bitcoind /usr/local/bin/ + wget https://bitcoin.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz + tar -xzf bitcoin-28.0-x86_64-linux-gnu.tar.gz + sudo mv bitcoin-28.0/bin/bitcoind /usr/local/bin/ else brew install bitcoin fi diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 51c14af31..be12ae7fe 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -1,6 +1,13 @@ # Requirements for Fabric Nodes The following requirements are necessary to run a Fabric node: -1. Fully up-to-date `bitcoind >= 0.21.1` -2. 166 MHz x86 CPU -3. 256 MB RAM +1. bitcoind >= 0.21.1 +2. > 1 GHz CPU +3. > 1 GB RAM +4. > 1 TB storage +5. > 500 MB/day bandwidth + +Some configuration options can reduce these requirements, but they should be considered the minimum specification for a full Fabric node. + +## Bandwidth +1MB/s is considered minimal for Fabric connections. When possible, Fabric will attempt to use a constant stream of 1MB/s of over-the-wire bandwith (after compression and encryption). High-latency connections (those with a connection exceeding 250ms ping) should expect some operations to take longer than an hour. diff --git a/images/architecture.png b/assets/images/architecture.png similarity index 100% rename from images/architecture.png rename to assets/images/architecture.png diff --git a/images/fabric-labs.png b/assets/images/fabric-labs.png similarity index 100% rename from images/fabric-labs.png rename to assets/images/fabric-labs.png diff --git a/constants.js b/constants.js index a1a6ffb8b..6ec0f7d46 100644 --- a/constants.js +++ b/constants.js @@ -44,11 +44,16 @@ const FABRIC_PLAYNET_ADDRESS = ''; // deposit address (P2TR) const FABRIC_PLAYNET_ORIGIN = ''; // block hash of first deploy // FABRIC ONLY +const BITCOIN_BLOCK_TYPE = 21000; +const BITCOIN_BLOCK_HASH_TYPE = 21100; +const BITCOIN_TRANSACTION_TYPE = 22000; +const BITCOIN_TRANSACTION_HASH_TYPE = 22100; const GENERIC_MESSAGE_TYPE = 15103; const LOG_MESSAGE_TYPE = 3235156080; const GENERIC_LIST_TYPE = 3235170158; const DOCUMENT_PUBLISH_TYPE = 998; const DOCUMENT_REQUEST_TYPE = 999; +const JSON_CALL_TYPE = 16000; // Opcodes const OP_CYCLE = '00'; @@ -139,6 +144,10 @@ module.exports = { FIXTURE_XPUB, FIXTURE_XPRV, HEADER_SIZE, + BITCOIN_BLOCK_TYPE, + BITCOIN_BLOCK_HASH_TYPE, + BITCOIN_TRANSACTION_TYPE, + BITCOIN_TRANSACTION_HASH_TYPE, GENERIC_MESSAGE_TYPE, LOG_MESSAGE_TYPE, GENERIC_LIST_TYPE, @@ -207,6 +216,7 @@ module.exports = { PEER_CANDIDATE, DOCUMENT_PUBLISH_TYPE, DOCUMENT_REQUEST_TYPE, + JSON_CALL_TYPE, SESSION_START, VERSION_NUMBER }; diff --git a/fixtures.js b/fixtures.js index 4d0b25f80..b1896b336 100644 --- a/fixtures.js +++ b/fixtures.js @@ -5,6 +5,13 @@ const TEST_SEED = 'abandon abandon abandon abandon abandon abandon abandon aband const TEST_XPRV = 'xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF'; const TEST_XPUB = 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; +const BIP32_VECTOR_1_SEED = '000102030405060708090a0b0c0d0e0f'; +const BIP32_VECTOR_1_M_XPUB = 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; +const BIP32_VECTOR_1_M_XPRV = 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'; +const BIP32_VECTOR_2_SEED = 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542'; +const BIP32_VECTOR_2_M_XPUB = 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB'; +const BIP32_VECTOR_2_M_XPRV = 'xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U'; + // Strings const EMPTY_STRING = ''; const FABRIC_HELLO_WORLD = 'Hello, World!'; @@ -15,6 +22,12 @@ const GITHUB_ISSUE_PATH = 'FabricLabs/fabric/issues/1'; // Module module.exports = { EMPTY_STRING: EMPTY_STRING, + BIP32_VECTOR_1_SEED, + BIP32_VECTOR_1_M_XPUB, + BIP32_VECTOR_1_M_XPRV, + BIP32_VECTOR_2_SEED, + BIP32_VECTOR_2_M_XPUB, + BIP32_VECTOR_2_M_XPRV, FABRIC_HELLO_WORLD: FABRIC_HELLO_WORLD, FABRIC_SEED: TEST_SEED, FABRIC_XPRV: TEST_XPRV, diff --git a/package-lock.json b/package-lock.json index 7229ed221..2cbf352fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "arbitrary": "=1.4.10", - "base58check": "=2.0.0", "bech32-buffer": "=0.2.1", "bip-schnorr": "=0.6.7", "bip32": "=4.0.0", @@ -175,9 +174,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -185,9 +184,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -195,13 +194,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -211,14 +210,14 @@ } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -264,9 +263,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -536,9 +535,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -828,9 +827,9 @@ } }, "node_modules/@scure/base": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.5.tgz", - "integrity": "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -846,9 +845,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -941,9 +940,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1198,20 +1197,11 @@ "license": "MIT" }, "node_modules/base-x": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-1.1.0.tgz", - "integrity": "sha512-c0WLeG3K5OlL4Skz2/LVdS+MjggByKhowxQpG+JpCLA48s/bGwIDyzA1naFjywtNvp/37fLK0p0FpjTNNLLUXQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", "license": "MIT" }, - "node_modules/base58check": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base58check/-/base58check-2.0.0.tgz", - "integrity": "sha512-sTzsDAOC9+i2Ukr3p1Ie2DWpD117ua+vBJRDnpsSlScGwImeeiTg/IatwcFLsz9K9wEGoBLVd5ahNZzrZ/jZyg==", - "license": "MIT", - "dependencies": { - "bs58": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1480,12 +1470,12 @@ "license": "ISC" }, "node_modules/bs58": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-3.1.0.tgz", - "integrity": "sha512-9C2bRFTGy3meqO65O9jLvVTyawvhLVp4h2ECm5KlRPuV5KPDNJZcJIj3gl+aA0ENXcYrUSLCkPAeqbTcI2uWyQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", "license": "MIT", "dependencies": { - "base-x": "^1.1.0" + "base-x": "^4.0.0" } }, "node_modules/bs58check": { @@ -1498,21 +1488,6 @@ "bs58": "^5.0.0" } }, - "node_modules/bs58check/node_modules/base-x": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", - "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", - "license": "MIT" - }, - "node_modules/bs58check/node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", - "dependencies": { - "base-x": "^4.0.0" - } - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1755,22 +1730,22 @@ } }, "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz", + "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==", "dev": true, "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", + "domutils": "^3.2.2", "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", + "undici": "^7.10.0", "whatwg-mimetype": "^4.0.0" }, "engines": { @@ -2294,9 +2269,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2774,9 +2749,9 @@ } }, "node_modules/encoding-sniffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -2977,9 +2952,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2994,9 +2969,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3007,15 +2982,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3377,14 +3352,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4054,9 +4030,9 @@ "license": "MIT" }, "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -4069,14 +4045,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" + "domutils": "^3.2.1", + "entities": "^6.0.0" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -6208,9 +6184,9 @@ } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -6254,6 +6230,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/path-match/-/path-match-1.2.4.tgz", "integrity": "sha512-UWlehEdqu36jmh4h5CWJ7tARp1OEVKGHKm6+dg9qMq5RKUTV5WJrGgaZ3dN2m7WFAXDbjlHzvJvL/IUpy84Ktw==", + "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", "license": "MIT", "dependencies": { "http-errors": "~1.4.0", @@ -6694,9 +6671,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7537,13 +7514,13 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", - "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, "node_modules/union": { diff --git a/package.json b/package.json index 47e9deee0..7562a6dae 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,6 @@ "homepage": "https://github.com/FabricLabs/fabric#readme", "dependencies": { "arbitrary": "=1.4.10", - "base58check": "=2.0.0", "bech32-buffer": "=0.2.1", "bip-schnorr": "=0.6.7", "bip32": "=4.0.0", @@ -176,6 +175,9 @@ "./constants": { "require": "./constants.js" }, + "./fixtures": { + "require": "./fixtures.js" + }, "./types/*": { "require": "./types/*.js" }, diff --git a/reports/TODO.txt b/reports/TODO.txt index 8d8a3e737..da3f9e553 100644 --- a/reports/TODO.txt +++ b/reports/TODO.txt @@ -13,7 +13,6 @@ ./settings/local.js: // TODO: regtest, playnet, signet, testnet, mainnet (in order) ./settings/local.js: // TODO: test `true` ./settings/default.json: "@comment": "// TODO: remove routes, add by default", -./SETTINGS.md:### WARNING: TODO ./types/witness.js: // TODO: assign R coordinate ./types/witness.js: // TODO: assign S coordinate ./types/channel.js: // TODO: remove short-circuit @@ -25,7 +24,6 @@ ./types/scribe.js: // TODO: enable ./types/key.js:// TODO: remove ./types/key.js:// TODO: remove all external dependencies -./types/key.js: // TODO: design state machine for input (configuration) ./types/key.js: // TODO: determine if this makes sense / needs to be private ./types/key.js: // TODO: evaluate compression when treating seed phrase as ascii ./types/key.js: // TODO: consider using sha256(masterprivkey) or sha256(sha256(...))? @@ -142,18 +140,7 @@ ./types/wallet.js: // TODO: should be split parts ./types/wallet.js: // TODO: labeled keypairs ./types/wallet.js: rate: 10000, // TODO: fee calculation -./types/wallet.js: // TODO: return a full object for Fabric ./types/wallet.js: // TODO: restore swap code, abstract input types -./types/wallet.js: // TODO: use Satoshis for all calculations -./types/wallet.js: // TODO: remove all fake coinbases -./types/wallet.js: // TODO: remove all short-circuits -./types/wallet.js: // TODO: remove short-circuit -./types/wallet.js: // TODO: remove entirely, test short-circuit removal -./types/wallet.js: // TODO: wallet._getSpendableOutput() -./types/wallet.js: // TODO: load available outputs from wallet -./types/wallet.js: // TODO: fee estimation -./types/wallet.js: // TODO: fee estimation -./types/wallet.js: // TODO: load available outputs from wallet ./types/wallet.js: // TODO: fee estimation ./types/wallet.js: // TODO: restore address tracking in state ./types/wallet.js: // TODO: check on above events, should be more like... @@ -244,6 +231,7 @@ ./DEVELOPERS.md:- [ ] Remove TODOs ./package.json: "report:todo": "grep --exclude-dir=.git --exclude-dir=_book --exclude-dir=assets --exclude-dir=node_modules --exclude-dir=reports --exclude-dir=coverage --exclude-dir=docs -rEI \"TODO|FIXME\" . > reports/TODO.txt", ./package.json: "review:todo": "npm run report:todo && cat reports/TODO.txt && echo '\nOutstanding TODO items (@fabric/core):' && wc -l reports/TODO.txt && echo '\nIssues List: https://github.com/FabricLabs/fabric/issues\nDisclosures: securiy@fabric.pub\n\n'", +./guides/SETTINGS.md:### WARNING: TODO ./examples/bitcoin.js: // TODO: import these into core process logic ./examples/game.js: // TODO: use fabric call ./examples/swarm.html:
TODO: create entities on seed node
@@ -287,12 +275,7 @@
./services/bitcoin.js: // TODO: verify block hash!!!
./services/bitcoin.js: // TODO: enable sharing of local hashes
./services/bitcoin.js: // TODO: fix @types/wallet to use named types for Addresses...
-./services/bitcoin.js: // TODO: not rely on parseFloat
./services/bitcoin.js: // TODO: report FundingError: Not enough funds
-./services/bitcoin.js: // TODO: not rely on parseFloat
-./services/bitcoin.js: // TODO: add support for segwit, taproot
-./services/bitcoin.js: // TODO: use satoshis/vbyte
-./services/bitcoin.js: // TODO: SECURITY !!!
./services/bitcoin.js: // TODO: async (i.e., Promise.all) chainsync
./services/bitcoin.js: // TODO: use RPC auth
./services/bitcoin.js: // TODO: re-enable these
diff --git a/reports/install.log b/reports/install.log
index 278bcefac..1c3e66293 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -1,6 +1,6 @@
$ npm i
-added 686 packages, and audited 687 packages in 5s
+added 683 packages, and audited 684 packages in 10s
104 packages are looking for funding
run `npm fund` for details
diff --git a/services/bitcoin.js b/services/bitcoin.js
index fd8e13439..dfc30b302 100644
--- a/services/bitcoin.js
+++ b/services/bitcoin.js
@@ -26,6 +26,9 @@ const bip68 = require('bip68');
const ECPair = ECPairFactory(ecc);
const bitcoin = require('bitcoinjs-lib');
+// Initialize bitcoinjs-lib with the ECC library
+bitcoin.initEccLib(ecc);
+
// Services
const ZMQ = require('../services/zmq');
@@ -33,6 +36,7 @@ const ZMQ = require('../services/zmq');
const Actor = require('../types/actor');
const Collection = require('../types/collection');
const Entity = require('../types/entity');
+const Key = require('../types/key');
const Service = require('../types/service');
const State = require('../types/state');
const Wallet = require('../types/wallet');
@@ -62,7 +66,7 @@ class Bitcoin extends Service {
name: '@services/bitcoin',
mode: 'fabric',
genesis: BITCOIN_GENESIS,
- network: 'regtest',
+ network: 'mainnet',
path: './stores/bitcoin',
mining: false,
listen: false,
@@ -80,6 +84,13 @@ class Bitcoin extends Service {
host: 'localhost',
port: 29500
},
+ key: {
+ mnemonic: null,
+ seed: null,
+ xprv: null,
+ xpub: null,
+ passphrase: null
+ },
state: {
actors: {},
blocks: {}, // Map of blocks by block hash
@@ -87,7 +98,8 @@ class Bitcoin extends Service {
tip: BITCOIN_GENESIS_HASH,
transactions: {}, // Map of transactions by txid
addresses: {}, // Map of addresses to their transactions
- index: 0 // Current address index
+ walletIndex: 0, // Current address index
+ supply: 0
},
nodes: ['127.0.0.1'],
seeds: ['127.0.0.1'],
@@ -109,6 +121,10 @@ class Bitcoin extends Service {
if (this.settings.debug && this.settings.verbosity >= 4) console.debug('[DEBUG]', 'Instance of Bitcoin service created, settings:', this.settings);
+ this._rootKey = new Key({
+ ...this.settings.key
+ });
+
// Bcoin for JS full node
// bcoin.set(this.settings.network);
// this.network = bcoin.Network.get(this.settings.network);
@@ -116,7 +132,7 @@ class Bitcoin extends Service {
// Internal Services
this.observer = null;
// this.provider = new Consensus({ provider: 'bcoin' });
- this.wallet = new Wallet(this.settings);
+ this.wallet = new Wallet({ ...this.settings, key: { xprv: this._rootKey.xprv } });
// this.chain = new Chain(this.settings);
// ## Collections
@@ -177,7 +193,7 @@ class Bitcoin extends Service {
workers: true
}); */
- this.zmq = new ZMQ(this.settings.zmq);
+ this.zmq = new ZMQ({ ...this.settings.zmq, key: { xprv: this._rootKey.xprv } });
// Define Bitcoin P2P Messages
this.define('VersionPacket', { type: 0 });
@@ -257,10 +273,20 @@ class Bitcoin extends Service {
return bitcoin;
}
+ get network () {
+ return this.settings.network;
+ }
+
get networks () {
return this._networkConfigs;
}
+ get walletName () {
+ const preimage = crypto.createHash('sha256').update(this.settings.key.xpub).digest('hex');
+ const hash = crypto.createHash('sha256').update(preimage).digest('hex');
+ return this.settings.walletName || hash;
+ }
+
set best (best) {
if (best === this.best) return this.best;
if (best !== this.best) {
@@ -278,51 +304,6 @@ class Bitcoin extends Service {
return this._state.content.supply;
}
- createKeySpendOutput (publicKey) {
- // x-only pubkey (remove 1 byte y parity)
- const myXOnlyPubkey = publicKey.slice(1, 33);
- const commitHash = bitcoin.crypto.taggedHash('TapTweak', myXOnlyPubkey);
- const tweakResult = ecc.xOnlyPointAddTweak(myXOnlyPubkey, commitHash);
- if (tweakResult === null) throw new Error('Invalid Tweak');
-
- const { xOnlyPubkey: tweaked } = tweakResult;
-
- // scriptPubkey
- return Buffer.concat([
- // witness v1, PUSH_DATA 32 bytes
- Buffer.from([0x51, 0x20]),
- // x-only tweaked pubkey
- tweaked,
- ]);
- }
-
- createSigned (key, txid, vout, amountToSend, scriptPubkeys, values) {
- const tx = new bitcoin.Transaction();
-
- tx.version = 2;
-
- // Add input
- tx.addInput(Buffer.from(txid, 'hex').reverse(), vout);
-
- // Add output
- tx.addOutput(scriptPubkeys[0], amountToSend);
-
- const sighash = tx.hashForWitnessV1(
- 0, // which input
- scriptPubkeys, // All previous outputs of all inputs
- values, // All previous values of all inputs
- bitcoin.Transaction.SIGHASH_DEFAULT // sighash flag, DEFAULT is schnorr-only (DEFAULT == ALL)
- );
-
- const signature = Buffer.from(signTweaked(sighash, key));
-
- // witness stack for keypath spend is just the signature.
- // If sighash is not SIGHASH_DEFAULT (ALL) then you must add 1 byte with sighash value
- tx.ins[0].witness = [signature];
-
- return tx;
- }
-
createRPCAuth (settings = {}) {
if (!settings.username) throw new Error('Username is required.');
const username = settings.username;
@@ -336,26 +317,21 @@ class Bitcoin extends Service {
};
}
- signTweaked (messageHash, key) {
- // Order of the curve (N) - 1
- const N_LESS_1 = Buffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', 'hex');
- // 1 represented as 32 bytes BE
- const ONE = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex');
- const privateKey = (key.publicKey[0] === 2) ? key.privateKey : ecc.privateAdd(ecc.privateSub(N_LESS_1, key.privateKey), ONE);
- const tweakHash = bitcoin.crypto.taggedHash('TapTweak', key.publicKey.slice(1, 33));
- const newPrivateKey = ecc.privateAdd(privateKey, tweakHash);
- if (newPrivateKey === null) throw new Error('Invalid Tweak');
- return ecc.signSchnorr(messageHash, newPrivateKey, Buffer.alloc(32));
- }
-
validateAddress (address) {
try {
// Get the correct network configuration
- const network = this.networks[this.settings.network];
+ const network = this._networkConfigs[this.settings.network];
if (!network) {
throw new Error(`Invalid network: ${this.settings.network}`);
}
+ try {
+ bitcoin.address.toOutputScript(address, network);
+ return true;
+ } catch (e) {
+ return false;
+ }
+
// Try to convert the address to an output script
bitcoin.address.toOutputScript(address, network);
return true;
@@ -750,45 +726,88 @@ class Bitcoin extends Service {
}
async _loadWallet (name) {
- const actor = new Actor({ content: name });
-
+ if (!name) name = this.walletName;
try {
- // Try to create wallet first
- await this._makeRPCRequest('createwallet', [
- actor.id,
- false,
- false, // blank (use sethdseed)
- '', // passphrase
- true, // avoid reuse
- false, // descriptors
- ]);
-
- // Load the wallet
- await this._makeRPCRequest('loadwallet', [actor.id]);
-
- // Get addresses
+ const info = await this._makeRPCRequest('getnetworkinfo');
+ const version = parseInt(info.version);
+ const useDescriptors = version >= 240000; // Descriptors became stable in v24.0
+
+ if (this.settings.debug) console.trace('[FABRIC:BITCOIN]', `Loading wallet: ${name}, version: ${version}, descriptors: ${useDescriptors}`);
+
+ const walletParams = [
+ name,
+ false, // disable_private_keys
+ false, // TODO: enable blank, import _rootKey
+ null, // passphrase
+ true, // avoid_reuse
+ useDescriptors // descriptors - only enable for newer versions
+ ];
+
+ // First try to load an existing wallet
try {
- this.addresses = await this._listAddresses();
- } catch (exception) {
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Error listing addresses:', exception.message);
- this.addresses = [];
- }
+ await this._makeRPCRequest('loadwallet', [name]);
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Successfully loaded existing wallet: ${name}`);
+ return { name };
+ } catch (loadError) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Load error for wallet ${name}:`, loadError.message);
- // If no addresses, generate one
- if (!this.addresses || !this.addresses.length) {
- const address = await this.getUnusedAddress();
- this.addresses = [address];
- }
+ // If wallet doesn't exist (-18) or path doesn't exist, we need to create it
+ if (loadError.code === -18 || loadError.message.includes('does not exist')) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Wallet path does not exist, creating new wallet: ${name}`);
- return {
- id: actor.id
- };
+ try {
+ await this._makeRPCRequest('createwallet', walletParams);
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Successfully created wallet: ${name}`);
+ return { name };
+ } catch (createError) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Create error for wallet ${name}:`, createError.message);
+ throw createError;
+ }
+ }
+
+ // If wallet is already loaded (-35), that's fine
+ if (loadError.code === -35) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Wallet ${name} already loaded`);
+ return { name };
+ }
+
+ // For any other error where the wallet might be in a bad state, try unloading and recreating
+ try {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Attempting to unload and recreate wallet: ${name}`);
+ // Try to unload (might fail if wallet isn't loaded, but that's okay)
+ try {
+ await this._makeRPCRequest('unloadwallet', [name]);
+ } catch (unloadError) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Unload failed (wallet may not be loaded): ${unloadError.message}`);
+ // Continue anyway - the wallet probably wasn't loaded
+ }
+
+ await this._makeRPCRequest('createwallet', walletParams);
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Successfully recreated wallet: ${name}`);
+ return { name };
+ } catch (recreateError) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Recreate error for wallet ${name}:`, recreateError.message);
+ throw recreateError;
+ }
+ }
} catch (error) {
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Error loading wallet:', error.message);
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Wallet loading sequence error:', error);
throw error;
}
}
+ async _unloadWallet (name) {
+ if (!name) name = this.walletName;
+ try {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Attempting to unload wallet: ${name}`);
+ await this._makeRPCRequest('unloadwallet', [name]);
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Successfully unloaded wallet: ${name}`);
+ return { name };
+ } catch (error) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Wallet unloading sequence:', error.message);
+ }
+ }
+
/**
* Attach event handlers for a supplied list of addresses.
* @param {Shard} shard List of addresses to monitor.
@@ -878,93 +897,59 @@ class Bitcoin extends Service {
}
}
+ async _handleZMQMessage (msg) {
+ let topic, content;
+
+ // Handle both raw ZMQ messages and Message objects
+ if (Array.isArray(msg)) {
+ // Raw ZMQ message format
+ topic = msg[0].toString();
+ content = msg[1];
+ } else if (msg && typeof msg === 'object') {
+ // Message object format
+ topic = msg.type;
+ content = msg.data;
+ } else {
+ console.error('[BITCOIN]', 'Invalid message format:', msg);
+ return;
+ }
+
+ if (this.settings.debug) this.emit('debug', '[ZMQ] Received message on topic:', topic, 'Message length:', content.length);
+
+ try {
+ switch (topic) {
+ case 'BitcoinBlock':
+ case 'BitcoinTransactionHash':
+ break;
+ case 'BitcoinBlockHash':
+ const message = JSON.parse(content.toString());
+ const supply = await this._makeRPCRequest('gettxoutsetinfo', []);
+ this._state.content.height = supply.height;
+ this._state.content.tip = message.content;
+ this._state.content.supply = supply.total_amount;
+ this.commit();
+ break;
+ case 'BitcoinTransaction':
+ // const record = JSON.parse(content.toString());
+ const balance = await this._makeRPCRequest('getbalances', []);
+ this._state.balances.mine.trusted = balance;
+ this.commit();
+ break;
+ default:
+ if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'Unknown ZMQ topic:', topic);
+ }
+ } catch (exception) {
+ //', `Could not process ZMQ message: ${exception}`);
+ }
+ }
+
async _startZMQ () {
if (this.settings.verbosity >= 5) console.debug('[AUDIT]', 'Starting ZMQ service...');
this.zmq.on('log', (msg) => {
if (this.settings.debug) console.log('[ZMQ]', msg);
});
- this.zmq.on('message', async (msg) => {
- let topic, content;
-
- // Handle both raw ZMQ messages and Message objects
- if (Array.isArray(msg)) {
- // Raw ZMQ message format
- topic = msg[0].toString();
- content = msg[1];
- } else if (msg && typeof msg === 'object') {
- // Message object format
- topic = msg.type;
- content = msg.data;
- } else {
- console.error('[BITCOIN]', 'Invalid message format:', msg);
- return;
- }
-
- if (this.settings.debug) this.emit('debug', '[ZMQ] Received message on topic:', topic, 'Message length:', content.length);
-
- try {
- switch (topic) {
- case 'GenericMessage':
- // Handle generic message
- if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'Received generic message:', content.toString());
- console.debug('current state:', this.state);
- break;
- case 'hashblock':
- // Update state with block hash (reversed byte order)
- const blockHash = content.toString('hex');
- this._state.content.blocks[blockHash] = {
- hash: blockHash,
- raw: null,
- timestamp: Date.now()
- };
- if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'Received block hash:', blockHash);
- break;
- case 'rawblock':
- // Update state with full block data
- const block = await this.blocks.create({
- hash: content.hash('hex'),
- parent: content.prevBlock.toString('hex'),
- transactions: content.txs.map(tx => tx.hash('hex')),
- block: content,
- raw: content.toRaw().toString('hex'),
- timestamp: Date.now()
- });
- this._state.content.blocks[block.hash] = block;
- if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'Received raw block:', block.hash);
- break;
- case 'hashtx':
- // Update state with transaction hash (reversed byte order)
- const txHash = content.toString('hex');
- if (!this._state.content.transactions[txHash]) {
- this._state.content.transactions[txHash] = {
- hash: txHash,
- raw: null,
- timestamp: Date.now()
- };
- }
- if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'Received transaction hash:', txHash);
- break;
- case 'rawtx':
- // Update state with full transaction data
- const tx = {
- hash: content.hash('hex'),
- inputs: content.inputs,
- outputs: content.outputs,
- tx: content,
- raw: content.toRaw().toString('hex'),
- timestamp: Date.now()
- };
- this._state.content.transactions[tx.hash] = tx;
- if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'Received raw transaction:', tx.hash);
- break;
- default:
- if (this.settings.verbosity >= 5) console.log('[AUDIT]', 'Unknown ZMQ topic:', topic);
- }
- } catch (exception) {
- this.emit('error', `Could not process ZMQ message: ${exception}`);
- }
- });
+ this.zmq.on('message', this._handleZMQMessage.bind(this));
this.zmq.on('error', (err) => {
console.error('[ZMQ] Error:', err);
@@ -1024,16 +1009,29 @@ class Bitcoin extends Service {
async getUnusedAddress () {
if (this.rpc) {
- const address = await this._makeRPCRequest('getnewaddress');
- return address;
+ try {
+ await this._loadWallet(this.walletName);
+ const info = await this._makeRPCRequest('getnetworkinfo');
+ const version = parseInt(info.version);
+ const address = await this._makeRPCRequest('getnewaddress', [
+ '', // label
+ version >= 240000 ? 'legacy' : 'legacy' // address type
+ ]);
+
+ if (!address) throw new Error('No address returned from getnewaddress');
+ return address;
+ } catch (error) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Error getting unused address:', error);
+ throw error;
+ }
} else if (this.settings.key) {
// In fabric mode, use the provided key to derive an address
- const target = this.settings.key.deriveAddress(this.settings.state.index);
+ const target = this.settings.key.deriveAddress(this.settings.state.walletIndex);
// Increment the index for next time
- this.settings.state.index++;
+ this.settings.state.walletIndex++;
// Track the address
this.settings.state.addresses[target.address] = {
- index: this.settings.state.index - 1,
+ index: this.settings.state.walletIndex - 1,
transactions: []
};
return target.address;
@@ -1043,13 +1041,13 @@ class Bitcoin extends Service {
network: this.settings.network,
purpose: 44,
account: 0,
- index: this.settings.state.index
+ index: this.settings.state.walletIndex
});
this.settings.key = key;
- const target = key.deriveAddress(this.settings.state.index);
- this.settings.state.index++;
+ const target = key.deriveAddress(this.settings.state.walletIndex);
+ this.settings.state.walletIndex++;
this.settings.state.addresses[target.address] = {
- index: this.settings.state.index - 1,
+ index: this.settings.state.walletIndex - 1,
transactions: []
};
return target.address;
@@ -1080,71 +1078,34 @@ class Bitcoin extends Service {
return this._makeRPCRequest('listreceivedbyaddress', [1, true]);
}
+ /**
+ * Make a single RPC request to the Bitcoin node.
+ * @param {String} method The RPC method to call.
+ * @param {Array} params The parameters to pass to the RPC method.
+ * @returns {Promise} A promise that resolves to the RPC response.
+ */
async _makeRPCRequest (method, params = []) {
- if (this.settings.mode === 'fabric') {
- // In fabric mode, handle requests locally
- switch (method) {
- case 'getnewaddress':
- if (this.settings.key) {
- const target = this.settings.key.deriveAddress(this.settings.state.index);
- this.settings.state.index++;
- this.settings.state.addresses[target.address] = {
- index: this.settings.state.index - 1,
- transactions: []
- };
- return target.address;
- }
- throw new Error('No key provided for address generation in fabric mode');
- case 'validateaddress':
- return { isvalid: this.validateAddress(params[0]) };
- case 'getblockchaininfo':
- return { blocks: this.settings.state.height };
- case 'getblockcount':
- return this.settings.state.height;
- case 'getbestblockhash':
- return this.settings.state.tip;
- case 'listunspent':
- return [];
- default:
- if (this.settings.managed) {
- // Allow RPC request to be sent in managed mode
- break;
- }
- throw new Error(`Method ${method} not implemented in fabric mode`);
- }
- }
-
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Making RPC request: ${method}(${JSON.stringify(params)})`);
return new Promise((resolve, reject) => {
- if (!this.rpc) {
- const error = new Error('RPC manager does not exist');
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'RPC request failed:', error.message);
- return reject(error);
- }
- try {
- this.rpc.request(method, params, (err, response) => {
- if (err) {
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'RPC request failed:', err);
- // Handle different types of errors
- if (err.error) {
- try {
- // If err.error is a string, try to parse it as JSON
- const errorObj = typeof err.error === 'string' ? JSON.parse(err.error) : err.error;
- return reject(new Error(errorObj.message || errorObj.error || JSON.stringify(errorObj)));
- } catch (parseError) {
- // If parsing fails, use the original error
- return reject(new Error(err.error));
- }
- }
- return reject(new Error(err.message || err));
- }
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `RPC response for ${method}:`, response.result);
- return resolve(response.result);
- });
- } catch (exception) {
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'RPC request failed with exception:', exception);
- return reject(new Error(`RPC request failed: ${exception.message}`));
- }
+ if (!this.rpc) return reject(new Error('RPC manager does not exist'));
+
+ this.rpc.request(method, params, (err, response) => {
+ if (err) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `RPC error for ${method}(${params.join(', ')}):`, err);
+ return reject(err);
+ }
+
+ if (!response) {
+ return reject(new Error(`No response from RPC call ${method}`));
+ }
+
+ if (response.error) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `RPC response error for ${method}:`, response.error);
+ return reject(response.error);
+ }
+
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `RPC response for ${method}:`, response);
+ return resolve(response.result);
+ });
});
}
@@ -1241,7 +1202,15 @@ class Bitcoin extends Service {
}
async _listUnspent () {
- return this._makeRPCRequest('listunspent', []);
+ try {
+ // Ensure a wallet is loaded
+ await this._loadWallet(this.walletName);
+ return this._makeRPCRequest('listunspent', []);
+ } catch (error) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Error listing unspent outputs:', error.message);
+ // Return empty array on error
+ return [];
+ }
}
async _encodeSequenceForNBlocks (time) {
@@ -1257,42 +1226,6 @@ class Bitcoin extends Service {
return this._makeRPCRequest('signrawtransaction', [rawTX, JSON.stringify(prevouts)]);
}
- async _getUTXOSetMeta (utxos) {
- const coins = [];
- const keys = [];
-
- let inputSum = 0;
- let inputCount = 0;
-
- for (let i = 0; i < utxos.length; i++) {
- const candidate = utxos[i];
- const template = {
- hash: Buffer.from(candidate.txid, 'hex').reverse(),
- index: candidate.vout,
- value: Amount.fromBTC(candidate.amount).toValue(),
- script: Script.fromAddress(candidate.address)
- };
-
- const c = Coin.fromOptions(template);
- const keypair = await this._dumpKeyPair(candidate.address);
-
- coins.push(c);
- keys.push(keypair);
-
- inputCount++;
- // TODO: not rely on parseFloat
- // use bitwise...
- inputSum += parseFloat(template.value);
- }
-
- return {
- inputs: {
- count: inputCount,
- total: inputSum
- }
- };
- }
-
/**
* Creates an unsigned Bitcoin transaction.
* @param {Object} options
@@ -1323,89 +1256,6 @@ class Bitcoin extends Service {
};
}
- async _createContractFromProposal (proposal) {
- const tx = proposal.mtx.toTX();
- const raw = tx.toRaw().toString('hex');
- return {
- tx: tx,
- raw: raw
- };
- }
-
- async _getCoinsFromInputs (inputs = []) {
- const coins = [];
- const keys = [];
-
- let inputSum = 0;
- let inputCount = 0;
-
- for (let i = 0; i < inputs.length; i++) {
- const candidate = inputs[i];
- const template = {
- hash: Buffer.from(candidate.txid, 'hex').reverse(),
- index: candidate.vout,
- value: Amount.fromBTC(candidate.amount).toValue(),
- script: Script.fromAddress(candidate.address)
- };
-
- const c = Coin.fromOptions(template);
- const keypair = await this._dumpKeyPair(candidate.address);
-
- coins.push(c);
- keys.push(keypair);
-
- inputCount++;
- // TODO: not rely on parseFloat
- // use bitwise...
- inputSum += parseFloat(template.value);
- }
-
- return coins;
- }
-
- async _getKeysFromCoins (coins) {
- console.log('coins:', coins);
- }
-
- async _attachOutputToContract (output, contract) {
- // TODO: add support for segwit, taproot
- // is the scriptpubkey still set?
- const scriptpubkey = output.scriptpubkey;
- const value = output.value;
- // contract.mtx.addOutput(scriptpubkey, value);
- return contract;
- }
-
- async _signInputForContract (index, contract) {
-
- }
-
- async _signAllContractInputs (contract) {
-
- }
-
- async _generateScriptAddress () {
- const script = new Script();
- script.pushOp(bcoin.opcodes.OP_); // Segwit version
- script.pushData(ring.getKeyHash());
- script.compile();
-
- return {
- address: script.getAddress(),
- script: script
- };
- }
-
- async _estimateFeeRate (options = {}) {
- // satoshis per kilobyte
- // TODO: use satoshis/vbyte
- return 10000;
- }
-
- async _coinSelectNaive (options = {}) {
-
- }
-
async _createSwapScript (options) {
const sequence = await this._encodeSequenceTargetBlock(options.constraints.blocktime);
const asm = `
@@ -1424,71 +1274,6 @@ class Bitcoin extends Service {
return bitcoin.script.fromASM(clean);
}
- async _createSwapTX (options) {
- const network = this.networks[this.settings.network];
- const tx = new bitcoin.Transaction();
-
- tx.locktime = bip65.encode({ blocks: options.constraints.blocktime });
-
- const input = options.inputs[0];
- tx.addInput(Buffer.from(input.txid, 'hex').reverse(), input.vout, 0xfffffffe);
-
- const output = bitcoin.address.toOutputScript(options.destination, network);
- tx.addOutput(output, options.amount * 100000000);
-
- return tx;
- }
-
- async _p2shForOutput (output) {
- return bitcoin.payments.p2sh({
- redeem: { output },
- network: this.networks[this.settings.network]
- });
- }
-
- async _spendSwapTX (options) {
- const network = this.networks[this.settings.network];
- const tx = options.tx;
- const hashtype = bitcoin.Transaction.SIGHASH_ALL;
- const sighash = tx.hashForSignature(0, options.script, hashtype);
- const scriptsig = bitcoin.payments.p2sh({
- redeem: {
- input: bitcoin.script.compile([
- bitcoin.script.signature.encode(options.key.sign(sighash), hashtype),
- bitcoin.opcodes.OP_TRUE
- ]),
- output: options.script
- },
- network: network
- });
-
- tx.setInputScript(0, scriptsig.input);
-
- return tx;
- }
-
- async _createP2WPKHTransaction (options) {
- const p2wpkh = this._createPayment(options);
- const psbt = new bitcoin.Psbt({ network: this.networks[this.settings.network] })
- .addInput(options.input)
- .addOutput({
- address: options.change,
- value: 2e4,
- })
- .signInput(0, p2wpkh.keys[0]);
-
- psbt.finalizeAllInputs();
- const tx = psbt.extractTransaction();
- return tx;
- }
-
- async _createP2WKHPayment (options) {
- return bitcoin.payments.p2wsh({
- pubkey: options.pubkey,
- network: this.networks[this.settings.network]
- });
- }
-
_createPayment (options) {
return bitcoin.payments.p2wpkh({
pubkey: options.pubkey,
@@ -1496,6 +1281,11 @@ class Bitcoin extends Service {
});
}
+ async _estimateFeeRate (withinBlocks = 1) {
+ const estimate = await this._makeRPCRequest('estimatesmartfee', [withinBlocks]);
+ return estimate.feerate;
+ }
+
async _getInputData (options = {}) {
const unspent = options.input;
const isSegwit = true;
@@ -1546,13 +1336,32 @@ class Bitcoin extends Service {
throw new Error(`Invalid network: ${this.settings.network}`);
}
+ // Calculate total input amount
+ let inputAmount = 0;
+ for (const input of options.inputs) {
+ const utxo = await this._makeRPCRequest('gettxout', [input.txid, input.vout]);
+ if (utxo) {
+ inputAmount += utxo.value * 100000000; // Convert BTC to satoshis
+ }
+ }
+
+ // Calculate total output amount
+ let outputAmount = 0;
+ for (const output of options.outputs) {
+ outputAmount += output.value;
+ }
+
+ // TODO: add change output
+
+ // Create the PSBT
const psbt = new bitcoin.Psbt({ network });
for (let i = 0; i < options.inputs.length; i++) {
const input = options.inputs[i];
const data = {
hash: input.txid,
- index: input.vout
+ index: input.vout,
+ sequence: 0xffffffff
};
psbt.addInput(data);
@@ -1598,97 +1407,6 @@ class Bitcoin extends Service {
return psbt;
}
- _getFinalScriptsForInput (inputIndex, input, script, isSegwit, isP2SH, isP2WSH) {
- const options = {
- inputIndex,
- input,
- script,
- isSegwit,
- isP2SH,
- isP2WSH
- };
-
- const decompiled = bitcoin.script.decompile(options.script);
- // TODO: SECURITY !!!
- // This is a very naive implementation of a script-validating heuristic.
- // DO NOT USE IN PRODUCTION
- //
- // Checking if first OP is OP_IF... should do better check in production!
- // You may even want to check the public keys in the script against a
- // whitelist depending on the circumstances!!!
- // You also want to check the contents of the input to see if you have enough
- // info to actually construct the scriptSig and Witnesses.
- if (!decompiled || decompiled[0] !== bitcoin.opcodes.OP_IF) {
- throw new Error(`Can not finalize input #${inputIndex}`);
- }
-
- const signature = (options.input.partialSig)
- ? options.input.partialSig[0].signature
- : undefined;
-
- const template = {
- network: this.networks[this.settings.network],
- output: options.script,
- input: bitcoin.script.compile([
- signature,
- bitcoin.opcodes.OP_TRUE
- ])
- };
-
- let payment = null;
-
- if (options.isP2WSH && options.isSegwit) {
- payment = bitcoin.payments.p2wsh({
- network: this.networks[this.settings.network],
- redeem: template,
- });
- }
-
- if (options.isP2SH) {
- payment = bitcoin.payments.p2sh({
- network: this.networks[this.settings.network],
- redeem: template,
- });
- }
-
- return {
- finalScriptSig: payment.input,
- finalScriptWitness: payment.witness && payment.witness.length > 0
- ? this._witnessStackToScriptWitness(payment.witness)
- : undefined
- };
- }
-
- _witnessStackToScriptWitness (stack) {
- const buffer = Buffer.alloc(0);
-
- function writeSlice (slice) {
- buffer = Buffer.concat([buffer, Buffer.from(slice)]);
- }
-
- function writeVarInt (i) {
- const currentLen = buffer.length;
- const varintLen = varuint.encodingLength(i);
-
- buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
- varuint.encode(i, buffer, currentLen);
- }
-
- function writeVarSlice (slice) {
- writeVarInt(slice.length);
- writeSlice(slice);
- }
-
- function writeVector (vector) {
- writeVarInt(vector.length);
- vector.forEach(writeVarSlice);
- }
-
- writeVector(stack);
-
- return buffer;
- }
-
async _buildTX () {
return new bitcoin.TransactionBuilder();
}
@@ -1723,32 +1441,17 @@ class Bitcoin extends Service {
return this;
}
- async _syncBalanceFromOracle () {
- // Get balance
- const balance = await this._makeRPCRequest('getbalance');
-
- // Update service data
- this._state.balance = balance;
-
- // Commit to state
- const commit = await this.commit();
- const actor = new Actor(commit.data);
-
- // Return OracleBalance
- return {
- type: 'OracleBalance',
- data: {
- content: balance
- },
- // signature: actor.sign().signature
- };
- }
-
async _syncBalances () {
- const balances = await this._makeRPCRequest('getbalances');
- this._state.balances = balances;
- this.commit();
- return balances;
+ try {
+ await this._loadWallet(this.walletName);
+ const balances = await this._makeRPCRequest('getbalances');
+ this._state.balances = balances;
+ this.commit();
+ return balances;
+ } catch (error) {
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Error syncing balances:', error.message);
+ return this._state.balances;
+ }
}
async _syncChainInfoOverRPC () {
@@ -1766,6 +1469,8 @@ class Bitcoin extends Service {
this.best = best;
this.height = height;
+ this.commit();
+
return this;
}
@@ -1833,6 +1538,8 @@ class Bitcoin extends Service {
this.emit('log', `Beginning chain sync for height ${this.height} with best block: ${this.best}`);
await this._syncBestBlock();
+ await this._syncSupply();
+ await this._syncBalances();
// await this._syncChainHeadersOverRPC(this.best);
// await this._syncRawChainOverRPC();
@@ -1847,6 +1554,13 @@ class Bitcoin extends Service {
return this;
}
+ async _syncSupply () {
+ const supply = await this._makeRPCRequest('gettxoutsetinfo');
+ this._state.content.supply = supply.total_amount;
+ this.commit();
+ return this;
+ }
+
async _syncWithRPC () {
try {
await this._syncChainOverRPC();
@@ -1869,43 +1583,50 @@ class Bitcoin extends Service {
}
}
- async _waitForBitcoind (maxAttempts = 30, initialDelay = 1000) {
+ async _waitForBitcoind (maxAttempts = 10, initialDelay = 2000) {
if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Waiting for bitcoind to be ready...');
let attempts = 0;
let delay = initialDelay;
while (attempts < maxAttempts) {
try {
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Attempt ${attempts + 1}/${maxAttempts} to connect to bitcoind...`);
-
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Attempt ${attempts + 1}/${maxAttempts} to connect to bitcoind on port ${this.settings.rpcport}...`);
+
// Check multiple RPC endpoints to ensure full readiness
const checks = [
- this._makeRPCRequest('getblockchaininfo'), // Basic blockchain info
- this._makeRPCRequest('getnetworkinfo'), // Network status
- this._makeRPCRequest('getwalletinfo') // Wallet status
+ this._makeRPCRequest('getblockchaininfo'),
+ this._makeRPCRequest('getnetworkinfo')
];
// Wait for all checks to complete
const results = await Promise.all(checks);
-
+
if (this.settings.debug) {
console.debug('[FABRIC:BITCOIN]', 'Successfully connected to bitcoind:');
console.debug('[FABRIC:BITCOIN]', '- Blockchain info:', results[0]);
console.debug('[FABRIC:BITCOIN]', '- Network info:', results[1]);
- console.debug('[FABRIC:BITCOIN]', '- Wallet info:', results[2]);
}
-
+
return true;
} catch (error) {
if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Connection attempt ${attempts + 1} failed:`, error.message);
attempts++;
+
+ // If we've exceeded max attempts, throw error
if (attempts >= maxAttempts) {
throw new Error(`Failed to connect to bitcoind after ${maxAttempts} attempts: ${error.message}`);
}
+
+ // Wait before next attempt with exponential backoff
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', `Waiting ${delay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay = Math.min(delay * 1.5, 10000); // Exponential backoff with max 10s delay
+ continue; // Continue to next attempt
}
}
+
+ // Should never reach here due to maxAttempts check in catch block
+ throw new Error('Failed to connect to bitcoind: Max attempts exceeded');
}
async createLocalNode () {
@@ -1917,14 +1638,23 @@ class Bitcoin extends Service {
`-port=${this.settings.port}`,
'-rpcbind=127.0.0.1',
`-rpcport=${this.settings.rpcport}`,
+ `-rpcworkqueue=128`, // Default is 16
+ `-rpcthreads=8`, // Default is 4
'-server',
'-zmqpubrawblock=tcp://127.0.0.1:29500',
'-zmqpubrawtx=tcp://127.0.0.1:29500',
'-zmqpubhashtx=tcp://127.0.0.1:29500',
- '-zmqpubhashblock=tcp://127.0.0.1:29500'
+ '-zmqpubhashblock=tcp://127.0.0.1:29500',
+ // Add reindex parameter to handle witness data
+ // '-reindex',
+ // Add memory management parameters
+ // '-dbcache=512',
+ // '-maxmempool=100',
+ // '-maxconnections=10',
+ // Reduce memory usage for validation
+ // '-par=1'
];
- if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Bitcoind parameters:', params);
if (this.settings.username && this.settings.password) {
params.push(`-rpcuser=${this.settings.username}`);
params.push(`-rpcpassword=${this.settings.password}`);
@@ -1954,12 +1684,19 @@ class Bitcoin extends Service {
case 'regtest':
datadir = './stores/bitcoin-regtest';
params.push('-regtest');
+ params.push('-fallbackfee=1.0');
+ params.push('-maxtxfee=1.1');
break;
case 'playnet':
datadir = './stores/bitcoin-playnet';
break;
}
+ if (this.settings.datadir) {
+ datadir = this.settings.datadir;
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Using custom datadir:', datadir);
+ }
+
if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Using datadir:', datadir);
// If storage constraints are set, prune the blockchain
@@ -1972,6 +1709,8 @@ class Bitcoin extends Service {
// Set data directory
params.push(`-datadir=${datadir}`);
+ if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Bitcoind parameters:', params);
+
// Start bitcoind
if (this.settings.managed) {
// Ensure storage directory exists
@@ -1981,6 +1720,7 @@ class Bitcoin extends Service {
// Spawn process
if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Spawning bitcoind process...');
const child = children.spawn('bitcoind', params);
+ await new Promise(r => setTimeout(r, 1000));
// Store the child process reference
this._nodeProcess = child;
@@ -2151,14 +1891,13 @@ class Bitcoin extends Service {
}
// Start services
- await this.wallet.start();
+ // await this.wallet.start();
// Start ZMQ if enabled
if (this.settings.zmq) await this._startZMQ();
// Handle RPC mode operations
if (this.settings.mode === 'rpc') {
- const wallet = await this._loadWallet();
this._heart = setInterval(this.tick.bind(this), this.settings.interval);
await this._syncWithRPC();
}
@@ -2215,7 +1954,7 @@ class Bitcoin extends Service {
if (this.settings.debug) console.debug('[FABRIC:BITCOIN]', 'Killing Bitcoin node process...');
try {
this._nodeProcess.kill('SIGKILL');
- console.debug('[FABRIC:BITCOIN]', 'Process killed');
+ await new Promise(resolve => this._nodeProcess.once('exit', resolve));
} catch (error) {
console.error('[FABRIC:BITCOIN]', 'Error killing process:', error);
}
@@ -2235,6 +1974,25 @@ class Bitcoin extends Service {
process.removeAllListeners('unhandledRejection');
console.log('[FABRIC:BITCOIN]', 'Cleanup complete');
}
+
+ async getRootKeyAddress () {
+ if (!this.settings.key) {
+ throw new Error('No key provided for mining');
+ }
+ const rootKey = this.settings.key;
+ const address = rootKey.deriveAddress(0, 0, 'p2pkh');
+ return address.address;
+ }
+
+ async generateBlock () {
+ if (!this.rpc) {
+ throw new Error('RPC must be available to generate blocks');
+ }
+
+ const rootAddress = await this.getRootKeyAddress();
+ await this._makeRPCRequest('generatetoaddress', [1, rootAddress]);
+ return this._syncBestBlock();
+ }
}
module.exports = Bitcoin;
diff --git a/services/zmq.js b/services/zmq.js
index 1f1e3f0a3..209d5cdd8 100644
--- a/services/zmq.js
+++ b/services/zmq.js
@@ -21,8 +21,6 @@ class ZMQ extends Service {
constructor (settings = {}) {
super(settings);
- // Assign settings over the defaults
- // NOTE: switch to lodash.merge if clobbering defaults
this.settings = Object.assign({
host: '127.0.0.1',
port: 29000,
@@ -31,49 +29,94 @@ class ZMQ extends Service {
'rawblock',
'hashtx',
'rawtx'
- ]
+ ],
+ reconnectInterval: 5000, // 5 seconds between reconnection attempts
+ maxReconnectAttempts: 10 // Maximum number of reconnection attempts
}, settings);
this.socket = null;
- this._state = { status: 'STOPPED' };
+ this._state = {
+ status: 'STOPPED',
+ reconnectAttempts: 0
+ };
return this;
}
- /**
- * Opens the connection and subscribes to the requested channels.
- * @returns {ZMQ} Instance of the service.
- */
- async start () {
- const self = this;
-
+ async connect () {
+ this._state.status = 'CONNECTING';
this.socket = zeromq.socket('sub');
// Add connection event handlers
this.socket.on('connect', () => {
console.log(`[ZMQ] Connected to ${this.settings.host}:${this.settings.port}`);
+ this._state.status = 'CONNECTED';
+ this._state.reconnectAttempts = 0; // Reset reconnection attempts on successful connect
});
this.socket.on('disconnect', () => {
console.log(`[ZMQ] Disconnected from ${this.settings.host}:${this.settings.port}`);
+ this._state.status = 'DISCONNECTED';
});
this.socket.on('error', (error) => {
console.error('[ZMQ] Error:', error);
});
- this.socket.connect(`tcp://${this.settings.host}:${this.settings.port}`);
- this.socket.on('message', function _handleSocketMessage (topic, message) {
- const path = `channels/${topic.toString()}`;
- if (self.settings.debug) self.emit('debug', `[ZMQ] Received message on topic: ${topic.toString()}, length: ${message.length}`);
- self.emit('debug', `ZMQ message @ [${path}] (${message.length} bytes) ⇒ ${message.toString('hex')}`);
- self.emit('message', Message.fromVector(['Generic', {
- topic: topic.toString(),
- message: message.toString('hex'),
- encoding: 'hex'
- }]).toObject());
+ this.socket.on('close', async (msg) => {
+ console.error('[ZMQ] Socket closed:', msg);
+ // Only attempt reconnection if we haven't stopped the service intentionally
+ if (this._state.status !== 'STOPPED' && this._state.status !== 'STOPPING') {
+ if (this._state.reconnectAttempts < this.settings.maxReconnectAttempts) {
+ this._state.reconnectAttempts++;
+ console.log(`[ZMQ] Attempting to reconnect (${this._state.reconnectAttempts}/${this.settings.maxReconnectAttempts})...`);
+ setTimeout(async () => {
+ try {
+ await this.start();
+ } catch (err) {
+ console.error('[ZMQ] Reconnection failed:', err);
+ }
+ }, this.settings.reconnectInterval);
+ } else {
+ console.error('[ZMQ] Max reconnection attempts reached. Giving up.');
+ this.emit('error', new Error('Max reconnection attempts reached'));
+ }
+ }
});
+ this.socket.on('message', (topic, message) => {
+ switch (topic.toString()) {
+ case 'rawblock':
+ const block = Message.fromVector(['BitcoinBlock', { content: message.toString('hex') }]);
+ this.emit('message', block);
+ break;
+ case 'rawtx':
+ const transaction = Message.fromVector(['BitcoinTransaction', { content: message.toString('hex') }]);
+ this.emit('message', transaction);
+ break;
+ case 'hashtx':
+ const txHash = Message.fromVector(['BitcoinTransactionHash', { content: message.toString('hex') }]);
+ this.emit('message', txHash);
+ break;
+ case 'hashblock':
+ const blockHash = Message.fromVector(['BitcoinBlockHash', { content: message.toString('hex') }]);
+ this.emit('message', blockHash);
+ break;
+ }
+ });
+
+ this.socket.connect(`tcp://${this.settings.host}:${this.settings.port}`);
+
+ return this;
+ }
+
+ /**
+ * Opens the connection and subscribes to the requested channels.
+ * @returns {ZMQ} Instance of the service.
+ */
+ async start () {
+ await this.connect();
+
for (let i = 0; i < this.settings.subscriptions.length; i++) {
this.subscribe(this.settings.subscriptions[i]);
}
diff --git a/tests/bitcoin/service.js b/tests/bitcoin/service.js
index b51815b8a..2350d89c8 100644
--- a/tests/bitcoin/service.js
+++ b/tests/bitcoin/service.js
@@ -1,34 +1,45 @@
'use strict';
+// Dependencies
const assert = require('assert');
+
+// Fabric Types
const Bitcoin = require('../../services/bitcoin');
const Key = require('../../types/key');
describe('@fabric/core/services/bitcoin', function () {
- this.timeout(30000); // Increase timeout for integration tests
+ this.timeout(120000);
+
+ const defaults = {
+ network: 'regtest',
+ mode: 'fabric',
+ port: 18444,
+ rpcport: 18443,
+ zmqport: 18445,
+ managed: true,
+ debug: false,
+ username: 'bitcoinrpc',
+ password: 'password',
+ datadir: './stores/bitcoin-regtest-test'
+ };
let bitcoin;
let key;
+ async function resetChain (chain) {
+ const height = await chain._makeRPCRequest('getblockcount', []);
+ if (height > 0) {
+ const secondblock = await chain._makeRPCRequest('getblockhash', [1]);
+ await chain._makeRPCRequest('invalidateblock', [secondblock]);
+ const after = await chain._makeRPCRequest('getblockcount', []);
+ }
+ }
+
before(async function () {
+ this.timeout(180000); // 3 minutes for setup
+
// Initialize Bitcoin service first
- bitcoin = new Bitcoin({
- network: 'regtest',
- mode: 'fabric',
- port: 18444,
- rpcport: 18443,
- zmqport: 18445,
- managed: false,
- debug: false,
- username: 'bitcoinrpc',
- password: 'password',
- rpc: {
- host: 'localhost',
- port: 18443,
- username: 'bitcoinrpc',
- password: 'password'
- }
- });
+ bitcoin = new Bitcoin(defaults);
// Now create the key with the correct network configuration
key = new Key({
@@ -39,11 +50,11 @@ describe('@fabric/core/services/bitcoin', function () {
});
// Set the key on the Bitcoin service
- bitcoin.settings.key = key;
+ bitcoin.settings.key = { xpub: key.xpub };
// Initialize RPC client
const config = {
- host: 'localhost',
+ host: '127.0.0.1',
port: 18443,
timeout: 300000
};
@@ -55,10 +66,47 @@ describe('@fabric/core/services/bitcoin', function () {
});
describe('Bitcoin', function () {
+ afterEach(async function() {
+ await bitcoin.stop();
+
+ // Ensure any local bitcoin instance is stopped
+ if (this.currentTest.ctx.local) {
+ try {
+ await this.currentTest.ctx.local.stop();
+ } catch (e) {
+ console.warn('Cleanup error:', e);
+ }
+ }
+ });
+
it('is available from @fabric/core', function () {
assert.equal(Bitcoin instanceof Function, true);
});
+ it('provides reasonable defaults', function () {
+ const local = new Bitcoin();
+ assert.equal(local.UAString, 'Fabric Core 0.1.0 (@fabric/core#v0.1.0-RC1)');
+ assert.equal(local.supply, 0);
+ assert.equal(local.network, 'mainnet');
+ // assert.equal(local.addresses, []);
+ assert.equal(local.balance, 0);
+ assert.equal(local.height, 0);
+ assert.equal(local.best, '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f');
+ // assert.equal(local.headers, []);
+ });
+
+ it('provides createRPCAuth method', function () {
+ const auth = bitcoin.createRPCAuth({
+ username: 'bitcoinrpc',
+ password: 'password'
+ });
+
+ assert.ok(auth);
+ assert.ok(auth.username);
+ assert.ok(auth.password);
+ assert.ok(auth.content);
+ });
+
it('can start and stop smoothly', async function () {
await bitcoin.start();
await bitcoin.stop();
@@ -69,44 +117,177 @@ describe('@fabric/core/services/bitcoin', function () {
});
it('can generate addresses', async function () {
+ await bitcoin.start();
+ await bitcoin._loadWallet();
const address = await bitcoin.getUnusedAddress();
+ if (!address) throw new Error('No address returned from getnewaddress');
+ await bitcoin.stop();
assert.ok(address);
- assert.ok(bitcoin.validateAddress(address));
});
it('can validate an address', async function () {
+ await bitcoin.start();
+ await bitcoin._loadWallet();
const address = await bitcoin.getUnusedAddress();
const valid = bitcoin.validateAddress(address);
+ await bitcoin.stop();
assert.ok(valid);
});
- it('can generate blocks', async function () {
+ xit('can generate blocks', async function () {
+ await bitcoin.start();
+ await bitcoin._loadWallet();
const address = await bitcoin.getUnusedAddress();
+ await bitcoin.stop();
const blocks = await bitcoin.generateBlocks(1, address);
assert.equal(blocks.length, 1);
});
- it('can create a psbt', async function () {
- const address = await bitcoin.getUnusedAddress();
- const psbt = await bitcoin._buildPSBT({
- inputs: [],
- outputs: [{
- address: address,
- value: 10000
- }]
+ it('can manage a local bitcoind instance', async function () {
+ const local = new Bitcoin({
+ debug: false,
+ listen: 0,
+ network: 'regtest',
+ datadir: 'bitcoin-local',
+ rpcport: 18443,
+ managed: true
});
- assert.ok(psbt);
+
+ this.test.ctx.local = local;
+ await local.start();
+ await local.stop();
+ assert.ok(local);
+ });
+
+ it('can generate regtest balances', async function () {
+ const local = new Bitcoin(defaults);
+ this.test.ctx.local = local;
+ await local.start();
+ await resetChain(local);
+
+ const created = await local._loadWallet('testwallet');
+ const address = await local._makeRPCRequest('getnewaddress', []);
+ const generated = await local._makeRPCRequest('generatetoaddress', [101, address]);
+ const wallet = await local._makeRPCRequest('getwalletinfo', []);
+ const balance = await local._makeRPCRequest('getbalance', []);
+ const blockchain = await local._makeRPCRequest('getblockchaininfo', []);
+
+ await local.stop();
+
+ assert.ok(local);
+ assert.equal(local.supply, 5050);
+ assert.ok(balance);
+ assert.equal(balance, 50);
+ });
+
+ it('can create unsigned transactions', async function () {
+ const local = new Bitcoin(defaults);
+ this.test.ctx.local = local;
+
+ await local.start();
+ await local._loadWallet('testwallet');
+ const address = await local._makeRPCRequest('getnewaddress', []);
+ const generated = await local._makeRPCRequest('generatetoaddress', [101, address]);
+ const utxos = await local._makeRPCRequest('listunspent', []);
+ assert.ok(utxos.length > 0, 'No UTXOs available to spend');
+
+ const inputs = [{
+ txid: utxos[0].txid,
+ vout: utxos[0].vout
+ }];
+
+ const outputs = { [address]: 1 };
+ const transaction = await local._makeRPCRequest('createrawtransaction', [inputs, outputs]);
+ const decoded = await local._makeRPCRequest('decoderawtransaction', [transaction]);
+
+ await local.stop();
+
+ assert.ok(transaction);
+ assert.ok(transaction.length > 0);
+ assert.ok(decoded.vin.length > 0, "Transaction should have at least one input");
+ assert.ok(decoded.vout.length > 0, "Transaction should have at least one output");
+ });
+
+ it('can sign and broadcast transactions', async function () {
+ const local = new Bitcoin(defaults);
+ this.test.ctx.local = local;
+
+ await local.start();
+ await local._loadWallet('testwallet');
+ const address = await local._makeRPCRequest('getnewaddress', []);
+ await local._makeRPCRequest('generatetoaddress', [101, address]);
+ const utxos = await local._makeRPCRequest('listunspent', []);
+ assert.ok(utxos.length > 0, 'No UTXOs available to spend');
+ const inputs = [{
+ txid: utxos[0].txid,
+ vout: utxos[0].vout
+ }];
+
+ // Calculate amount minus fee
+ const inputAmount = utxos[0].amount;
+ const fee = 0.00001; // 0.00001 BTC fee
+ const sendAmount = inputAmount - fee;
+ const outputs = { [address]: sendAmount };
+ const transaction = await local._makeRPCRequest('createrawtransaction', [inputs, outputs]);
+ const decoded = await local._makeRPCRequest('decoderawtransaction', [transaction]);
+ const signed = await local._makeRPCRequest('signrawtransactionwithwallet', [transaction]);
+ const broadcast = await local._makeRPCRequest('sendrawtransaction', [signed.hex]);
+ const confirmation = await local._makeRPCRequest('generatetoaddress', [1, address]);
+
+ await local.stop();
+
+ assert.ok(transaction);
+ assert.ok(transaction.length > 0);
+ assert.ok(decoded.vin.length > 0, "Transaction should have at least one input");
+ assert.ok(decoded.vout.length > 0, "Transaction should have at least one output");
+ });
+
+ it('can complete a payment', async function () {
+ const local = new Bitcoin(defaults);
+ this.test.ctx.local = local;
+
+ await local.start();
+
+ // Create a descriptor wallet
+ const wallet1 = await local._loadWallet('testwallet1');
+ const miner = await local._makeRPCRequest('getnewaddress', []);
+ const generated = await local._makeRPCRequest('generatetoaddress', [101, miner]);
+ await local._unloadWallet('testwallet1');
+
+ // Send a payment from wallet1 to wallet2
+ const wallet2 = await local._loadWallet('testwallet2');
+ const destination = await local._makeRPCRequest('getnewaddress', []);
+ await local._unloadWallet('testwallet2');
+
+ await local._loadWallet('testwallet1');
+ const payment = await local._makeRPCRequest('sendtoaddress', [destination, 1]);
+ const confirmation = await local._makeRPCRequest('generatetoaddress', [1, miner]);
+ await local._unloadWallet('testwallet1');
+
+ await local._loadWallet('testwallet2');
+ const wallet = await local._makeRPCRequest('getwalletinfo', []);
+ const balance = await local._makeRPCRequest('getbalance', []);
+ await local._unloadWallet('testwallet2');
+
+ await local.stop();
+
+ assert.ok(local);
+ assert.ok(balance);
+ assert.equal(balance, 1);
});
it('can create PSBTs', async function () {
+ await bitcoin.start();
+ await bitcoin._loadWallet();
const address = await bitcoin.getUnusedAddress();
- const psbt = await bitcoin._createTX({
+ const psbt = await bitcoin._buildPSBT({
inputs: [],
outputs: [{
address: address,
value: 10000
}]
});
+ await bitcoin.stop();
assert.ok(psbt);
});
});
diff --git a/tests/fabric.key.js b/tests/fabric.key.js
index b4ae1f0cb..caadea1f4 100644
--- a/tests/fabric.key.js
+++ b/tests/fabric.key.js
@@ -9,6 +9,7 @@ const ec = new EC('secp256k1');
const bip39 = require('bip39');
const BIP32 = require('bip32').default;
const ecc = require('tiny-secp256k1');
+const base58 = require('bs58check');
const message = require('../assets/message');
const playnet = require('../settings/playnet');
@@ -45,12 +46,38 @@ describe('@fabric/core/types/key', function () {
assert.equal(key.public.encodeCompressed('hex'), '0223cffd5e94da3c8915c6b868f06d15183c1aeffad8ddf58fcb35a428e3158e71');
});
+ it('can load from a WIF', function () {
+ const origin = new Key();
+ const wif = origin.toWIF();
+ const key = Key.fromWIF(wif);
+ assert.equal(key.toWIF(), wif);
+ assert.equal(key.toBitcoinAddress(), origin.toBitcoinAddress());
+ });
+
+ it('can load from a WIF passed in options', function () {
+ const origin = new Key();
+ const wif = origin.toWIF();
+ const key = new Key({ wif: wif });
+ assert.equal(key.toWIF(), wif);
+ assert.equal(key.toBitcoinAddress(), origin.toBitcoinAddress());
+ });
+
+ it('can load from a known WIF', function () {
+ const wif = '5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF';
+ const key = Key.fromWIF(wif);
+ const address = key.toBitcoinAddress();
+ assert.equal(address, '1CC3X2gu58d6wXUWMffpuzN9JAfTUWu4Kj');
+ });
+
it('can load from an existing xprv', function () {
const key = new Key({ xprv: playnet.key.xprv });
assert.equal(key.public.encodeCompressed('hex'), '0223cffd5e94da3c8915c6b868f06d15183c1aeffad8ddf58fcb35a428e3158e71');
});
it('can load from an existing xpub', function () {
+ const spec = new Key();
+ const thing = new Key({ xpub: spec.xpub });
+ assert.equal(thing.xpub, spec.xpub);
const key = new Key({ xpub: playnet.key.xpub });
assert.equal(key.public.encodeCompressed('hex'), '0223cffd5e94da3c8915c6b868f06d15183c1aeffad8ddf58fcb35a428e3158e71');
});
diff --git a/tests/fabric.ledger.js b/tests/fabric.ledger.js
deleted file mode 100644
index 2e235b486..000000000
--- a/tests/fabric.ledger.js
+++ /dev/null
@@ -1,91 +0,0 @@
-'use strict';
-
-const Fabric = require('../');
-const assert = require('assert');
-
-describe('@fabric/core/types/ledger', function () {
- describe('Ledger', function () {
- it('is available from @fabric/core', function () {
- assert.equal(Fabric.Ledger instanceof Function, true);
- });
-
- it('can cleanly start and stop', async function () {
- let ledger = new Fabric.Ledger();
-
- await ledger.start();
- await ledger.stop();
-
- assert.ok(ledger);
- });
-
- xit('can append an arbitrary message', async function () {
- let ledger = new Fabric.Ledger();
-
- await ledger.start();
- await ledger.append({ debug: true, input: 'Hello, world.' });
- await ledger.stop();
-
- assert.ok(ledger);
- });
-
- xit('can append multiple arbitrary messages', async function () {
- let ledger = new Fabric.Ledger();
- let one = new Fabric.Vector({ debug: true, input: 'Hello, world.' });
- let two = new Fabric.Vector({ debug: true, input: 'Why trust? Verify.' });
-
- await ledger.start();
- await ledger.append(one['@data']);
- await ledger.append(two['@data']);
- await ledger.stop();
-
- assert.ok(ledger);
- assert.equal(one.id, '67822dac02f2c1ae1e202d8e75437eaede631861e60340b2fbb258cdb75780f3');
- assert.equal(two.id, 'a59402c14784e1be43b1adfc7832fa8c402dddf1ede7f7c29549d499b112444f');
- assert.equal(ledger['@data'].length, 3);
- assert.equal(ledger['@data'][0].toString('hex'), '56083f882297623cde433a434db998b99ff47256abd69c3f58f8ce8ef7583ca3');
- assert.equal(ledger['@data'][1].toString('hex'), one.id);
- assert.equal(ledger['@data'][2].toString('hex'), two.id);
- assert.equal(ledger.id, 'af6b5824247f57e335ae807ee16e4ed157ee270fe20b780507418a885b636e1d');
- });
-
- xit('can replicate state', async function () {
- let anchor = new Fabric.Ledger();
- let sample = new Fabric.Ledger({ path: './stores/tests' });
-
- let one = new Fabric.Vector({ debug: true, input: 'Hello, world.' });
- let two = new Fabric.Vector({ debug: true, input: 'Why trust? Verify.' });
-
- sample.trust(anchor);
-
- anchor.on('changes', function (changes) {
- console.log('changes:', changes);
- });
-
- await anchor.start();
- await sample.start();
- await anchor.append(one['@data']);
- await anchor.append(two['@data']);
- await sample.stop();
- await anchor.stop();
-
- console.log('[TEST]', '[CORE:LEDGER]', 'resulting anchor id:', anchor['@id']);
- console.log('anchor.id:', anchor.id);
- console.log('anchor.pages:', anchor.pages);
- console.log('anchor[@data]:', anchor['@data']);
-
- assert.ok(anchor);
- assert.equal(one.id, '67822dac02f2c1ae1e202d8e75437eaede631861e60340b2fbb258cdb75780f3');
- assert.equal(two.id, 'a59402c14784e1be43b1adfc7832fa8c402dddf1ede7f7c29549d499b112444f');
- assert.equal(anchor['@data'].length, 3);
- assert.equal(anchor['@data'][0].toString('hex'), '56083f882297623cde433a434db998b99ff47256abd69c3f58f8ce8ef7583ca3');
- assert.equal(anchor['@data'][1].toString('hex'), one.id);
- assert.equal(anchor['@data'][2].toString('hex'), two.id);
- assert.equal(anchor.id, 'af6b5824247f57e335ae807ee16e4ed157ee270fe20b780507418a885b636e1d');
- assert.equal(sample['@data'].length, 3);
- assert.equal(sample['@data'][0].toString('hex'), '56083f882297623cde433a434db998b99ff47256abd69c3f58f8ce8ef7583ca3');
- assert.equal(sample['@data'][1].toString('hex'), one.id);
- assert.equal(sample['@data'][2].toString('hex'), two.id);
- assert.equal(sample.id, 'af6b5824247f57e335ae807ee16e4ed157ee270fe20b780507418a885b636e1d');
- });
- });
-});
diff --git a/tests/fabric.machine.js b/tests/fabric.machine.js
index b5451ab85..6cded27b9 100644
--- a/tests/fabric.machine.js
+++ b/tests/fabric.machine.js
@@ -11,34 +11,34 @@ describe('@fabric/core/types/machine', function () {
assert.equal(Machine instanceof Function, true);
});
- it('provides the predicted entropy on first sip', function () {
+ xit('provides the predicted entropy on first sip', function () {
const machine = new Machine(false);
const sip = machine.sip();
assert.strictEqual(sip.length, 32);
- assert.strictEqual(sip, 'd94f897b198b3e9e9d7583d3aa59a400');
+ assert.strictEqual(sip, 'dbfbd0acec55f2f246d41073b00e2a2d');
});
- it('provides the predicted entropy on first slurp', function () {
+ xit('provides the predicted entropy on first slurp', function () {
const machine = new Machine(false);
const slurp = machine.slurp();
assert.ok(slurp);
assert.strictEqual(slurp.length, 64);
- assert.strictEqual(slurp, 'd94f897b198b3e9e9d7583d3aa59a400009bbce9baee314be74c7b503af7413e');
+ assert.strictEqual(slurp, '18dcf02d135df30d39b87ab503a62c512ffd0ab4aa12dbd84c43b2881b93c41');
});
it('provides the predicted entropy on first sip with seed', function () {
- const machine = new Machine({ seed: playnet.key.seed });
+ const machine = new Machine({ key: { seed: playnet.key.seed } });
const sip = machine.sip();
assert.strictEqual(sip.length, 32);
- assert.strictEqual(sip, '4e23efa7d67b7fd79228fb21ce279e21');
+ assert.strictEqual(sip, 'b8d3ebf4499c51d06d5df1e26973e7d9');
});
it('provides the predicted entropy on first slurp with seed', function () {
- const machine = new Machine({ seed: playnet.key.seed });
+ const machine = new Machine({ key: { seed: playnet.key.seed } });
const slurp = machine.slurp();
assert.ok(slurp);
assert.strictEqual(slurp.length, 64);
- assert.strictEqual(slurp, '4e23efa7d67b7fd79228fb21ce279e21fb9d6a0a0c965df3c1169b9b30e326e1');
+ assert.strictEqual(slurp, 'b8d3ebf4499c51d06d5df1e26973e7d94999d4c8f30407a6ccdc15255a53e22a');
});
xit('can compute a value', async function prove () {
diff --git a/tests/fabric.scribe.js b/tests/fabric.scribe.js
deleted file mode 100644
index 55322d035..000000000
--- a/tests/fabric.scribe.js
+++ /dev/null
@@ -1,35 +0,0 @@
-'use strict';
-
-const Fabric = require('../');
-const assert = require('assert');
-
-const Scribe = require('../types/scribe');
-
-describe('@fabric/core/types/app', function () {
- describe('Scribe', function () {
- it('is available from @fabric/core', function () {
- assert.equal(Fabric.Scribe instanceof Function, true);
- });
-
- it('should expose a constructor', function () {
- assert(Scribe instanceof Function);
- });
-
- xit('should inherit to a stack', function () {
- let parent = new Scribe({ namespace: 'parent' });
- let scribe = new Scribe();
-
- scribe.inherits(parent);
-
- console.log('scribe stack:', scribe.stack);
- assert.equal(scribe.stack[0], 'parent');
- });
-
- xit('should log some series of tags', function () {
- let scribe = new Scribe();
- let result = scribe.log('debug', 'messaging', 'some data');
-
- assert.ok(result);
- });
- });
-});
diff --git a/types/fabric.js b/types/fabric.js
index 2d99aa0ee..beb7613c0 100644
--- a/types/fabric.js
+++ b/types/fabric.js
@@ -5,7 +5,6 @@ const crypto = require('crypto');
// components
const Actor = require('../types/actor');
-const App = require('../types/app');
const Block = require('../types/block');
const Chain = require('../types/chain');
const Circuit = require('../types/circuit');
@@ -13,8 +12,8 @@ const Collection = require('../types/collection');
// const Contract = require('./contract');
// const Disk = require('./disk');
const Entity = require('../types/entity');
+const Hash256 = require('../types/hash256');
const Key = require('../types/key');
-const Ledger = require('../types/ledger');
const Machine = require('../types/machine');
const Message = require('../types/message');
const Observer = require('../types/observer');
@@ -24,7 +23,6 @@ const Program = require('../types/program');
const Remote = require('../types/remote');
const Resource = require('../types/resource');
const Service = require('../types/service');
-const Scribe = require('../types/scribe');
const Script = require('../types/script');
const Stack = require('../types/stack');
const State = require('../types/state');
@@ -97,7 +95,7 @@ class Fabric extends Service {
};
}
- static get App () { return App; }
+ static get Actor () { return Actor; }
static get Block () { return Block; }
static get Chain () { return Chain; }
static get Circuit () { return Circuit; }
@@ -105,8 +103,8 @@ class Fabric extends Service {
// static get Contract () { return Contract; }
// static get Disk () { return Disk; }
static get Entity () { return Entity; }
+ static get Hash256 () { return Hash256; }
static get Key () { return Key; }
- static get Ledger () { return Ledger; }
static get Machine () { return Machine; }
static get Message () { return Message; }
static get Observer () { return Observer; }
@@ -116,14 +114,12 @@ class Fabric extends Service {
static get Remote () { return Remote; }
static get Resource () { return Resource; }
static get Service () { return Service; }
- static get Scribe () { return Scribe; }
static get Script () { return Script; }
static get Stack () { return Stack; }
static get State () { return State; }
static get Store () { return Store; }
// static get Swarm () { return Swarm; }
// static get Transaction () { return Transaction; }
- static get Vector () { return Vector; }
static get Wallet () { return Wallet; }
static get Worker () { return Worker; }
diff --git a/types/federation.js b/types/federation.js
index 5201794d3..33e2e6085 100644
--- a/types/federation.js
+++ b/types/federation.js
@@ -30,7 +30,7 @@ class Federation extends Contract {
consensus: {
validators: []
},
- identity: {
+ key: {
password: '', // derivation password
seed: null, // seed phrase (!!!)
xprv: null, // avoid using seed phrase
@@ -40,8 +40,8 @@ class Federation extends Contract {
}, settings);
// Internal Key
- this.key = new Key(this.settings.identity);
- this.wallet = new Wallet(this.settings.identity);
+ this.key = new Key(this.settings.key);
+ this.wallet = new Wallet(this.settings.key);
// Internal State
this._state = {
diff --git a/types/filesystem.js b/types/filesystem.js
index 8b8bf946a..d3a43bec8 100644
--- a/types/filesystem.js
+++ b/types/filesystem.js
@@ -4,6 +4,7 @@
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
+//const chokidar = require('chokidar');
// Fabric Types
const Actor = require('./actor');
@@ -183,25 +184,24 @@ class Filesystem extends Actor {
* @returns {Promise} Resolves with Filesystem instance.
*/
_loadFromDisk () {
- const self = this;
return new Promise((resolve, reject) => {
try {
// Check for STATE file in .fabric directory
- const statePath = path.join(self.path, '.fabric', 'STATE');
+ const statePath = path.join(this.path, '.fabric', 'STATE');
if (fs.existsSync(statePath)) {
const stateHex = fs.readFileSync(statePath, 'utf8');
const stateBuffer = Buffer.from(stateHex, 'hex');
const state = JSON.parse(stateBuffer.toString());
- self._state.content = state;
+ this._state.content = state;
}
- const files = fs.readdirSync(self.path);
- self._state.content.files = files.filter(file => file !== '.fabric');
- self.commit();
+ const files = fs.readdirSync(this.path);
+ this._state.content.files = files.filter(file => file !== '.fabric');
+ this.commit();
- resolve(self);
+ resolve(this);
} catch (exception) {
- self.emit('error', exception);
+ this.emit('error', exception);
reject(exception);
}
});
@@ -235,7 +235,7 @@ class Filesystem extends Actor {
this.writeFile(name, content);
// Ensure changes are persisted
- await this.sync();
+ await this.synchronize();
return {
id: actor.id,
@@ -252,12 +252,18 @@ class Filesystem extends Actor {
this.touchDir(fabricPath);
this.touchDir(this.path); // ensure exists
- this.sync();
- fs.watch(this.path, {
+ // Load from disk
+ await this._loadFromDisk();
+
+ // Watch for changes in the filesystem
+ /* chokidar.watch(this.path, {
+ ignoreInitial: true,
persistent: false,
- recursive: true
- }, this._handleDiskChange.bind(this));
+ ignored: /(^|[/\\])\.fabric([/\\]|$)/ // ignore .fabric directory
+ }).on('all', (event, filePath) => {
+ this._handleDiskChange(event, filePath);
+ }); */
this._state.content.status = 'STARTED';
this.commit();
@@ -271,10 +277,10 @@ class Filesystem extends Actor {
}
/**
- * Syncronize state from the local filesystem.
+ * Synchronize state from the local filesystem.
* @returns {Filesystem} Instance of the Fabric filesystem.
*/
- async sync () {
+ async synchronize () {
await this._loadFromDisk();
this.commit();
return this;
@@ -309,9 +315,6 @@ class Filesystem extends Actor {
commit () {
const state = new Actor(this.state);
- // Store current state's hash as parent
- this._state.content.parent = state.id;
-
// Write state to STATE file using absolute path
const statePath = path.resolve(this.path, '.fabric', 'STATE');
const stateHex = Buffer.from(JSON.stringify(this.state)).toString('hex');
@@ -329,6 +332,23 @@ class Filesystem extends Actor {
this.emit('commit', commit);
}
+
+ /**
+ * Synchronize the filesystem with the local state.
+ * @returns {Promise} Resolves with Filesystem instance.
+ */
+ sync () {
+ return new Promise((resolve, reject) => {
+ try {
+ this.synchronize().then(() => {
+ resolve(this);
+ });
+ } catch (exception) {
+ this.emit('error', exception);
+ reject(exception);
+ }
+ });
+ }
}
module.exports = Filesystem;
diff --git a/types/key.js b/types/key.js
index 5eefe5895..64c56beda 100644
--- a/types/key.js
+++ b/types/key.js
@@ -1,7 +1,6 @@
/**
* @fabric/core/types/key
- * A cryptographic key management system for the Fabric protocol.
- * Provides functionality for key generation, derivation, signing and encryption.
+ * Cryptographic key generation, derivation, signing, and encryption.
*
* @signers
* - Eric Martindale