cosmjs icon indicating copy to clipboard operation
cosmjs copied to clipboard

Broadcasting a custom multisigned transaction

Open stanfilip0v opened this issue 3 years ago • 8 comments

Hi there.

I'm trying to broadcast a custom transaction from a multisig which has collected the required number of signatures, but for some reason I'm getting Broadcasting transaction failed with code 4 (codespace: sdk). Log: signature verification failed; please verify account number (3), sequence (8) and chain-id (custom-network): unauthorized I've double checked all of the 3 given properties and they're all correct.

I'm assuming it has something to do with the custom registry and custom aminoTypes that I have to pass to the SigningStargateClient.offline/connectWithSigner() functions. Right now I'm not really sure what the prefix needs to be in this case. Here's a sample code

window.keplr.defaultOptions = {
  sign: {
    preferNoSetMemo: true,
    preferNoSetFee: true,
    disableBalanceCheck: true,
  },
};

const offlineSigner = window.getOfflineSignerOnlyAmino(
      process.env.REACT_APP_CHAIN_ID
);

const typeUrl =
      "/custom.MsgCustom";

const customRegistry = new Registry(defaultRegistryTypes);
customRegistry.register(
  typeUrl,
  MsgCustom as GeneratedType
);

const customAminoTypes = new AminoTypes({
  prefix: "custom?", // not sure if this has to be "cosmos" or the custom prefix of our bech32addresses
  additions: {
    "/custom.MsgCustom": {
      aminoType: "custom/MsgCustom",
      toAmino: ({
        initiator,
        toAddress,
        coins,
      }) => {
        return {
          initiator,
          to_address: toAddress,
          coins: [...coins],
        };
      },
      fromAmino: ({
        initiator,
        to_address,
        coins,
      }) => {
        return {
          initiator,
          toAddress: to_address,
          coins: [...coins],
        };
      },
    },
  },
});

const signingClient = await SigningStargateClient.offline(
  offlineSigner,
  { registry: customRegistry, aminoTypes: customAminoTypes, prefix: "custom?" } // not sure if this has to be "cosmos" or the custom prefix of our bech32addresses
);

await window.keplr.enable(process.env.REACT_APP_CHAIN_ID);
const walletAccount = await window.keplr.getKey(
  process.env.REACT_APP_CHAIN_ID
);

const message = {
  initiator: "custom1z2all86gvvkluus6c3sv6el9725wk7lfr9yaqy", // Address of the multisig itself
  toAddress: "custom1eh3tdj03xmc6re77tyrdqktsx05tacp26vawlp",
  coins: [
    {
      denom: config.denom,
      amount: "10000000000000000000",
    },
  ],
};

const messages = [
  {
    typeUrl,
    value: message,
  },
];

const msgFee = {
  amount: [
    {
      denom: config.denom,
      amount: "180000",
    },
  ],
  gas: "200000",
};

const msgMemo = "Custom Transaction";

const signerData = {
  accountNumber,
  sequence,
  chainId
};

const { signatures, bodyBytes } = await signingClient.sign(
  walletAccount.bech32Address,
  messages,
  msgFee,
  msgMemo,
  signerData
);

I should add that I was able to sign and broadcast this custom transaction with a regular (not a multisig) wallet. Main difference was that we don't need to use window.getOfflineSignerOnlyAmino which means we don't need to pass in aminoTypes to the SigningStargateClient.offline/connectWithSigner() functions. The signing mode ends up being SIGN_MODE_DIRECT and everything goes smoothly

stanfilip0v avatar Mar 16 '22 12:03 stanfilip0v

Right now makeMultisignedTx hardcodes SIGN_MODE_LEGACY_AMINO_JSON for all signers. This is an implementation limitation that was used to get something working and it allows Ledger signing. In theory it would be possible to use SIGN_MODE_DIRECT too but this needs to be implemented and tested in CosmJS.

Now what you should do in order to break down the problem is figuring out of SIGN_MODE_LEGACY_AMINO_JSON works at all for your custom type. The way you create the Amino converter above looks roughly right. To do that get rid of all the offline signing and multisig code and try to send your message type from a simple account. Ideally you even use the Secp256k1HdWallet from @cosmjs/amino to do Amino Signing without Ledger.

webmaster128 avatar Mar 16 '22 14:03 webmaster128

@webmaster128 Well I've been able to broadcast this type of transaction through the CLI. And the auth_info in the final json file holding the signed transaction looks like this

"auth_info": {
  "signer_infos": [
    {
      "public_key": {
        "@type": "/cosmos.crypto.multisig.LegacyAminoPubKey",
        "threshold": 2,
        "public_keys": [
          {
            "@type": "/cosmos.crypto.secp256k1.PubKey",
            "key": "A1VVrpTWviVDpVQA2F99k3SoeqH46nHPWG1EOXe3Upvy"
          },
          {
            "@type": "/cosmos.crypto.secp256k1.PubKey",
            "key": "Aq+YVg8lhwoif6Fq+UMAOgzVO8O7aFAkADeCWC/x/21r"
          }
        ]
      },
      "mode_info": {
        "multi": {
          "bitarray": { "extra_bits_stored": 2, "elems": "wA==" },
          "mode_infos": [
            { "single": { "mode": "SIGN_MODE_LEGACY_AMINO_JSON" } },
            { "single": { "mode": "SIGN_MODE_LEGACY_AMINO_JSON" } }
          ]
        }
      },
      "sequence": "3"
    }
  ],

That should be enough to prove that our custom message works with SIGN_MODE_LEGACY_AMINO_JSON right?

stanfilip0v avatar Mar 16 '22 16:03 stanfilip0v

That should be enough to prove that our custom message works with SIGN_MODE_LEGACY_AMINO_JSON right?

Unfortunately no. We have seen 2 examples in the wild where Amino JSON signing with Ledger works in the CLI by accident but is buggy. The first one was the Stargate airdrop claim message and the second one is described here: https://medium.com/confio/authz-and-ledger-signing-an-executive-summary-8754a0dc2a88

webmaster128 avatar Mar 16 '22 16:03 webmaster128

@webmaster128 Thanks for clearing that up. One more question. We have the option to generate the transaction using this rest method. The generated tx looks like this

{
    "type": "cosmos-sdk/StdTx",
    "value": {
        "msg": [
            {
                "type": "admin/AdminSpendCommunityPool",
                "value": {
                    "initiator": "cudos1qy7a8qvmqtqrscz7rf9l3xlllm0l6x3xnmarze",
                    "to_address": "cudos1w93na52aw66vcp3rscn5s4mf4uu92qhnqn63rp",
                    "coins": [
                        {
                            "denom": "acudos",
                            "amount": "1000000"
                        }
                    ]
                }
            }
        ],
        "fee": {
            "amount": [],
            "gas": "200000"
        },
        "signatures": [],
        "memo": "",
        "timeout_height": "0"
    }
}

Can we in any way just sign this given JSON or are the signing methods from the SigningStargateClient the only option?

stanfilip0v avatar Mar 17 '22 09:03 stanfilip0v

No, the structure of that JSON is similar but not equal to the structure you need (see makeSignDoc/StdSignDoc). There is no shortcut.

webmaster128 avatar Mar 17 '22 13:03 webmaster128

Hi @webmaster128 any idea why we would encounter same issue with the basic signer?

import {lavanet, getSigningLavanetClient} from '@lavanet/lavajs';
import { Secp256k1Wallet, Secp256k1HdWallet  } from '@cosmjs/amino'
import { fromHex } from "@cosmjs/encoding";
import Long from 'long';
import { SigningStargateClient } from "@cosmjs/stargate";

// sending queries specifically to lava
async function run() {
  // create a lava client factory  
  const client = await lavanet.ClientFactory.createRPCQueryClient({ rpcEndpoint: publicRpc})
  let wallet = await Secp256k1Wallet.fromKey(fromHex(privKey), "lava@")
  const [firstAccount] = await wallet.getAccounts();
  console.log(firstAccount)
  let signingClient = await getSigningLavanetClient({
    rpcEndpoint: publicRpc,
    signer: wallet,
  })
  // buy a subscription (lava tx)
  const msg = lavanet.lava.subscription.MessageComposer.withTypeUrl.buy({
    creator: firstAccount.address,
    consumer: firstAccount.address,
    index: "explorer",
    duration: new Long(1), /* in months */
  })
  const fee = {
    amount: [{ amount: "1", denom: "ulava" }], // Replace with the desired fee amount and token denomination
    gas: "50000000", // Replace with the desired gas limit
  }
  console.log(await signingClient.signAndBroadcast(firstAccount.address,[msg], fee, "Buying subscription on Lava blockchain!"))
}

run()

we recently upgraded to cosmos 47.3 and using cosmology/telescope's compiler we are building the lavajs aminos additionally we upgraded the cosmjs dependencies

"dependencies": {
    "@cosmjs/amino": "^0.31.1",
    "@cosmjs/proto-signing": "^0.31.1",
    "@cosmjs/stargate": "^0.31.1",
    "@cosmjs/tendermint-rpc": "^0.31.1",
    "@cosmology/lcd": "^0.12.0"
  }
  

KodamaHQ avatar Aug 29 '23 14:08 KodamaHQ

I don't know. Signature verification errors can have a lot of different causes. You can try DirectSecp256k1Wallet instead of Secp256k1Wallet to see if it works with direct sign mode instead of Amino JSON signing.

webmaster128 avatar Aug 29 '23 14:08 webmaster128

I don't know. Signature verification errors can have a lot of different causes. You can try DirectSecp256k1Wallet instead of Secp256k1Wallet to see if it works with direct sign mode instead of Amino JSON signing.

@webmaster128 the direct wallet worked! any idea why the amino signer wouldn't work?

KodamaHQ avatar Aug 30 '23 12:08 KodamaHQ