Aegis: DigiByte addresses follow the chosen address family
WalletKeys.entry() built a bech32 p2wpkh address whatever the account path's purpose, so picking Legacy (D...), Wrapped SegWit (S...) or Taproot (dgb1p...) in Settings showed a dgb1q address from the BIP44/ 49/86 key tree, one no other wallet restoring that path would find. Each family now derives its own address and script, and spending supplies what its inputs need (previous tx for P2PKH, redeem script for P2SH-P2WPKH, tap-tweaked key for Taproot, which is active on DigiByte), with per-family fee sizes. Unknown purposes are refused instead of falling back to BIP84. Also: inputs were signed in selection order but the PSBT orders them by BIP69, so a spend from two addresses tried to sign each input with the other's key. They are now signed in the PSBT's order.
This commit is contained in:
parent
03140fae9d
commit
03b497dcae
1 changed files with 82 additions and 22 deletions
|
|
@ -9,9 +9,9 @@
|
|||
// Optional Blockbook mode is on the roadmap; ElectrumX is the default
|
||||
// because it matches Aegis's transport shape and needs no per-server keys.
|
||||
//
|
||||
// Only BIP84 (m/84'/20'/0'/0/x → dgb1q…) is exposed in this rev; the
|
||||
// vendored core also supports BIP44 (D…) and BIP49 (S…) — plumb them by
|
||||
// switching the purpose passed to accountNode(). Address recovery from any
|
||||
// All four address families follow the account path's purpose: BIP84
|
||||
// (m/84'/20'/0'/0/x → dgb1q…, the default), BIP44 (D…), BIP49 (S…) and
|
||||
// BIP86 (dgb1p…, Taproot is active on DigiByte). Address recovery from any
|
||||
// BIP39 tool at coin type 20 is guaranteed by bitcoinjs-lib's Network
|
||||
// object, so a seed exported here can be restored on iancoleman.io/bip39
|
||||
// or the SilentCode Digibyte web-wallet with matching addresses.
|
||||
|
|
@ -52,8 +52,35 @@ module.exports = function makeDgbAdapter({
|
|||
const rev = Buffer.from(h).reverse();
|
||||
return rev.toString("hex");
|
||||
}
|
||||
function scriptPubKeyBuf(pubkeyBuf) {
|
||||
return payments.p2wpkh({ pubkey: pubkeyBuf, network: digibyte }).output;
|
||||
// Taproot outputs need bitcoinjs-lib's schnorr backend; chain-btc.js does
|
||||
// the same at load, but this adapter must not depend on load order.
|
||||
try { bitcoinjs.initEccLib && bitcoinjs.initEccLib(ecc); } catch {}
|
||||
|
||||
// Address family from the derivation-path purpose, with everything the
|
||||
// PSBT layer needs to spend an input of that family. entry() used to make
|
||||
// a bech32 p2wpkh address whatever the purpose, so picking "Legacy (D…)"
|
||||
// or "Taproot (dgb1p…)" in Settings showed a dgb1q address from the wrong
|
||||
// key tree — one no other wallet restoring that path would ever find.
|
||||
// Same shapes as chain-btc.js; BIP49 uses DGB's current "S…" prefix.
|
||||
const PURPOSES = new Set([44, 49, 84, 86]);
|
||||
function paymentFor(purpose, node) {
|
||||
const pubkey = Buffer.from(node.publicKey);
|
||||
if (purpose === 44) {
|
||||
const p = payments.p2pkh({ pubkey, network: digibyte });
|
||||
return { family: "bip44", address: p.address, output: Buffer.from(p.output), send: "p2pkh" };
|
||||
}
|
||||
if (purpose === 49) {
|
||||
const redeem = payments.p2wpkh({ pubkey, network: digibyte });
|
||||
const p = payments.p2sh({ redeem, network: digibyte });
|
||||
return { family: "bip49", address: p.address, output: Buffer.from(p.output), redeem: Buffer.from(redeem.output), send: "p2sh-p2wpkh" };
|
||||
}
|
||||
if (purpose === 86) {
|
||||
const internalPubkey = Buffer.from(pubkey.subarray(1, 33));
|
||||
const p = payments.p2tr({ internalPubkey, network: digibyte });
|
||||
return { family: "bip86", address: p.address, output: Buffer.from(p.output), internalPubkey, send: "p2tr" };
|
||||
}
|
||||
const p = payments.p2wpkh({ pubkey, network: digibyte });
|
||||
return { family: "bip84", address: p.address, output: Buffer.from(p.output), send: "p2wpkh" };
|
||||
}
|
||||
|
||||
// ---- keys --------------------------------------------------------------
|
||||
|
|
@ -62,7 +89,10 @@ module.exports = function makeDgbAdapter({
|
|||
// input under its own key.
|
||||
class WalletKeys {
|
||||
constructor(root32, accountPath) {
|
||||
const purpose = parsePurposeFromPath(accountPath) || DEFAULT_PURPOSE;
|
||||
const purpose = parsePurposeFromPath(accountPath) ?? DEFAULT_PURPOSE;
|
||||
if (!PURPOSES.has(purpose)) {
|
||||
throw new Error(`DGB: unsupported derivation purpose ${purpose} — use 44, 49, 84 or 86 (m/84'/${DGB_COIN_TYPE}'/0' is the default)`);
|
||||
}
|
||||
this._purpose = purpose;
|
||||
// bip32.fromSeed uses bitcoinjs-lib's Network object — pass DGB's so
|
||||
// extended keys serialize with the right BIP32 magic (0x0488B21E).
|
||||
|
|
@ -81,13 +111,16 @@ module.exports = function makeDgbAdapter({
|
|||
if (!e) {
|
||||
const node = this._branch[branch].derive(index);
|
||||
const pubkey = Buffer.from(node.publicKey);
|
||||
const address = p2wpkhAddress(node, digibyte);
|
||||
const script = Buffer.from(scriptPubKeyBuf(pubkey));
|
||||
const pay = paymentFor(this._purpose, node);
|
||||
const script = pay.output;
|
||||
e = {
|
||||
branch, index, path: this._accountPath + "/" + branch + "/" + index,
|
||||
publicKey: pubkey, script, scriptHex: script.toString("hex"),
|
||||
scripthash: scripthashOf(script),
|
||||
address,
|
||||
address: pay.address,
|
||||
family: pay.family, send: pay.send,
|
||||
redeemScript: pay.redeem || null,
|
||||
tapInternalKey: pay.internalPubkey || null,
|
||||
_node: node,
|
||||
};
|
||||
this._cache.set(k, e);
|
||||
|
|
@ -115,10 +148,11 @@ module.exports = function makeDgbAdapter({
|
|||
}
|
||||
|
||||
// ---- vsize model (fee estimation ahead of PSBT.getFee) -----------------
|
||||
// Input sizes per family, as in chain-btc.js.
|
||||
const OVERHEAD_VB = 10.5;
|
||||
const P2WPKH_INPUT_VB = 68;
|
||||
const P2WPKH_OUTPUT_VB = 31;
|
||||
const feeVb = (nIn, nOut, feePerVb) => Math.ceil((OVERHEAD_VB + nIn * P2WPKH_INPUT_VB + nOut * P2WPKH_OUTPUT_VB) * feePerVb);
|
||||
const OUTPUT_VB = 31;
|
||||
const INPUT_VB = { p2pkh: 148, "p2sh-p2wpkh": 91, p2wpkh: 68, p2tr: 58 };
|
||||
const feeVb = (kind, nIn, nOut, feePerVb) => Math.ceil((OVERHEAD_VB + nIn * (INPUT_VB[kind] || 68) + nOut * OUTPUT_VB) * feePerVb);
|
||||
|
||||
function scopedStorage(storage, keyPrefix) {
|
||||
const k = (key) => keyPrefix + key;
|
||||
|
|
@ -338,7 +372,7 @@ module.exports = function makeDgbAdapter({
|
|||
if (sendMax) {
|
||||
const chosen = spendable;
|
||||
const sum = chosen.reduce((a, u) => a + u.value, 0);
|
||||
const fee = feeVb(chosen.length, 1, rate);
|
||||
const fee = feeVb(change.send, chosen.length, 1, rate);
|
||||
if (sum <= fee) throw new Error("balance does not cover the fee");
|
||||
return {
|
||||
_chosen: chosen, _rate: rate, _to: dest, _sendMax: true, _change: change,
|
||||
|
|
@ -352,7 +386,7 @@ module.exports = function makeDgbAdapter({
|
|||
let sum = 0; const chosen = [];
|
||||
for (const u of spendable) {
|
||||
chosen.push(u); sum += u.value;
|
||||
const withChange = feeVb(chosen.length, 2, rate);
|
||||
const withChange = feeVb(change.send, chosen.length, 2, rate);
|
||||
if (sum >= value + withChange) {
|
||||
const changeVal = sum - value - withChange;
|
||||
const fee = changeVal > 546 ? withChange : sum - value;
|
||||
|
|
@ -370,11 +404,30 @@ module.exports = function makeDgbAdapter({
|
|||
}
|
||||
|
||||
async signAndBroadcast(plan) {
|
||||
const psbtInputs = plan._chosen.map((u) => ({
|
||||
txid: u.txid,
|
||||
vout: u.vout,
|
||||
witness: { scriptHex: u.entry.scriptHex, value: u.value },
|
||||
}));
|
||||
// buildPsbt orders inputs by BIP69; put ours in that same order first
|
||||
// so input i is signed with the key of the UTXO that actually sits at
|
||||
// i. Signing in selection order failed on any multi-address spend.
|
||||
const chosen = plan._chosen.slice().sort((a, b) => {
|
||||
const cmp = a.txid.localeCompare(b.txid);
|
||||
return cmp !== 0 ? cmp : a.vout - b.vout;
|
||||
});
|
||||
// P2PKH (BIP44) inputs need the whole previous transaction for the
|
||||
// legacy sighash; fetch each one before assembling the PSBT.
|
||||
const prevHex = new Map();
|
||||
const needsPrev = chosen.filter((u) => u.entry.family === "bip44");
|
||||
if (needsPrev.length) {
|
||||
const results = await Promise.all(needsPrev.map((u) => this._client.call("blockchain.transaction.get", [u.txid, false])));
|
||||
needsPrev.forEach((u, i) => prevHex.set(u.txid, String(results[i])));
|
||||
}
|
||||
const psbtInputs = chosen.map((u) => {
|
||||
const fam = u.entry.family;
|
||||
const inp = { txid: u.txid, vout: u.vout };
|
||||
if (fam === "bip44") inp.nonWitnessTxHex = prevHex.get(u.txid);
|
||||
else inp.witness = { scriptHex: u.entry.scriptHex, value: u.value };
|
||||
if (fam === "bip49") inp.redeemScriptHex = u.entry.redeemScript.toString("hex");
|
||||
if (fam === "bip86") inp.tapInternalKeyHex = u.entry.tapInternalKey.toString("hex");
|
||||
return inp;
|
||||
});
|
||||
const psbtOutputs = [{ address: plan._to, value: plan._sendMax ? plan.recipients[0].value : plan._value }];
|
||||
if (!plan._sendMax && plan._changeVal > 0) {
|
||||
psbtOutputs.push({ address: plan._change.address, value: plan._changeVal });
|
||||
|
|
@ -383,9 +436,16 @@ module.exports = function makeDgbAdapter({
|
|||
// Sign per-input with the exact key that funded that UTXO. signAllInputs
|
||||
// would work when all inputs share a key, but each derived address
|
||||
// has its own key, so we go per-input.
|
||||
for (let i = 0; i < plan._chosen.length; i++) {
|
||||
const signer = this._keys.signerFor(plan._chosen[i].entry);
|
||||
psbt.signInput(i, signer);
|
||||
for (let i = 0; i < chosen.length; i++) {
|
||||
const entry = chosen[i].entry;
|
||||
if (entry.family === "bip86") {
|
||||
// Taproot key-path: the signer must be the tap-tweaked key, which
|
||||
// ECPair.tweak() produces (as chain-btc.js does).
|
||||
const raw = ECPair.fromPrivateKey(Buffer.from(entry._node.privateKey), { network: digibyte });
|
||||
psbt.signInput(i, raw.tweak(bitcoinjs.crypto.taggedHash("TapTweak", entry.tapInternalKey)));
|
||||
} else {
|
||||
psbt.signInput(i, this._keys.signerFor(entry));
|
||||
}
|
||||
}
|
||||
const { hex, txid } = finalizeAndExtract(psbt);
|
||||
const broadcast = await this._client.call("blockchain.transaction.broadcast", [hex]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue