Skip to content

Commit 51348ca

Browse files
authored
Add handling and utilities for multisignature transactions (#55)
Add robust handling and utilities for multisignature transactions - Introduced new utilities in `multisig.ts` to handle multisignature decoding, parsing, and validation. - Enhanced transaction processing flows to support multisignature accounts and proper address derivation. - Updated `transactions.ts` and `validator.ts` to integrate multisign-specific functionality. - Improved error handling and added support for `Secp256k1` and `LegacyAminoPubKey` multisig types.
1 parent d3f9e3c commit 51348ca

File tree

7 files changed

+325
-16
lines changed

7 files changed

+325
-16
lines changed

project.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const dotenvPath = path.resolve(__dirname, `.env.${mode}`);
1414
dotenv.config({ path: dotenvPath });
1515

1616
const endpoints: string[] = process.env.ENDPOINT?.split(",") as string[];
17-
console.log(`Endpoints: ${endpoints}`);
17+
console.log(`Parsed Endpoints: ${endpoints}`);
1818

1919
// Can expand the Datasource processor types via the generic param
2020
const project: CosmosProject = {
@@ -424,6 +424,7 @@ const project: CosmosProject = {
424424
startBlock: 1,
425425
// migration at 25507 on alpha
426426
// msg grants at 23196 on alpha
427+
// mutlsig send tx at 39712 on beta
427428
kind: CosmosDatasourceKind.Runtime,
428429
mapping: {
429430
file: "./dist/index.js",

schema.graphql

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,19 @@ type ValidatorCommissionParams @jsonField {
232232
maxChangeRate: String!
233233
}
234234

235+
type Multisig @jsonField {
236+
from: String!
237+
all: [String]!
238+
signed: [String]!
239+
# Using indices, threshold, extraBitsStored, pubkeysBase64, bitarrayElems, extraBitsStored
240+
# allows to rebuild the fields: from, all and signed
241+
indices: [Int]!
242+
threshold: Int!
243+
extraBitsStored: Int!
244+
multisigPubKey: String!
245+
bitarrayElems: String!
246+
}
247+
235248
### ENTITIES
236249

237250
# Represent the balance of an account at the genesis state (usually genesis file)
@@ -328,8 +341,13 @@ type Transaction @entity {
328341
idx: Int!
329342
codespace: String
330343
timeoutHeight: BigInt @index
331-
# NB: only the first signer!
344+
# Mode = Single -> First signer
345+
# Mode = Multi -> Address from multisig public key
332346
signerAddress: String @index
347+
# indicates if the transaction is signed using /cosmos.crypto.multisig.LegacyAminoPubKey
348+
isMultisig: Boolean!
349+
# could be null is isMulti = false
350+
multisig: Multisig
333351
messages: [Message] @derivedFrom(field: "transaction")
334352
events: [Event]@derivedFrom(field: "transaction")
335353
}

src/mappings/pocket/validator.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import {
44
CosmosMessage,
55
} from "@subql/types-cosmos";
66
import type { MsgCreateValidator } from "cosmjs-types/cosmos/staking/v1beta1/tx";
7-
import { isNil } from "lodash";
7+
import {
8+
isEmpty,
9+
isNil,
10+
} from "lodash";
811
import { parseCoins } from "../../cosmjs/utils";
912
import {
1013
StakeStatus,
@@ -13,6 +16,7 @@ import {
1316
import { MsgCreateValidator as MsgCreateValidatorEntity } from "../../types/models/MsgCreateValidator";
1417
import { ValidatorCommissionProps } from "../../types/models/ValidatorCommission";
1518
import { ValidatorRewardProps } from "../../types/models/ValidatorReward";
19+
import { SignerInfo } from "../../types/proto-interfaces/cosmos/tx/v1beta1/tx";
1620
import { enforceAccountsExists } from "../bank";
1721
import {
1822
PREFIX,
@@ -25,6 +29,11 @@ import {
2529
messageId,
2630
} from "../utils/ids";
2731
import { stringify } from "../utils/json";
32+
import {
33+
extractThresholdAndPubkeysFromMultisig,
34+
getMultiSignPubKeyAddress,
35+
isMulti,
36+
} from "../utils/multisig";
2837
import {
2938
Ed25519,
3039
pubKeyToAddress,
@@ -36,19 +45,41 @@ async function _handleValidatorMsgCreate(msg: CosmosMessage<MsgCreateValidator>)
3645
const msgId = messageId(msg);
3746
const blockId = getBlockId(msg.block);
3847
const createValMsg = msg.msg.decodedMsg;
39-
const signer = msg.tx.decodedTx.authInfo.signerInfos[0];
4048

41-
if (isNil(signer) || isNil(signer.publicKey)) {
42-
throw new Error("Signer is nil");
49+
if (isEmpty(msg.tx.decodedTx.authInfo.signerInfos) || isNil(msg.tx.decodedTx.authInfo.signerInfos[0]?.publicKey)) {
50+
throw new Error(`[handleValidatorMsgCreate] (block ${msg.block.block.header.height}): hash=${msg.tx.hash} missing signerInfos public key`);
51+
}
52+
53+
const signerInfo = (msg.tx.decodedTx.authInfo.signerInfos as SignerInfo[])[0];
54+
55+
if (!signerInfo.publicKey) {
56+
throw new Error(`[handleValidatorMsgCreate] (block ${msg.tx.block.block.header.height}): hash=${msg.tx.hash} missing signerInfos public key`);
57+
}
58+
59+
const signerType = signerInfo.publicKey.typeUrl;
60+
let signerAddress, poktSignerAddress;
61+
62+
if (isMulti(signerInfo)) {
63+
// TODO: is this doable?
64+
// probably yes, but we should attempt to reproduce this and see if this
65+
// code satisfied this well enough
66+
const { pubkeysBase64, threshold } = extractThresholdAndPubkeysFromMultisig(signerInfo.publicKey.value);
67+
const { from: validatorFrom } = getMultiSignPubKeyAddress(pubkeysBase64, threshold, VALIDATOR_PREFIX);
68+
signerAddress = validatorFrom;
69+
const { from: poktValidatorFrom } = getMultiSignPubKeyAddress(pubkeysBase64, threshold, PREFIX);
70+
poktSignerAddress = poktValidatorFrom;
71+
} else if (signerType === Secp256k1) {
72+
signerAddress = pubKeyToAddress(Secp256k1, signerInfo.publicKey.value, VALIDATOR_PREFIX);
73+
poktSignerAddress = pubKeyToAddress(Secp256k1, signerInfo.publicKey.value, PREFIX);
74+
} else {
75+
signerAddress = `Unsupported Signer: ${signerType}`;
76+
poktSignerAddress = `Unsupported Signer: ${signerType}`;
4377
}
4478

4579
if (isNil(createValMsg.pubkey)) {
4680
throw new Error("Pubkey is nil");
4781
}
4882

49-
const signerAddress = pubKeyToAddress(Secp256k1, signer.publicKey.value, VALIDATOR_PREFIX);
50-
const poktSignerAddress = pubKeyToAddress(Secp256k1, signer.publicKey.value, PREFIX);
51-
5283
const msgCreateValidator = MsgCreateValidatorEntity.create({
5384
id: msgId,
5485
pubkey: {

src/mappings/primitives/genesis.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ async function _handleGenesisServices(genesis: Genesis, block: CosmosBlock): Pro
389389
memo: "",
390390
log: "",
391391
status: TxStatus.Success,
392+
isMultisig: false,
392393
code: 0,
393394
});
394395

@@ -447,6 +448,7 @@ async function _handleGenesisSuppliers(genesis: Genesis, block: CosmosBlock): Pr
447448
memo: "",
448449
log: "",
449450
timeoutHeight: BigInt(0),
451+
isMultisig: false,
450452
});
451453

452454
supplierMsgStakes.push({
@@ -574,6 +576,7 @@ async function _handleGenesisApplications(genesis: Genesis, block: CosmosBlock):
574576
memo: "",
575577
log: "",
576578
status: TxStatus.Success,
579+
isMultisig: false,
577580
code: 0,
578581
});
579582

@@ -687,6 +690,7 @@ async function _handleGenesisGateways(genesis: Genesis, block: CosmosBlock): Pro
687690
memo: "",
688691
log: "",
689692
status: TxStatus.Success,
693+
isMultisig: false,
690694
code: 0,
691695
});
692696

@@ -870,6 +874,7 @@ async function _handleGenesisGenTxs(genesis: Genesis, block: CosmosBlock): Promi
870874
memo: "",
871875
log: "",
872876
status: TxStatus.Success,
877+
isMultisig: false,
873878
code: 0,
874879
});
875880
}

src/mappings/primitives/transactions.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,79 @@ import {
33
isEmpty,
44
isNil,
55
} from "lodash";
6+
import { Multisig } from "../../types";
67
import { TransactionProps } from "../../types/models/Transaction";
8+
import { SignerInfo } from "../../types/proto-interfaces/cosmos/tx/v1beta1/tx";
79
import {
810
PREFIX,
911
VALIDATOR_PREFIX,
1012
} from "../constants";
1113
import { optimizedBulkCreate } from "../utils/db";
1214
import { getBlockId } from "../utils/ids";
15+
import {
16+
getMultisigInfo,
17+
isMulti,
18+
} from "../utils/multisig";
1319
import {
1420
getTxStatus,
1521
isMsgValidatorRelated,
1622
} from "../utils/primitives";
17-
import { pubKeyToAddress } from "../utils/pub_key";
23+
import {
24+
pubKeyToAddress,
25+
Secp256k1,
26+
} from "../utils/pub_key";
1827

1928
function _handleTransaction(tx: CosmosTransaction): TransactionProps {
2029
let signerAddress;
21-
if (isEmpty(tx.decodedTx.authInfo.signerInfos) || isNil(tx.decodedTx.authInfo.signerInfos[0]?.publicKey)) {
30+
31+
if (isEmpty(tx.decodedTx.authInfo.signerInfos) || isNil(tx.decodedTx.authInfo.signerInfos[0].publicKey)) {
2232
throw new Error(`[handleTransaction] (block ${tx.block.block.header.height}): hash=${tx.hash} missing signerInfos public key`);
23-
} else {
24-
const prefix = isMsgValidatorRelated(tx.decodedTx.body.messages[0].typeUrl) ? VALIDATOR_PREFIX : PREFIX;
25-
// if the first message is a MsgCreateValidator, we assume the signer is the account related to it,
26-
// that is hashed with a different prefix.
33+
}
34+
35+
const prefix = isMsgValidatorRelated(tx.decodedTx.body.messages[0].typeUrl) ? VALIDATOR_PREFIX : PREFIX;
36+
37+
const signerInfo = (tx.decodedTx.authInfo.signerInfos as SignerInfo[])[0];
38+
39+
if (!signerInfo.publicKey) {
40+
throw new Error(`[handleTransaction] (block ${tx.block.block.header.height}): hash=${tx.hash} missing signerInfos public key`);
41+
}
42+
43+
const signerType = signerInfo.publicKey.typeUrl;
44+
const isMultisig = isMulti(signerInfo);
45+
let multisigObject: Multisig | undefined;
46+
47+
if (isMultisig) {
48+
const {
49+
allSignerAddresses,
50+
bitarrayElems,
51+
extraBitsStored,
52+
fromAddress,
53+
multisigPubKey,
54+
signedSignerAddresses,
55+
signerIndices,
56+
threshold,
57+
} = getMultisigInfo(signerInfo);
58+
59+
// TODO: We should probably "create" this account otherwise maybe will not exists?
60+
signerAddress = fromAddress;
61+
multisigObject = {
62+
from: fromAddress,
63+
all: allSignerAddresses,
64+
signed: signedSignerAddresses,
65+
indices: signerIndices,
66+
threshold,
67+
multisigPubKey,
68+
bitarrayElems,
69+
extraBitsStored,
70+
};
71+
} else if (signerType === Secp256k1) {
2772
signerAddress = pubKeyToAddress(
28-
tx.decodedTx.authInfo.signerInfos[0]?.publicKey.typeUrl,
73+
signerType,
2974
tx.decodedTx.authInfo.signerInfos[0]?.publicKey.value,
3075
prefix,
3176
);
77+
} else {
78+
signerAddress = `Unsupported Signer: ${signerType}`;
3279
}
3380

3481
const feeAmount = !isNil(tx.decodedTx.authInfo.fee) ? tx.decodedTx.authInfo.fee.amount : [];
@@ -45,6 +92,8 @@ function _handleTransaction(tx: CosmosTransaction): TransactionProps {
4592
log: tx.tx.log || "",
4693
status: getTxStatus(tx),
4794
signerAddress,
95+
isMultisig,
96+
multisig: isMultisig ? multisigObject : undefined,
4897
code: tx.tx.code,
4998
codespace: tx.tx.codespace,
5099
};

0 commit comments

Comments
 (0)