polkadot-js / api

Promise and RxJS APIs around Polkadot and Substrate based chains via RPC calls. It is dynamically generated based on what the Substrate runtime provides in terms of metadata.
Apache License 2.0
1.07k stars 354 forks source link

Offline signing without using keyring/ECDSA signatures. #5934

Closed EvanTedesco closed 4 months ago

EvanTedesco commented 4 months ago

class SigningService { constructor() { this.keys = {} }

computePublicKeysFromPrivateKey({ key }) { return Promise.resolve({ publicKeys: [ { format: 'compressed', value: Buffer.from(secp256k1.getPublicKey(key, true)).toString('hex') }, { format: 'uncompressed', value: Buffer.from(secp256k1.getPublicKey(key, false)).toString('hex').substring(2) } ] }); }

async importPrivateKey({ key }) { const id = crypto.randomUUID(); const publicKeys = await this.computePublicKeysFromPrivateKey({ key });

this.keys[id] = key;

return Promise.resolve({ id, ...publicKeys });

}

async sign({ hash, id }) { const signature = secp256k1.sign(hash, keys[id]);

return Promise.resolve({ recovery: signature.recovery, signature: signature.toCompactHex(), signatureFull: signature });

} }

const transferECDSA = async (to, from, amount) => { const keyService = new SigningService(); const { id: keyId } = await keyService.importPrivateKey({ key: '' });

const RPC_ENDPOINT = 'wss://rococo-muse-rpc.polkadot.io'; const wsProvider = new WsProvider(RPC_ENDPOINT); const api = await ApiPromise.create({ provider: wsProvider });

await api.isReady; await cryptoWaitReady();

const extrinsic = api.tx.balances.transferKeepAlive(to, amount);

const signer = { signPayload: async payload => { const extrinsicPayload = api.registry.createType('ExtrinsicPayload', payload, { mode: 0, registry: api.registry, version: EXTRINSIC_VERSION }); const extrinsicPayloadU8a = extrinsicPayload.toU8a({ method: true }); const actualPayload = extrinsicPayloadU8a.length > 256 ? api.registry.hash(extrinsicPayloadU8a) : extrinsicPayloadU8a; const { recovery, signature } = await keyService.sign({ hash: keccakAsHex(actualPayload).substring(2), id: keyId }); const recoveryU8a = Uint8Array.from([recovery || 0]);

  return { id: 1, signature: u8aToHex(u8aConcat(hexToU8a(signature), recoveryU8a)) };
}

};

const signedExtrinsic = await extrinsic.signAsync(from, { signer });

const unsub = await signedExtrinsic.send(({ status }) => { if (status.isInBlock) { console.log(Transaction included at blockHash ${status.asInBlock}); } else if (status.isFinalized) { console.log(Transaction finalized at blockHash ${status.asFinalized}); unsub(); throw new Error('Exiting Process'); } }); }


 My goal is to be able to create the signer and sign completely offline.  Currently the    `const extrinsicPayload = api.registry.createType('ExtrinsicPayload', payload, { mode: 0, registry: api.registry, version: EXTRINSIC_VERSION });` bit in the signer is keeping me from having the signer completely offline.  I have tried countless other approaches and I cannot generate a valid signature for some reason.  Any help is greatly appreciated. 
EvanTedesco commented 4 months ago

This ended up being an issue with how I was generating the payload contents for the ExtrinsicPayload. I solved it by using the createType interface to create the payload values E.G.

 const latestBlockHash = (await api.rpc.chain.getFinalizedHead()).toHex();
    const blockNumber = (await api.rpc.chain.getBlock(latestBlockHash)).block.header.number.toHex();
    const era = api.registry.createType('ExtrinsicEra', {
      current: blockNumber,
      period: 64
    });

    const nonceRaw = await api.call.accountNonceApi.accountNonce(from);
    const nonce = api.registry.createType('Compact<Index>', nonceRaw.toNumber());

    const payload = {
      address: from,
      blockHash: latestBlockHash,
      blockNumber,
      era: era.toHex(),
      genesisHash: api.genesisHash.toHex(),
      method: tx.method.toHex(),
      nonce: nonce.toHex(),
      signedExtensions: api.registry.signedExtensions,
      specVersion: api.runtimeVersion.specVersion.toHex(),
      tip: api.registry.createType('Compact<Balance>', 0).toHex(),
      transactionVersion: api.runtimeVersion.transactionVersion.toHex(),
      version: tx.version
    };

    const extrinsicPayload = api.registry.createType('ExtrinsicPayload', payload, {
      version: payload.version
    });

    const extrinsicPayloadU8a = extrinsicPayload.toU8a(true);
    const actualPayload = extrinsicPayloadU8a.length > 256 ? api.registry.hash(extrinsicPayloadU8a) : extrinsicPayloadU8a;
    const { recovery, signature } = await keyService.sign({ algorithm: 'ecdsa', encoding: 'raw', hash: keccakAsHex(actualPayload).substring(2), id: keyId });
    const recoveryU8a = Uint8Array.from([recovery || 0]);
    const sig = u8aToHex(u8aConcat(hexToU8a(signature), recoveryU8a));
    const signedExtrinsic = tx.addSignature(from, sig, extrinsicPayload.toHex());

Then I was able to call .send successfully.

polkadot-js-bot commented 3 months ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue if you think you have a related problem or query.