Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to make 2 messages transaction with 2 signers #10843

Closed
4 tasks
fulldiver-ilya opened this issue Dec 27, 2021 · 4 comments
Closed
4 tasks

Unable to make 2 messages transaction with 2 signers #10843

fulldiver-ilya opened this issue Dec 27, 2021 · 4 comments

Comments

@fulldiver-ilya
Copy link

fulldiver-ilya commented Dec 27, 2021

Summary of Bug

I want to implement 2 signers 2 messages transaction from a client via gRPC, but I always receive the same error:

signature verification failed; please verify account number (x) and chain-id (y): unauthorized

x — is an account number of transaction composer and he is the first signer
y — is a correct chain-id of my node

Version

cosmos-sdk v0.44.2

Steps to Reproduce

The flow is pretty simple:

Alice is a first signer and transaction composer
Bob is a second signer, fee payer and user who broadcast complete transaction

Alice prepare the transaction, that looks like

{
    "body": {
        "messages": [
            {
                "@type": "send-something-message",
                "sender": "alice-address",
                "recipient": "bob-address"
            },
            {
                "@type": "send-penny-message",
                "sender": "bob-address",
                "recipient": "alice-address"
            }
        ]
    },
    "auth_info": {
        "signer_infos": [
            {
                "public_key": {
                    "@type": "/cosmos.crypto.secp256k1.PubKey",
                    "key": "alice-public-key"
                },
                "mode_info": {
                    "single": {
                        "mode": "SIGN_MODE_DIRECT_AUX"
                    }
                },
                "sequence": "1"
            }
        ],
        "fee": {
            "amount": [some-coins],
            "gas_limit": "200000",
            "payer": "bob-address",
            "granter": ""
        }
    },
    "signatures": [
        "alice-signature-with-sign-doc-aux"
    ]
}

Alice encode that json into an array of bytes as a partial tx request and send it to Bob with 3party transport

Bob receive that bytes array and decode it into Protobuff messages, he read all the things inside that transaction and add his own signature (DIRECT)

{
    ...

    "auth_info": {
        "signer_infos": [
            {
                "public_key": {
                    "@type": "/cosmos.crypto.secp256k1.PubKey",
                    "key": "alice-public-key"
                },
                "mode_info": {
                    "single": {
                        "mode": "SIGN_MODE_DIRECT_AUX"
                    }
                },
                "sequence": "1"
            },
            {
                "public_key": {
                    "@type": "/cosmos.crypto.secp256k1.PubKey",
                    "key": "bob-public-key"
                },
                "mode_info": {
                    "single": {
                        "mode": "SIGN_MODE_DIRECT"
                    }
                },
                "sequence": "3"
            }
        ],
        ...
    },
    "signatures": [
        "alice-signature-with-sign-doc-aux",
        "bob-signature-with-sign-doc"
    ]
}

and after that Bob broadcast that complete tx request and receive an error described earlier.

There is 2 raw code snippets with Swift language

  1. Generating transaction semi-signed by Alice
static func prepareSemiSignedTransaction(
    messages: [(route: String, data: Data)],
    fees: (fee: Fee, payer: String),
    keys: Wallet.Mnemonic.RawKeys, // (`private`: Data, `public`: Data) generated by HDWalletKit swift library
    environment: Environment // all the fields here obtained right before calling that method by querying node and account info
) -> Data {
    let body = Cosmos_Tx_V1beta1_TxBody.with({
        $0.messages = messages.map({ m in
            Google_Protobuf_Any.with({
                $0.typeURL = m.route
                $0.value = m.data
            })
        })
    })
    
    let authInfo = Cosmos_Tx_V1beta1_AuthInfo.with({
        $0.fee = Cosmos_Tx_V1beta1_Fee.with({
            $0.payer = fees.payer
            $0.gasLimit = fees.fee.gasLimit
            $0.amount = [
                Cosmos_Base_V1beta1_Coin.with({
                    $0.amount = String(fees.fee.coins.amount)
                    $0.denom = fees.fee.coins.denom.rawValue
                })
            ]
        })
        $0.signerInfos = [
            Cosmos_Tx_V1beta1_SignerInfo.with({
                $0.publicKey = Google_Protobuf_Any.with({
                    $0.typeURL = "/cosmos.crypto.secp256k1.PubKey"
                    $0.value = try! Cosmos_Crypto_Secp256k1_PubKey.with({
                        $0.key = keys.public
                    }).serializedData()
                })
                $0.sequence = environment.account.sequence
                $0.modeInfo = Cosmos_Tx_V1beta1_ModeInfo.with({
                    $0.single = Cosmos_Tx_V1beta1_ModeInfo.Single.with({
                        $0.mode = .directAux
                    })
                })
            })
        ]
    })
    
    let signDoc = Cosmos_Tx_V1beta1_SignDocDirectAux.with({
        $0.chainID = environment.node.network
        $0.accountNumber = environment.account.number
        $0.sequence = environment.account.sequence
        $0.publicKey = Google_Protobuf_Any.with({
            $0.typeURL = "/cosmos.crypto.secp256k1.PubKey"
            $0.value = try! Cosmos_Crypto_Secp256k1_PubKey.with({
                $0.key = keys.public
            }).serializedData()
        })
        $0.bodyBytes = try! body.serializedData()
    })
    
    let hash = try! signDoc.serializedData().sha256()
    let signature = try! ECDSA.compactsign(hash, privateKey: keys.private)
    
    let rawTx = Cosmos_Tx_V1beta1_TxRaw.with({
        $0.bodyBytes = try! body.serializedData()
        $0.authInfoBytes = try! authInfo.serializedData()
        $0.signatures = [signature]
    })
    
    return try! rawTx.serializedData(partial: true)
}

(private: Data, public: Data) generated by HDWalletKit swift library

That keys works fine when I use them with single message (single signer) transactions

  1. Bob's signing part after receiving a partially serialized transaction from Alice
static func complete(
    semiSigned partialTransaction: Data,
    keys: Wallet.Mnemonic.RawKeys,
    environment: Environment
) -> Cosmos_Tx_V1beta1_BroadcastTxRequest {
    var partialRawTx = try! Cosmos_Tx_V1beta1_TxRaw(serializedData: partialTransaction, partial: true)
    let body = try! Cosmos_Tx_V1beta1_TxBody(serializedData: partialRawTx.bodyBytes)
    var authInfo = try! Cosmos_Tx_V1beta1_AuthInfo(serializedData: partialRawTx.authInfoBytes)
    
    authInfo.signerInfos.append({
        Cosmos_Tx_V1beta1_SignerInfo.with({
            $0.publicKey = Google_Protobuf_Any.with({
                $0.typeURL = "/cosmos.crypto.secp256k1.PubKey"
                $0.value = try! Cosmos_Crypto_Secp256k1_PubKey.with({
                    $0.key = keys.public
                }).serializedData()
            })
            $0.sequence = environment.account.sequence
            $0.modeInfo = Cosmos_Tx_V1beta1_ModeInfo.with({
                $0.single = Cosmos_Tx_V1beta1_ModeInfo.Single.with({
                    $0.mode = .direct
                })
            })
        })
    }())
    
    let signDoc = Cosmos_Tx_V1beta1_SignDoc.with({
        $0.accountNumber = environment.account.number
        $0.chainID = environment.node.network
        $0.bodyBytes = try! body.serializedData()
        $0.authInfoBytes = try! authInfo.serializedData()
    })
    
    let hash = try! signDoc.serializedData().sha256()
    let signature = try! ECDSA.compactsign(hash, privateKey: keys.private)
    partialRawTx.signatures.append(signature)
    
    let completeRawTx = Cosmos_Tx_V1beta1_TxRaw.with({
        $0.bodyBytes = try! body.serializedData()
        $0.authInfoBytes = try! authInfo.serializedData()
        $0.signatures = partialRawTx.signatures
    })
    
    return Cosmos_Tx_V1beta1_BroadcastTxRequest.with({
        $0.mode = .sync
        $0.txBytes = try! completeRawTx.serializedData()
    })
}

I tried to use .legacyAminoJson with custom StdSignDoc with sorted keys when encoding that struct to json at Alice step

struct StdSignDoc: Encodable {
    struct Coin: Encodable {
        let denom: String
        let amount: UInt64
        
        enum CodingKeys: String, CodingKey {
            case denom  = "denom"
            case amount = "amount"
        }
    }
    
    struct Fee: Encodable {
        let amount: [Coin]
        let gas: UInt64
        let payer: String
        
        enum CodingKeys: String, CodingKey {
            case amount = "amount"
            case gas    = "gas"
            case payer  = "payer"
        }
    }
    
    struct AminoMsg: Encodable {
        let type: String
        let value: String // json-encoded tx-message
        
        enum CodingKeys: String, CodingKey {
            case type   = "type"
            case value  = "value"
        }
    }
    
    let accountNumber: UInt64
    let sequence: UInt64
    let chainId: String
    let memo: String            = ""
    let fee: Fee
    let messages: [AminoMsg]
    
    enum CodingKeys: String, CodingKey {
        case accountNumber  = "account_number"
        case sequence       = "sequence"
        case chainId        = "chain_id"
        case memo           = "memo"
        case fee            = "fee"
        case messages       = "msgs"
    }
}
...
let signDoc = StdSignDoc(
    accountNumber: environment.account.number,
    sequence: environment.account.sequence,
    chainId: environment.node.network,
    fee: .init(
        amount: [
            .init(denom: fees.fee.coins.denom.rawValue, amount: fees.fee.coins.amount)
        ],
        gas: fees.fee.gasLimit,
        payer: fees.payer
    ),
    messages: messages.compactMap({
        guard let data = $0.message.jsonData, let jsonString = String(data: data, encoding: .utf8) else {
            return nil
        }
        return .init(type: $0.route, value: jsonString)
    })
)


let signDocEncoder: JSONEncoder = {
    let e = JSONEncoder()
    e.outputFormatting = .sortedKeys
    return e
}()

let hash = try! signDocEncoder.encode(signDoc).sha256()
let signature = try! ECDSA.compactsign(hash, privateKey: keys.private)
...

but I always get the same error :]

Does anyone know it is valid usage of gRPC cosmos API? And what I'm doing wrong?


For Admin Use

  • Not duplicate issue
  • Appropriate labels applied
  • Appropriate contributors tagged
  • Contributor assigned/self-assigned
@fulldiver-ilya
Copy link
Author

And there is a related question to my flow described above.

Alice made a semi-signed transaction with Bob, and then Alice made another one with Charlie.
After some time, Charlie completes their transaction with Alice. Does it mean that Bob's transaction will always fail after that? Because Alice's sequence number increased with Charlie's transaction?

@fulldiver-ilya
Copy link
Author

fulldiver-ilya commented Dec 28, 2021

Update

About main comment #10843 (comment)

That was surprising for me (my fault) but /master branch contains a lot of stuff that is not included in 0.44.2
DIRECT_AUX for example, or fee.payer, etc. Never mind 🙈

When I rollback my client *.proto files to 0.44.2 I was forced to use .legacyAminoJson at Alice step. After logging the result of StdSignBytes from cosmos-sdk on my localhost I found a lot of mistakes (incorrect types, escaping slashes, some fields, etc..) with StdSignDoc and json serialized from that object.

In general: my main issue is now resolved and workflow works fine 🎉

But I interested in #10843 (comment) if anyone could answer it will be very helpfully for me :]

@aaronc
Copy link
Member

aaronc commented Jan 3, 2022

And there is a related question to my flow described above.

Alice made a semi-signed transaction with Bob, and then Alice made another one with Charlie. After some time, Charlie completes their transaction with Alice. Does it mean that Bob's transaction will always fail after that? Because Alice's sequence number increased with Charlie's transaction?

That's correct. The sequence number will increase.

Glad you have this figured out 👍 . Okay to close this issue now?

@fulldiver-ilya
Copy link
Author

Yea, thanks, could be closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants