Skip to content

Commit

Permalink
Merge pull request #19 from IoFinnet/bittensor-recovery-tool
Browse files Browse the repository at this point in the history
feat(scripts/bittensor): add bittensor recovery tool
  • Loading branch information
JacobPlaster authored Dec 24, 2024
2 parents c6f83d1 + 02799ea commit 9713a82
Show file tree
Hide file tree
Showing 10 changed files with 1,865 additions and 4 deletions.
2 changes: 1 addition & 1 deletion internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func Banner() string {
b := "\n"
b += fmt.Sprintf("%s%s %s\n", AnsiCodes["invertOn"], AnsiCodes["bold"], AnsiCodes["reset"])
b += fmt.Sprintf("%s%s io.finnet Key Recovery Tool %s\n", AnsiCodes["invertOn"], AnsiCodes["bold"], AnsiCodes["reset"])
b += fmt.Sprintf("%s%s v5.1.0 %s\n", AnsiCodes["invertOn"], AnsiCodes["bold"], AnsiCodes["reset"])
b += fmt.Sprintf("%s%s v5.2.0 %s\n", AnsiCodes["invertOn"], AnsiCodes["bold"], AnsiCodes["reset"])
b += fmt.Sprintf("%s%s %s\n", AnsiCodes["invertOn"], AnsiCodes["bold"], AnsiCodes["reset"])
b += "\n"
return b
Expand Down
134 changes: 134 additions & 0 deletions scripts/bittensor-recovery-script/README.md
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.

239 changes: 239 additions & 0 deletions scripts/bittensor-recovery-script/main.ts
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('');
}
Loading

0 comments on commit 9713a82

Please sign in to comment.