-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from IoFinnet/bittensor-recovery-tool
feat(scripts/bittensor): add bittensor recovery tool
- Loading branch information
Showing
10 changed files
with
1,865 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# Bittensor Disaster Recovery Transfer Tool | ||
|
||
A command-line tool for transferring **TAO** (Bittensor) tokens using EdDSA/Ed25519 keypairs. This tool allows you to: | ||
- Import Ed25519 keypairs in raw hex format for disaster recovery | ||
- Derive the correct Bittensor SS58 wallet address | ||
- Create and sign TAO transfer transactions | ||
- Broadcast transactions to the Bittensor Substrate network | ||
|
||
## Prerequisites | ||
|
||
- **Node.js 20** or higher | ||
- **npm** or **yarn** | ||
|
||
## Installation | ||
|
||
1. Clone this repository or download the files. | ||
2. Install dependencies: | ||
```bash | ||
npm install | ||
``` | ||
|
||
## Running the Tool | ||
|
||
Run the script using **tsx** (TypeScript Execution): | ||
|
||
```bash | ||
npx tsx main.ts | ||
``` | ||
|
||
## Usage Flow | ||
|
||
1. Enter your **EdDSA keypair** in raw hex format: | ||
- Public key (64 hex characters) | ||
- Private key (64 hex characters) | ||
2. Verify your **derived SS58 address**. | ||
3. Enter transaction details: | ||
- Destination Bittensor wallet address (SS58 format) | ||
- Amount of TAO to transfer | ||
4. Review the transaction details. | ||
5. Sign and broadcast the transaction to the network. | ||
|
||
--- | ||
|
||
## Example Keys Format | ||
|
||
The tool expects your Ed25519 public and private keys in raw hex format (32 bytes each, 64 hex characters total). Example: | ||
|
||
- **Public key**: | ||
`d6c1b2d7c4e47e72d937f64c90bc2e3775d40a8e38c2990c4397dcb1b0d6a512` | ||
|
||
- **Private key**: | ||
`32e1f1725190e14500a865a4dae637129d7959025164a9c0f58247c4d00ebd12` | ||
|
||
--- | ||
|
||
## Security Notes | ||
|
||
- Your private key is **never stored or transmitted**. | ||
- The tool can run **offline**, except for broadcasting the transaction. | ||
- Always verify the derived wallet address before proceeding. | ||
- Use the **testnet** environment first to ensure the keys and flow are correct. | ||
|
||
--- | ||
|
||
## Network Configuration | ||
|
||
You can specify a Bittensor node endpoint for broadcasting transactions: | ||
|
||
- **Mainnet**: `wss://entrypoint-finney.opentensor.ai:443` | ||
- **Testnet**: Replace with the appropriate endpoint. | ||
|
||
You will be prompted to enter the endpoint during execution. | ||
|
||
--- | ||
|
||
## Offline Usage | ||
|
||
The tool can be run offline for: | ||
- Keypair recovery and SS58 address derivation. | ||
- Transaction preparation and signing. | ||
|
||
**Note**: Broadcasting requires an active connection to the Bittensor Substrate network. | ||
|
||
--- | ||
|
||
## Error Handling | ||
|
||
The tool includes validation for: | ||
- Private and public key format (32 bytes / 64 hex characters). | ||
- SS58 address format (Bittensor prefix `42`). | ||
- Valid TAO transfer amounts. | ||
- Network connectivity issues when broadcasting. | ||
|
||
If a mismatch occurs between the derived public key and provided public key, the tool will alert you and stop execution. | ||
|
||
--- | ||
|
||
## Example Workflow | ||
|
||
### 1. Run the Tool | ||
|
||
```bash | ||
npm run start | ||
``` | ||
|
||
### 2. Enter Keypair | ||
|
||
```plaintext | ||
Public Key (64 hex chars): d6c1b2d7c4e47e72d937f64c90bc2e3775d40a8e38c2990c4397dcb1b0d6a512 | ||
Private Key (64 hex chars): 32e1f1725190e14500a865a4dae637129d7959025164a9c0f58247c4d00ebd12 | ||
``` | ||
|
||
### 3. Verify Derived Address | ||
|
||
```plaintext | ||
Derived Address (SS58): 5DDRNSii3jUdb4yWot6ZvzeXg3DtUG7vWXQzVJkbVnZKYqUG | ||
``` | ||
|
||
### 4. Enter Transaction Details & Broadcast | ||
|
||
```plaintext | ||
Enter the destination address: 5Fexample1234567890addressHere | ||
Enter the amount of TAO to transfer: 1.0 | ||
Enter the endpoint (default: wss://entrypoint-finney.opentensor.ai:443): | ||
``` | ||
|
||
--- | ||
|
||
## Troubleshooting | ||
|
||
- **Invalid Signature Error**: Ensure that the public key matches the private key. If using an MPC system, verify the keys align correctly. | ||
- **Address Mismatch**: The derived address must match the expected address. Double-check the private key input. | ||
- **Network Issues**: Verify the Substrate endpoint URL and your internet connection. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; | ||
import readlineSync from 'readline-sync'; | ||
import { BN } from 'bn.js'; | ||
import { blake2AsHex, cryptoWaitReady } from '@polkadot/util-crypto'; | ||
import { Signer, SignerPayloadRaw, SignerResult } from '@polkadot/types/types'; | ||
import * as ed from '@noble/ed25519'; | ||
import { bytesToNumberBE, bytesToNumberLE, numberToBytesLE } from '@noble/curves/abstract/utils'; | ||
import { sha512 } from '@noble/hashes/sha512'; | ||
import { webcrypto } from 'crypto'; | ||
|
||
// Constants | ||
const DECIMALS = 9; | ||
const PLANCK = new BN(10).pow(new BN(DECIMALS)); // 1 unit = 10^9 Planck | ||
const SS58_FORMAT = 42; // SS58 address format for the Bittensor network | ||
|
||
// Assign SHA512 hash function for noble-ed25519 compatibility | ||
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); | ||
|
||
/** | ||
* Transfers funds on the Bittensor network. | ||
* @param privateKeyHex - The raw Ed25519 private key in hex format. | ||
* @param destination - The SS58 destination address. | ||
* @param amount - The amount to transfer in Planck units. | ||
* @param endpoint - The WebSocket endpoint for the Bittensor network. | ||
*/ | ||
async function transferFunds(privateKeyHex: string, destination: string, amount: string, endpoint: string) { | ||
await cryptoWaitReady(); | ||
|
||
console.log("Connecting to the Bittensor network..."); | ||
const provider = new WsProvider(endpoint); | ||
const api = await ApiPromise.create({ provider }); | ||
|
||
console.log("Creating keyring..."); | ||
const keyring = new Keyring({ type: 'ed25519', ss58Format: SS58_FORMAT }); | ||
|
||
// Prepare the private key | ||
let privateKey = Buffer.from(privateKeyHex, 'hex'); | ||
if (privateKey.length !== 32) { | ||
throw new Error('Private key must be 32 bytes'); | ||
} | ||
|
||
// Derive the public key from the private key | ||
const publicKey = ed.ExtendedPoint.BASE.multiply(bytesToNumberBE(privateKey)).toRawBytes(); | ||
console.log("Derived Public Key (Hex):", Buffer.from(publicKey).toString('hex')); | ||
|
||
// Add the keypair to the keyring | ||
const keyPair = keyring.addFromPair({ | ||
publicKey: publicKey, | ||
secretKey: new Uint8Array(0), | ||
}); | ||
|
||
console.log("Derived Address (SS58):", keyPair.address); | ||
|
||
const wantToTransfer = readlineSync.keyInYNStrict('\nWould you like to proceed with the transaction?'); | ||
if (!wantToTransfer) { | ||
console.log('Exiting...'); | ||
throw new Error('User cancelled'); | ||
} | ||
|
||
console.log("Creating transfer transaction..."); | ||
const transfer = api.tx.balances.transferKeepAlive(destination, amount); | ||
|
||
console.log("Signing and sending transaction..."); | ||
const signer = new CustomSigner(privateKey); | ||
const hash = await transfer.signAndSend(keyPair.address, { nonce: -1, signer }); | ||
console.log(`Transaction sent successfully. Hash: ${hash.toHex()}`); | ||
|
||
await api.disconnect(); | ||
} | ||
|
||
/** | ||
* Validates if the input string is a valid hex string of the given byte length. | ||
* @param input - The input string to validate. | ||
* @param length - The expected byte length. | ||
* @returns True if the input is a valid hex string; otherwise, false. | ||
*/ | ||
function validateHex(input: string, length: number): boolean { | ||
return input.length === length * 2 && /^[0-9a-fA-F]+$/.test(input); | ||
} | ||
|
||
/** | ||
* Validates if the input is a valid SS58 address. | ||
* @param address - The SS58 address to validate. | ||
* @returns True if the address is valid; otherwise, false. | ||
*/ | ||
function validateAddress(address: string): boolean { | ||
return address.length === 48 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(address); | ||
} | ||
|
||
/** | ||
* Validates if the input is a positive numeric amount. | ||
* @param amount - The input amount. | ||
* @returns True if the amount is valid; otherwise, false. | ||
*/ | ||
function validateAmount(amount: string): boolean { | ||
const num = Number(amount); | ||
return !isNaN(num) && num > 0; | ||
} | ||
|
||
async function main() { | ||
console.log('Bittensor Transfer Tool\n'); | ||
|
||
let privateKey; | ||
do { | ||
privateKey = readlineSync.question('Private Key (64 hex chars): ', { hideEchoBack: true }); | ||
if (!validateHex(privateKey, 32)) { | ||
console.log('Invalid private key format. Must be 64 hexadecimal characters.'); | ||
} | ||
} while (!validateHex(privateKey, 32)); | ||
|
||
let destination; | ||
do { | ||
destination = readlineSync.question('Enter the destination address: '); | ||
if (!validateAddress(destination)) { | ||
console.log('Invalid address format. Must be a valid Bittensor address.'); | ||
} | ||
} while (!validateAddress(destination)); | ||
|
||
let amount; | ||
do { | ||
amount = readlineSync.question('Enter the amount to transfer: '); | ||
if (!validateAmount(amount)) { | ||
console.log('Invalid amount. Must be a positive number.'); | ||
} | ||
} while (!validateAmount(amount)); | ||
|
||
const endpoint = readlineSync.question( | ||
'Enter the endpoint (e.g., wss://entrypoint-finney.opentensor.ai:443): ', | ||
{ defaultInput: 'wss://entrypoint-finney.opentensor.ai:443' } | ||
); | ||
|
||
console.log('\nProcessing your transaction...\n'); | ||
try { | ||
// Convert amount to Planck units (smallest denomination) | ||
const amountInPlanck = new BN(Number(amount) * Number(PLANCK)); | ||
await transferFunds(privateKey, destination, amountInPlanck.toString(), endpoint); | ||
} catch (error) { | ||
console.error('Error processing transaction:', error.message); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
main().catch(console.error); | ||
|
||
class CustomSigner implements Signer { | ||
private privateKey: Uint8Array; | ||
|
||
constructor(privateKey: Uint8Array) { | ||
this.privateKey = privateKey; | ||
} | ||
|
||
// this is the interface function of the Signer interface called by Polkadot.js | ||
public async signRaw({ data }: SignerPayloadRaw): Promise<SignerResult> { | ||
// eslint-disable-next-line no-async-promise-executor | ||
return new Promise(async (resolve, reject): Promise<void> => { | ||
const payloadHex = (data.length > (256 + 1) * 2 ? blake2AsHex(data) : data) as `0x${string}`; | ||
console.info('Signer Payload:', payloadHex); | ||
|
||
let { signature: signatureHex } = await signWithScalar(payloadHex, Buffer.from(this.privateKey).toString('hex')); | ||
signatureHex = '00' + signatureHex.slice(0, 128) | ||
if (signatureHex.length !== 65 * 2) { | ||
reject(new Error(`Invalid signature, must be hex of the expected length (got: ${signatureHex.length})`)); | ||
return; | ||
} else { | ||
resolve({ id: 1, signature: '0x' + signatureHex as `0x{string}` }); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
async function signWithScalar(messageHex: string, privateKeyHex: string): Promise<{ signature: string, publicKey: string }> { | ||
// Remove '0x' prefix if present from inputs | ||
messageHex = messageHex.replace(/^0x/, ''); | ||
privateKeyHex = privateKeyHex.replace(/^0x/, ''); | ||
|
||
// Convert hex message to Uint8Array | ||
const message = Buffer.from(messageHex, 'hex'); | ||
|
||
// Convert hex private key scalar to Uint8Array | ||
const privateKeyBytes = Buffer.from(privateKeyHex, 'hex'); | ||
const scalar = bytesToNumberBE(privateKeyBytes); | ||
if (scalar >= ed.CURVE.n) { | ||
throw new Error('Private key scalar must be less than curve order'); | ||
} | ||
|
||
// Validate private key length (32 bytes for Ed25519) | ||
if (privateKeyBytes.length !== 32) { | ||
throw new Error('Private key must be 32 bytes'); | ||
} | ||
|
||
// Calculate public key directly from private key scalar | ||
const publicKey = ed.ExtendedPoint.BASE.multiply(bytesToNumberBE(privateKeyBytes)).toRawBytes(); | ||
|
||
// Note: This nonce generation differs from standard Ed25519, which uses | ||
// the first half of SHA-512(private_key_seed). We're creating a deterministic | ||
// nonce from the raw scalar and message instead. | ||
const nonceInput = new Uint8Array([...privateKeyBytes, ...message]); | ||
const nonceArrayBuffer = await webcrypto.subtle.digest('SHA-512', nonceInput); | ||
const nonceArray = new Uint8Array(nonceArrayBuffer); | ||
|
||
// Reduce nonce modulo L (Ed25519 curve order) | ||
const reducedNonce = bytesToNumberLE(nonceArray) % ed.CURVE.n; | ||
|
||
// Calculate R = k * G | ||
const R = ed.ExtendedPoint.BASE.multiply(reducedNonce); | ||
|
||
// Calculate S = (r + H(R,A,m) * s) mod L | ||
const hramInput = new Uint8Array([ | ||
...R.toRawBytes(), | ||
...publicKey, | ||
...message | ||
]); | ||
|
||
const hArrayBuffer = await webcrypto.subtle.digest('SHA-512', hramInput); | ||
const h = new Uint8Array(hArrayBuffer); | ||
const hnum = bytesToNumberLE(h) % ed.CURVE.n; | ||
|
||
const s = bytesToNumberBE(privateKeyBytes); | ||
const S = (reducedNonce + (hnum * s)) % ed.CURVE.n; | ||
|
||
// Combine R and S to form signature | ||
const signature = new Uint8Array([ | ||
...R.toRawBytes(), | ||
...numberToBytesLE(S, 32) | ||
]); | ||
|
||
// Convert outputs to hex strings | ||
return { | ||
signature: bytesToHex(signature), | ||
publicKey: bytesToHex(publicKey) | ||
}; | ||
} | ||
|
||
// Helper function to convert Uint8Array to hex string | ||
function bytesToHex(bytes: Uint8Array): string { | ||
return Array.from(bytes) | ||
.map(b => b.toString(16).padStart(2, '0')) | ||
.join(''); | ||
} |
Oops, something went wrong.