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 @@ -30,6 +29,7 @@ const BN = require('bn.js'); const EC = require('elliptic').ec; const ec = new EC('secp256k1'); const ecc = require('tiny-secp256k1'); +const base58 = require('bs58check'); const payments = require('bitcoinjs-lib/src/payments'); // Fabric Dependencies @@ -57,6 +57,7 @@ class Key extends EventEmitter { * @param {String} [settings.seed] Mnemonic seed for initializing the key. * @param {String} [settings.public] Public key in hex. * @param {String} [settings.private] Private key in hex. + * @param {String} [settings.wif] WIF-encoded private key. * @param {String} [settings.purpose=44] Constrains derivations to this space. */ constructor (input = {}) { @@ -72,6 +73,7 @@ class Key extends EventEmitter { prefix: '00', public: null, private: null, + wif: null, purpose: 44, account: 0, bits: 256, @@ -134,10 +136,14 @@ class Key extends EventEmitter { this.master = null; this.private = null; this.public = null; + this._state = null; // Initialize as null to defer state updates - // TODO: design state machine for input (configuration) - if (this.settings.seed) { + if (this.settings.mnemonic) { + this._mode = 'FROM_MNEMONIC'; + } else if (this.settings.seed) { this._mode = 'FROM_SEED'; + } else if (this.settings.wif) { + this._mode = 'FROM_WIF'; } else if (this.settings.private) { this._mode = 'FROM_PRIVATE_KEY'; } else if (this.settings.xprv) { @@ -150,10 +156,13 @@ class Key extends EventEmitter { this._mode = 'FROM_RANDOM'; } + let seed = null; + let root = null; + switch (this._mode) { - case 'FROM_SEED': - const seed = bip39.mnemonicToSeedSync(this.settings.seed, this.settings.passphrase); - const root = this.bip32.fromSeed(seed); + case 'FROM_MNEMONIC': + seed = bip39.mnemonicToSeedSync(this.settings.mnemonic, this.settings.passphrase); + root = this.bip32.fromSeed(seed); this.seed = this.settings.seed; this.xprv = root.toBase58(); this.xpub = root.neutered().toBase58(); @@ -161,6 +170,31 @@ class Key extends EventEmitter { this.keypair = ec.keyFromPrivate(root.privateKey); this.status = 'seeded'; break; + case 'FROM_SEED': + // TODO: allow setting of raw seed (deprecates passing a mnemonic in the `seed` property) + seed = bip39.mnemonicToSeedSync(this.settings.seed, this.settings.passphrase); + root = this.bip32.fromSeed(seed); + this.seed = this.settings.seed; + this.xprv = root.toBase58(); + this.xpub = root.neutered().toBase58(); + this.master = root; + this.keypair = ec.keyFromPrivate(root.privateKey); + break; + case 'FROM_WIF': + const decoded = base58.decode(this.settings.wif); + const version = decoded[0]; + const privateKey = decoded.slice(1, 33); + const isCompressed = decoded.length === 34 && decoded[33] === 0x01; + this.keypair = ec.keyFromPrivate(privateKey); + if (!isCompressed) { + const pub = this.keypair.getPublic(); + pub.compressed = false; + // Force the public key to be uncompressed + this.public = pub; + } else { + this.public = this.keypair.getPublic(true); + } + break; case 'FROM_XPRV': this.master = this.bip32.fromBase58(this.settings.xprv); this.xprv = this.master.toBase58(); @@ -168,8 +202,9 @@ class Key extends EventEmitter { this.keypair = ec.keyFromPrivate(this.master.privateKey); break; case 'FROM_XPUB': - const xpub = this.bip32.fromBase58(this.settings.xpub); - this.keypair = ec.keyFromPublic(xpub.publicKey); + this.master = this.bip32.fromBase58(this.settings.xpub); + this.xpub = this.master.neutered().toBase58(); + this.keypair = ec.keyFromPublic(this.master.publicKey); break; case 'FROM_PRIVATE_KEY': // Key is private @@ -182,19 +217,18 @@ class Key extends EventEmitter { this.keypair = ec.keyFromPublic((pubkey instanceof Buffer) ? pubkey : Buffer.from(pubkey, 'hex')); break; case 'FROM_RANDOM': - const mnemonic = bip39.generateMnemonic(); - const interim = bip39.mnemonicToSeedSync(mnemonic); + this.mnemonic = bip39.generateMnemonic(); + // TODO: set property `seed` as the actual derived seed, not the seed phrase + const interim = bip39.mnemonicToSeedSync(this.mnemonic); this.master = this.bip32.fromSeed(interim); + this.xprv = this.master.toBase58(); + this.xpub = this.master.neutered().toBase58(); this.keypair = ec.keyFromPrivate(this.master.privateKey); break; } // Read the pair - this.private = ( - !this.settings.seed && - !this.settings.private && - !this.settings.xprv - ) ? false : this.keypair.getPrivate(); + this.private = (this.keypair.priv) ? this.keypair.getPrivate() : null; this.public = this.keypair.getPublic(true); // TODO: determine if this makes sense / needs to be private @@ -204,15 +238,7 @@ class Key extends EventEmitter { // WARNING: this will currently loop after 2^32 bits // TODO: evaluate compression when treating seed phrase as ascii // TODO: consider using sha256(masterprivkey) or sha256(sha256(...))? - - this._starseed = Hash256.digest(( - this.settings.seed || - this.settings.xprv || - this.settings.private - ) + '').toString('hex'); - - if (!this._starseed) this._starseed = '0000000000000000000000000000000000000000000000000000000000000000'; - + this._starseed = Hash256.digest(this.pubkeyhash).toString('hex'); this.q = parseInt(this._starseed.substring(0, 4), 16); this.generator = new Generator(this.q); @@ -234,10 +260,18 @@ class Key extends EventEmitter { return this; } + /** + * Create a Key instance from a WIF-encoded private key. + * @param {String} wif - The WIF-encoded private key + * @param {Object} [options] - Additional options for key creation + * @returns {Key} A new Key instance + */ + static fromWIF (wif, options = {}) { + return new Key({ ...options, wif }); + } + static Mnemonic (seed) { - if (!seed) { - seed = crypto.randomBytes(32); - } + if (!seed) seed = crypto.randomBytes(32); const mnemonic = bip39.entropyToMnemonic(seed); const seedBuffer = bip39.mnemonicToSeedSync(mnemonic); const bip32 = new BIP32(ecc); @@ -264,11 +298,7 @@ class Key extends EventEmitter { } get iv () { - const self = this; - const bits = new BN([...Array(128)].map(() => { - return self.bit().toString(); - }).join(''), 2).toString(16); - return Buffer.from(bits.toString(16), 'hex'); + return crypto.randomBytes(16); } get path () { @@ -614,11 +644,13 @@ class Key extends EventEmitter { */ secure () { // Clear sensitive key material + Buffer.write(this.private, 0, this.private.length); + + // Null out sensitive properties this.private = null; this.privkey = null; this.seed = null; this.master = null; - this.keypair = null; // Clear any derived keys this.xprv = null; @@ -639,6 +671,58 @@ class Key extends EventEmitter { get pubkey () { return this.public.encodeCompressed('hex'); } + + /** + * Exports the private key in Wallet Import Format (WIF) + * @returns {String} The private key encoded in WIF format + * @throws {Error} If the key doesn't have a private component + */ + toWIF () { + if (!this.private) throw new Error('Cannot export WIF without private key'); + let privateKeyBuffer; + + if (Buffer.isBuffer(this.private)) { + privateKeyBuffer = this.private; + } else if (BN.isBN(this.private)) { + privateKeyBuffer = Buffer.from(this.private.toString(16).padStart(64, '0'), 'hex'); + } else if (typeof this.private === 'string') { + privateKeyBuffer = Buffer.from(this.private.padStart(64, '0'), 'hex'); + } else { + throw new Error('Invalid private key format'); + } + + const network = this.settings.network === 'regtest' + ? this.settings.networks.testnet + : this.settings.networks[this.settings.network] || this.settings.networks.mainnet; + + const prefix = Buffer.from([network.wif]); + const payload = Buffer.concat([ + prefix, + privateKeyBuffer, + Buffer.from([0x01]) + ]); + + const firstHash = crypto.createHash('sha256').update(payload).digest(); + const secondHash = crypto.createHash('sha256').update(firstHash).digest(); + const checksum = secondHash.slice(0, 4); + const combined = Buffer.concat([payload, checksum]); + + return base58.encode(combined); + } + + toBitcoinAddress () { + if (!this.public) throw new Error('Cannot derive Bitcoin address without public key'); + const network = this.settings.network === 'regtest' + ? this.settings.networks.testnet + : this.settings.networks[this.settings.network] || this.settings.networks.mainnet; + + const p2pkh = payments.p2pkh({ + pubkey: Buffer.from(this.public.encode('hex'), 'hex'), + network: network + }); + + return p2pkh.address; + } } module.exports = Key; diff --git a/types/machine.js b/types/machine.js index e59a16fe0..dd04d87a8 100644 --- a/types/machine.js +++ b/types/machine.js @@ -32,18 +32,14 @@ class Machine extends Actor { debug: false, deterministic: true, interval: 60, // seconds + key: null, precision: 8, script: [], - seed: 1, // TODO: select seed for production type: 'fabric' }, settings); // machine key - this.key = new Key({ - seed: this.settings.seed + '', // casts to string - xprv: this.settings.xprv, - private: this.settings.private, - }); + this.key = new Key(this.settings.key); // internal clock this.clock = this.settings.clock; diff --git a/types/message.js b/types/message.js index 3ff0a2e82..9fbd0f5d9 100644 --- a/types/message.js +++ b/types/message.js @@ -9,6 +9,10 @@ const { GENERIC_MESSAGE_TYPE, LOG_MESSAGE_TYPE, GENERIC_LIST_TYPE, + BITCOIN_BLOCK_TYPE, + BITCOIN_BLOCK_HASH_TYPE, + BITCOIN_TRANSACTION_TYPE, + BITCOIN_TRANSACTION_HASH_TYPE, P2P_GENERIC, P2P_IDENT_REQUEST, P2P_IDENT_RESPONSE, @@ -28,6 +32,7 @@ const { CHAT_MESSAGE, DOCUMENT_PUBLISH_TYPE, DOCUMENT_REQUEST_TYPE, + JSON_CALL_TYPE, BLOCK_CANDIDATE, PEER_CANDIDATE, SESSION_START @@ -111,6 +116,10 @@ class Message extends Actor { return this; } + get author () { + return this.raw.author.toString('hex'); + } + get body () { return this.raw.data.toString('utf8'); } @@ -374,6 +383,10 @@ class Message extends Actor { get types () { // Message Types return { + 'BitcoinBlock': BITCOIN_BLOCK_TYPE, + 'BitcoinBlockHash': BITCOIN_BLOCK_HASH_TYPE, + 'BitcoinTransaction': BITCOIN_TRANSACTION_TYPE, + 'BitcoinTransactionHash': BITCOIN_TRANSACTION_HASH_TYPE, 'GenericMessage': GENERIC_MESSAGE_TYPE, 'GenericLogMessage': LOG_MESSAGE_TYPE, 'GenericList': GENERIC_LIST_TYPE, @@ -382,6 +395,7 @@ class Message extends Actor { 'FabricServiceLogMessage': LOG_MESSAGE_TYPE, 'GenericTransferQueue': GENERIC_LIST_TYPE, 'JSONBlob': GENERIC_MESSAGE_TYPE + 1, + 'JSONCall': JSON_CALL_TYPE, // TODO: document Generic type // P2P Commands 'Generic': P2P_GENERIC, @@ -457,6 +471,14 @@ Object.defineProperty(Message.prototype, 'type', { get () { const code = parseInt(this.raw.type.toString('hex'), 16); switch (code) { + case BITCOIN_BLOCK_TYPE: + return 'BitcoinBlock'; + case BITCOIN_BLOCK_HASH_TYPE: + return 'BitcoinBlockHash'; + case BITCOIN_TRANSACTION_TYPE: + return 'BitcoinTransaction'; + case BITCOIN_TRANSACTION_HASH_TYPE: + return 'BitcoinTransactionHash'; case GENERIC_MESSAGE_TYPE: return 'GenericMessage'; case GENERIC_MESSAGE_TYPE + 1: @@ -503,6 +525,8 @@ Object.defineProperty(Message.prototype, 'type', { return 'StartSession'; case CHAT_MESSAGE: return 'ChatMessage'; + case JSON_CALL_TYPE: + return 'JSONCall'; case P2P_START_CHAIN: return 'StartChain'; default: diff --git a/types/peer.js b/types/peer.js index 73f5b0481..d2bced881 100644 --- a/types/peer.js +++ b/types/peer.js @@ -116,7 +116,7 @@ class Peer extends Service { this.sessions = {}; // Internal Stack Machine - this.machine = new Machine(); + this.machine = new Machine({ key: this.settings.key }); this.observer = null; this.meta = { @@ -211,6 +211,11 @@ class Peer extends Service { return this.settings.port || 7777; } + get publicPeers () { + const peers = []; + return peers; + } + beat () { const initial = new Actor(this.state); const now = (new Date()).toISOString(); diff --git a/types/service.js b/types/service.js index 5f83e5313..9ba5e1ec5 100644 --- a/types/service.js +++ b/types/service.js @@ -19,6 +19,7 @@ const manager = require('fast-json-patch'); const Actor = require('./actor'); const Collection = require('./collection'); const Entity = require('./entity'); +const Filesystem = require('./filesystem'); const Hash256 = require('./hash256'); const Identity = require('./identity'); const Key = require('./key'); @@ -68,6 +69,7 @@ class Service extends Actor { fs: { path: `./stores/fabric-service-${this.name}` }, + key: null, state: { ...super.state, actors: {}, // TODO: schema @@ -110,6 +112,7 @@ class Service extends Actor { // Error: Not implemented yet this.key = new Key(this.settings.key); this.identity = new Identity(this.settings.key); + this.fs = new Filesystem({ ...this.settings.fs, key: this.settings.key }); if (this.settings.persistent) { try { diff --git a/types/store.js b/types/store.js index 4232617f5..ec87fcf93 100644 --- a/types/store.js +++ b/types/store.js @@ -433,7 +433,7 @@ class Store extends Actor { let id = pointer.escape(key); let router = store.sha256(id); - let state = new State(value); + let state = new Actor(value); pointer.set(store['@entity']['@data'], `${name}`, value); pointer.set(store['@entity']['@data'], `/states/${state.id}`, value); diff --git a/types/wallet.js b/types/wallet.js index 89c477bce..bf67b2cba 100644 --- a/types/wallet.js +++ b/types/wallet.js @@ -4,7 +4,6 @@ const BN = require('bn.js'); const EC = require('elliptic').ec; const merge = require('lodash.merge'); -const payments = require('bitcoinjs-lib/src/payments'); const networks = require('bitcoinjs-lib/src/networks'); // Mnemonics @@ -76,16 +75,9 @@ class Wallet extends Service { this.seed = null; // Initialize key management - if (this.settings.key && this.settings.key.seed) { - this.key = new Key({ - seed: this.settings.key.seed, - passphrase: this.settings.key.passphrase || '' - }); - this.seed = this.settings.key.seed; - } else { - this.key = new Key(); - this.seed = this.key.mnemonic; - } + this.key = new Key(this.settings.key); + this.seed = this.key.seed; + this.wallet = { keypair: this.key.keypair }; @@ -544,20 +536,6 @@ class Wallet extends Service { }; } - addInputForCrowdfund (coin, inputIndex, mtx, keyring, hashType) { - let sampleCoin = coin instanceof Coin ? coin : Coin.fromJSON(coin); - if (!hashType) hashType = Script.hashType.ANYONECANPAY | Script.hashType.ALL; - - mtx.addCoin(sampleCoin); - mtx.scriptInput(inputIndex, sampleCoin, keyring); - mtx.signInput(inputIndex, sampleCoin, keyring, hashType); - - console.log('MTX after Input added (and signed):', mtx); - - // TODO: return a full object for Fabric - return mtx; - } - balanceFromState (state) { if (!state.transactions) throw new Error('State does not provide a `transactions` property.'); if (!state.transactions.length) return 0; @@ -608,66 +586,6 @@ class Wallet extends Service { }; } - async _splitCoinbase (funderKeyring, coin, targetAmount, txRate) { - // loop through each coinbase coin to split - let coins = []; - - const mtx = new MTX(); - - assert(coin.value > targetAmount, 'coin value is not enough!'); - - // creating a transaction that will have an output equal to what we want to fund - mtx.addOutput({ - address: funderKeyring.getAddress(), - value: targetAmount - }); - - // the fund method will automatically split - // the remaining funds to the change address - // Note that in a real application these splitting transactions will also - // have to be broadcast to the network - await mtx.fund([coin], { - rate: txRate, - // send change back to an address belonging to the funder - changeAddress: funderKeyring.getAddress() - }).then(() => { - // sign the mtx to finalize split - mtx.sign(funderKeyring); - assert(mtx.verify()); - - const tx = mtx.toTX(); - assert(tx.verify(mtx.view)); - - const outputs = tx.outputs; - - // get coins from tx - outputs.forEach((outputs, index) => { - coins.push(Coin.fromTX(tx, index, -1)); - }); - }).catch(e => console.log('There was an error: ', e)); - - return coins; - } - - async composeCrowdfund (coins) { - const funderCoins = {}; - // Loop through each coinbase - for (let index in coins) { - const coinbase = coins[index][0]; - // estimate fee for each coin (assuming their split coins will use same tx type) - const estimatedFee = getFeeForInput(coinbase, fundeeAddress, funders[index], txRate); - const targetPlusFee = amountToFund + estimatedFee; - - // split the coinbase with targetAmount plus estimated fee - const splitCoins = await Utils.splitCoinbase(funders[index], coinbase, targetPlusFee, txRate); - - // add to funderCoins object with returned coins from splitCoinbase being value, - // and index being the key - funderCoins[index] = splitCoins; - } - // ... we'll keep filling out the rest of the code here - } - async _addOutputToSpendables (coin) { this._state.utxos.push(coin); return this; @@ -686,90 +604,6 @@ class Wallet extends Service { }); } - async _generateFakeCoinbase (amount = 1) { - // TODO: use Satoshis for all calculations - let num = new BN(amount, 10); - - // TODO: remove all fake coinbases - // TODO: remove all short-circuits - // fake coinbase - let cb = new MTX(); - let clean = await this.generateCleanKeyPair(); - - // Coinbase Input - cb.addInput({ - prevout: new Outpoint(), - script: new Script(), - sequence: 0xffffffff - }); - - // Add Output to pay ourselves - cb.addOutput({ - address: clean.address, - value: 5000000000 - }); - - // TODO: remove short-circuit - let coin = Coin.fromTX(cb, 0, -1); - let tx = cb.toTX(); - - // TODO: remove entirely, test short-circuit removal - // await this._addOutputToSpendables(coin); - - return { - type: 'BitcoinTransactionOutput', - data: { - tx: cb, - coin: coin - } - }; - } - - async _getFreeCoinbase (amount = 1) { - let num = new BN(amount, 10); - let max = new BN('5000000000000', 10); // upper limit per coinbase - let hun = new BN('100000000', 10); // one hundred million - let value = num.mul(hun); // amount in Satoshis - - if (value.gt(max)) { - console.warn('Value (in satoshis) higher than max:', value.toString(10), `(max was ${max.toString(10)})`); - value = max; - } - - let v = value.toString(10); - let w = parseInt(v); - - await this._load(); - - const coins = {}; - const coinbase = new MTX(); - - // INSERT 1 Input - coinbase.addInput({ - prevout: new Outpoint(), - script: new Script(), - sequence: 0xffffffff - }); - - try { - // INSERT 1 Output - coinbase.addOutput({ - address: this._getDepositAddress(), - value: w - }); - } catch (E) { - console.error('Could not add output:', E); - } - - // TODO: wallet._getSpendableOutput() - let coin = Coin.fromTX(coinbase, 0, -1); - this._state.utxos.push(coin); - - // console.log('coinbase:', coinbase); - - return coinbase; - } - /** * Signs a transaction with the keyring. * @param {BcoinTX} tx @@ -820,46 +654,6 @@ class Wallet extends Service { return this._loadSeed(mnemonic.toString()); } - async _createIncentivizedTransaction (config) { - console.log('creating incentivized transaction with config:', config); - - let mtx = new MTX(); - let data = new Script(); - let clean = await this.generateCleanKeyPair(); - - data.pushSym('OP_IF'); - data.pushSym('OP_SHA256'); - data.pushData(Buffer.from(config.hash)); - data.pushSym('OP_EQUALVERIFY'); - data.pushData(Buffer.from(config.payee)); - data.pushSym('OP_ELSE'); - data.pushInt(config.locktime); - data.pushSym('OP_CHECKSEQUENCEVERIFY'); - data.pushSym('OP_DROP'); - data.pushData(Buffer.from(clean.public)); - data.pushSym('OP_ENDIF'); - data.pushSym('OP_CHECKSIG'); - data.compile(); - - console.log('address data:', data); - let segwitAddress = await this.getAddressForScript(data); - - mtx.addOutput({ - address: segwitAddress, - value: 0 - }); - - // TODO: load available outputs from wallet - let out = await mtx.fund([] /* coins */, { - // TODO: fee estimation - rate: 10000, - changeAddress: this.ring.getAddress() - }); - - console.log('transaction:', out); - return out; - } - async _getBondAddress () { await this._load(); @@ -925,62 +719,6 @@ class Wallet extends Service { ); } - async getRedeemTX (address, fee, fundingTX, fundingTXoutput, redeemScript, inputScript, locktime, privateKey) { - // Create a mutable transaction object - let redeemTX = new MTX(); - - // Get the output we want to spend (coins sent to the P2SH address) - let coin = Coin.fromTX(fundingTX, fundingTXoutput, -1); - - // Add that coin as an input to our transaction - redeemTX.addCoin(coin); - - // Redeem the input coin with either the swap or refund script - redeemTX.inputs[0].script = inputScript; - - // Create the output back to our primary wallet - redeemTX.addOutput({ - address: address, - value: coin.value - fee - }); - - // If this was a refund redemption we need to set the sequence - // Sequence is the relative timelock value applied to individual inputs - if (locktime) { - redeemTX.setSequence(0, locktime, this.CSV_seconds); - } else { - redeemTX.inputs[0].sequence = 0xffffffff; - } - - // Set SIGHASH and replay protection bits - let version_or_flags = 0; - let type = null; - - if (this.libName === 'bcash') { - version_or_flags = this.flags; - type = Script.hashType.SIGHASH_FORKID | Script.hashType.ALL; - } - - // Create the signature authorizing the input script to spend the coin - let sig = await this.signInput( - redeemTX, - 0, - redeemScript, - coin.value, - privateKey, - type, - version_or_flags - ); - - // Insert the signature into the input script where we had a `0` placeholder - inputScript.setData(0, sig); - - // Finish up and return - inputScript.compile(); - - return redeemTX; - } - /** * Generate {@link Script} for claiming a {@link Swap}. * @param {*} redeemScript @@ -1013,70 +751,6 @@ class Wallet extends Service { return inputRefund; } - async _createOrderForPubkey (pubkey) { - this.emit('log', `creating ORDER transaction with pubkey: ${pubkey}`); - - let mtx = new MTX(); - let data = new Script(); - let clean = await this.generateCleanKeyPair(); - - let secret = 'fixed secret :)'; - let sechash = require('crypto').createHash('sha256').update(secret).digest('hex'); - - this.emit('log', `SECRET CREATED: ${secret}`); - this.emit('log', `SECHASH: ${sechash}`); - - data.pushSym('OP_IF'); - data.pushSym('OP_SHA256'); - data.pushData(Buffer.from(sechash)); - data.pushSym('OP_EQUALVERIFY'); - data.pushData(Buffer.from(pubkey)); - data.pushSym('OP_ELSE'); - data.pushInt(86400); - data.pushSym('OP_CHECKSEQUENCEVERIFY'); - data.pushSym('OP_DROP'); - data.pushData(Buffer.from(clean.public)); - data.pushSym('OP_ENDIF'); - data.pushSym('OP_CHECKSIG'); - data.compile(); - - this.emit('log', `[AUDIT] address data: ${data}`); - let segwitAddress = await this.getAddressForScript(data); - let address = await this.getAddressFromRedeemScript(data); - - this.emit('log', `[AUDIT] segwit address: ${segwitAddress}`); - this.emit('log', `[AUDIT] normal address: ${address}`); - - mtx.addOutput({ - address: address, - value: 25000000 - }); - - // ensure a coin exists... - // NOTE: this is tracked in this._state.coins - // and thus does not need to be cast to a variable... - let coinbase = await this._getFreeCoinbase(); - - // TODO: load available outputs from wallet - let out = await mtx.fund(this._state.utxos, { - // TODO: fee estimation - rate: 10000, - changeAddress: this.ring.getAddress() - }); - - let tx = mtx.toTX(); - let sig = await mtx.sign(this.ring); - - this.emit('log', 'transaction:', tx); - this.emit('log', 'sig:', sig); - - return { - tx: tx, - mtx: mtx, - sig: sig - }; - } - async _scanBlockForTransactions (block) { console.log('[AUDIT]', 'Scanning block for transactions:', block); let found = [];