Skip to content

Commit

Permalink
✨ Merge signatures for the same tx
Browse files Browse the repository at this point in the history
  • Loading branch information
doitian committed Feb 28, 2024
1 parent 7f71b55 commit 05d0ae4
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 7 deletions.
7 changes: 5 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ function findAddress(state, args) {
function Router() {
const [page, setPage] = useHash();
const [isPending, startTransition] = useTransition();
const [state, { addAddress, deleteAddress }] = usePersistReducer();
const [state, { addAddress, deleteAddress, addTransaction }] =
usePersistReducer();

const navigate = (url) => startTransition(() => setPage(url));

Expand All @@ -35,7 +36,9 @@ function Router() {
"#/addresses/import": () => (
<ImportAddressPage {...{ navigate, addAddress }} />
),
"#/transactions/import": () => <ImportTransactionPage />,
"#/transactions/import": () => (
<ImportTransactionPage {...{ navigate, addTransaction }} />
),
};
staticRoutes[""] = staticRoutes["#/"];
const dynamicRoutes = [
Expand Down
4 changes: 3 additions & 1 deletion src/ImportTransactionPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export default function ImportTransactionPage({ addTransaction, navigate }) {
const fileContent = await readAsText(fileInput.files[0]);
const transaction = importTransaction(JSON.parse(fileContent));
addTransaction(transaction);
navigate(`#/transaction/${transaction.hash}`);
navigate(
`#/transaction/${transaction.buildingPacket.value.payload.hash}`,
);
} catch (error) {
setState({
isProcessing: false,
Expand Down
127 changes: 126 additions & 1 deletion src/lib/transaction.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,128 @@
import {
Transaction,
getRawTransaction,
RawTransaction,
} from "@ckb-cobuild/ckb-molecule-codecs";
import { toJson } from "@ckb-cobuild/molecule";
import { ckbHasher } from "@ckb-cobuild/ckb-hasher";
import {
buildAction,
SCRIPT_INFO,
SCRIPT_INFO_HASH,
MultisigConfig,
} from "./multisig-lock-action.js";
import { convertDeprecatedSecp256k1Address } from "./multisig-address.js";
import { decodeCkbAddress } from "./ckb-address.js";

/**
* Import transaction from the ckb-cli tx JSON.
*
* Pay attention to following two issues:
*
* 1. There's no resolved inputs in the ckb-cli JSON. The transaction page need a feature to load resolved inputs from a CKB node via JSONRPC.
* 2. It does not who have provided the signature tell the signature. Fortunately, the pubkey can be recovered from the signature via `ecrecover`.
*/
export function importFromCkbCli(jsonContent) {
const payload = Transaction.parse({
hash: `0x${"0".repeat(64)}`,
...jsonContent.transaction,
});
payload.hash = ckbHasher()
.update(RawTransaction.pack(getRawTransaction(payload)))
.digest();

// If the tx file has any input with the secp256k1 lock, it's impossible to restore the witnesses.

const buildingPacket = {
type: "BuildingPacketV1",
value: {
message: { actions: [] },
// There's no resolved inputs info in ckb-cli tx.json
resolved_inputs: {
outputs: [],
outputs_data: [],
},
change_output: null,
script_infos: [SCRIPT_INFO],
// The tx file does not contain the resolved inputs. Without resolved inputs, it's impossible to compute the sighash. And without sighash, we cannot recover the pubkey.
lock_actions: [],
payload,
},
};

return toJson({
pendingSecp256k1Signatures: jsonContent.signatures,
buildingPacket,
});
}

export function importTransaction(jsonContent) {
throw new Error("Not a valid JSON");
if (
"transaction" in jsonContent &&
"multisig_configs" in jsonContent &&
"signatures" in jsonContent
) {
return importFromCkbCli(jsonContent);
}
throw new Error("Unknown JSON format");
}

export function resolvePendingSecp256k1Signatures(transaction) {
// TODO: implement
return transaction;
}

export function mergeTransaction(target, from) {
// 1. Merge lockActions
for (const lockAction of from.buildingPacket.value.lock_actions) {
// Assume that Action.script_hash must be unique
const existing = target.buildingPacket.value.lock_actions.find(
(item) => item.script_hash === lockAction.script_hash,
);
if (existing === undefined) {
target.buildingPacket.value.lock_actions.push(lockAction);
} else if (lockAction.script_info_hash != toJson(SCRIPT_INFO_HASH)) {
// non-multisig lock action, just overwrite
Object.assign(existing, lockAction);
} else {
const existingData = MultisigConfig.unpack(existing.data);
const newData = MultisigConfig.unpack(lockAction.data);
for (const sig of newData.signed) {
const newPubKeyHash = toJson(sig.pubkey_hash);
const existingSigIndex = existingData.findIndex(
(item) => toJson(item.pubkey_hash) === newPubKeyHash,
);
if (existingSigIndex !== -1) {
existingData.signed.splice(existingSigIndex, 1);
}
existingData.signed.push(sig);
}
}
}

// 2. Merge pendingSecp256k1Signatures
for (const [args, signatures] of Object.entries(
from.pendingSecp256k1Signatures,
)) {
if (args in target.pendingSecp256k1Signatures) {
for (const signature of signatures) {
if (!target.pendingSecp256k1Signatures[args].includes(signature)) {
target.pendingSecp256k1Signatures[args].push(signature);
}
}
} else {
target.pendingSecp256k1Signatures[args] = signatures;
}
}

// 3. Merge witnesses
for (const [i, witness] of from.buildingPacket.value.payload.witnesses) {
if (witness !== null && witness !== undefined && witness !== "0x") {
target.buildingPacket.value.payload.witnesses[i] = witness;
}
}

if (target.buildingPacket.value.resolved_inputs.outputs.length > 0) {
resolvePendingSecp256k1Signatures(target);
}
}
51 changes: 48 additions & 3 deletions src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,34 @@ import { current } from "immer";
import { useCallback } from "react";
import { useLocalStorage } from "react-use";
import { useImmerReducer } from "use-immer";
import { mergeTransaction } from "./lib/transaction.js";

const LOCAL_STORAGE_KEY = "ckb-multisig";

const INITIAL_STATE = {
addresses: [],
transactions: [],
};

function deleteAddressByArgs(draft, args) {
const index = draft.addresses.findIndex((address) => address.args === args);
if (index !== -1) {
draft.addresses.splice(index, 1);
return draft.addresses.splice(index, 1)[0];
}
}

function findTransactionByHash(draft, hash) {
return draft.transactions.find(
(tx) => tx.buildingPacket.value.payload.hash === hash,
);
}

function deleteTransactionByHash(draft, hash) {
const index = draft.transactions.findIndex(
(tx) => tx.buildingPacket.value.payload.hash === hash,
);
if (index !== -1) {
return draft.transactions.splice(index, 1)[0];
}
}

Expand All @@ -30,6 +47,20 @@ function reducer(draft, action) {
case "deleteAddress":
deleteAddressByArgs(draft, action.payload);
break;
case "addTransaction":
const existing = findTransactionByHash(
draft,
action.payload.buildingPacket.value.payload.hash,
);
if (existing === undefined) {
draft.transactions.push(action.payload);
} else {
mergeTransaction(existing, action.payload);
}
break;
case "deleteTransaction":
deleteTransactionByHash(draft, action.payload);
break;
default:
throw new Error(`Unknown action type ${action.type}`);
}
Expand Down Expand Up @@ -58,7 +89,10 @@ const usePersistReducer = () => {
// use wrapped reducer and the saved value from
// `localStorage` as params to `useReducer`.
// this will return `[state, dispatch]`
const [state, dispatch] = useImmerReducer(reducerLocalStorage, savedState);
const [state, dispatch] = useImmerReducer(reducerLocalStorage, {
...INITIAL_STATE,
...savedState,
});
const addAddress = useCallback(
(address) => dispatch({ type: "addAddress", payload: address }),
[dispatch],
Expand All @@ -67,7 +101,18 @@ const usePersistReducer = () => {
(args) => dispatch({ type: "deleteAddress", payload: args }),
[dispatch],
);
return [state, { addAddress, deleteAddress }];
const addTransaction = useCallback(
(transaction) => dispatch({ type: "addTransaction", payload: transaction }),
[dispatch],
);
const deleteTransaction = useCallback(
(hash) => dispatch({ type: "deleteTransaction", payload: hash }),
[dispatch],
);
return [
state,
{ addAddress, deleteAddress, addTransaction, deleteTransaction },
];
};

export default usePersistReducer;

0 comments on commit 05d0ae4

Please sign in to comment.