Unable to make grpc call to SubmitTransactionRequest successfully

Update: For anyone coming here looking for C# sample code it’s at https://gist.github.com/sipsorcery/9420892747104d72054e6504317d3b24.

I’ve been attempting to call SubmitTransaction against the testnet grpc endpoint at ac.testnet.libra.org:8000. To date I have not been able to get a transaction accepted into the Libra state. My C# sample code is at the end of this post.

The response to my call has AcStatus as Accepted which I believe indicated the signature on the transaction is good? But the submitted transaction never shows up in the state indicating there is probably something I’m doing wrong with the raw transaction serialisation.

As a separate test I have generated an unsigned RawTransaction blob and successfully submitted it using the Libra cli with the submit command:

libra% submit 9c147f48c31695955d78955766eb796ff300bc438b1ce79e465672fad09ca8cb /tmp/libra_tx.raw

I have a couple of questions:

  • Are there any examples of how to call SubmitTransaction on ac.testnet.libra.org:8000?
  • Is there any plan to add something like submitraw to the Libra CLI? It’s invaluable for testing to be able to submit signed transactions against a local node.
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Google.Protobuf;
using Grpc.Core;
using NBitcoin.DataEncoders;
using NSec.Cryptography;

namespace LibraTestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                HexEncoder hex = new HexEncoder();

                SharedSecret sharedSecret = SharedSecret.Import(Encoding.UTF8.GetBytes("dummy"));
                HkdfSha512 kdf = new HkdfSha512();
                var key = kdf.DeriveKey(sharedSecret, null, null, Ed25519.Ed25519);
                var sender = key.PublicKey.Export(KeyBlobFormat.RawPublicKey);

                Channel channel = new Channel("ac.testnet.libra.org:8000", ChannelCredentials.Insecure);
                var client = new AdmissionControl.AdmissionControl.AdmissionControlClient(channel);

                UInt64 seqNum = 0;
                string senderHex = hex.EncodeData(sender);

                var rawTx = CreateRawTx(senderHex, seqNum, "9c147f48c31695955d78955766eb796ff300bc438b1ce79e465672fad09ca8cb", 1UL, 10000UL, 0, 0UL);

                Console.WriteLine($"RawTx: {Convert.ToBase64String(rawTx.ToByteArray())}");

                Types.SignedTransaction signedTx = new Types.SignedTransaction();
                signedTx.SenderPublicKey = Google.Protobuf.ByteString.CopyFrom(sender);
                signedTx.RawTxnBytes = rawTx.ToByteString();
                var sig = NSec.Cryptography.Ed25519.Ed25519.Sign(key, signedTx.RawTxnBytes.Span);
                signedTx.SenderSignature = Google.Protobuf.ByteString.CopyFrom(sig);

                AdmissionControl.SubmitTransactionRequest submitTxReq = new AdmissionControl.SubmitTransactionRequest();
                submitTxReq.SignedTxn = signedTx;

                Console.WriteLine($"Submitting signed tx for {senderHex} and seqnum {seqNum}.");

                var reply = client.SubmitTransaction(submitTxReq);
                Console.WriteLine($"Reply AcStatus {reply.AcStatus}.");

                GetTransaction(client, senderHex, seqNum);
            }
            catch (Exception excp)
            {
                Console.WriteLine($"Exception Main. {excp.Message}");
            }
        }

        private static Types.RawTransaction CreateRawTx(string senderHex, UInt64 seqNum, string receipientHex, UInt64 recipientAmount, UInt64 maxGasAmount, UInt64 maxGasUnitPrice, UInt64 expirationTime)
        {
            HexEncoder hex = new HexEncoder();

            Types.RawTransaction rawTx = new Types.RawTransaction();
            rawTx.SenderAccount = Google.Protobuf.ByteString.CopyFrom(hex.DecodeData(senderHex));
            rawTx.SequenceNumber = seqNum;
            rawTx.Program = new Types.Program();
            rawTx.Program.Code = Google.Protobuf.ByteString.CopyFrom(Convert.FromBase64String("TElCUkFWTQoBAAcBSgAAAAQAAAADTgAAAAYAAAAMVAAAAAUAAAANWQAAAAQAAAAFXQAAACkAAAAEhgAAACAAAAAHpgAAAA0AAAAAAAABAAIAAQMAAgACBAIDAgQCBjxTRUxGPgxMaWJyYUFjY291bnQEbWFpbg9wYXlfZnJvbV9zZW5kZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgAEAAwADAERAQI="));

            var recipientArg = new Types.TransactionArgument { Type = Types.TransactionArgument.Types.ArgType.Address };
            recipientArg.Data = Google.Protobuf.ByteString.CopyFrom(hex.DecodeData(receipientHex));
            rawTx.Program.Arguments.Add(recipientArg);

            var amountArg = new Types.TransactionArgument { Type = Types.TransactionArgument.Types.ArgType.U64 };
            amountArg.Data = Google.Protobuf.ByteString.CopyFrom(BitConverter.GetBytes(recipientAmount));
            rawTx.Program.Arguments.Add(amountArg);

            rawTx.MaxGasAmount = maxGasAmount;
            rawTx.GasUnitPrice = maxGasUnitPrice;
            rawTx.ExpirationTime = expirationTime;

            return rawTx;
        }

        private static void GetTransaction(AdmissionControl.AdmissionControl.AdmissionControlClient client, string accountHex, UInt64 seqNum)
        {
            Console.WriteLine($"GetTransaction for {accountHex} and seqnum {seqNum}.");

            HexEncoder hex = new HexEncoder();

            Types.UpdateToLatestLedgerRequest updToLatestLedgerReq = new Types.UpdateToLatestLedgerRequest();
            var getTxReq = new Types.GetAccountTransactionBySequenceNumberRequest();
            getTxReq.SequenceNumber = seqNum;
            getTxReq.Account = Google.Protobuf.ByteString.CopyFrom(hex.DecodeData(accountHex));
            Types.RequestItem reqItem = new Types.RequestItem();
            reqItem.GetAccountTransactionBySequenceNumberRequest = getTxReq;
            updToLatestLedgerReq.RequestedItems.Add(reqItem);
            var reply = client.UpdateToLatestLedger(updToLatestLedgerReq);

            if (reply?.ResponseItems?.Count == 1)
            {
                var resp = reply.ResponseItems[0].GetAccountTransactionBySequenceNumberResponse;

                if (resp.SignedTransactionWithProof == null)
                {
                    Console.WriteLine("GetTransaction request did not return a signed transaction.");
                }
                else
                {
                    var signedTx = resp.SignedTransactionWithProof;

                    Console.WriteLine($"Sender {hex.EncodeData(signedTx.SignedTransaction.SenderPublicKey.ToByteArray())}.");
                    Console.WriteLine($"RawTxnBytes {hex.EncodeData(signedTx.SignedTransaction.RawTxnBytes.ToByteArray())}");

                    Types.RawTransaction rawTx = Types.RawTransaction.Parser.ParseFrom(signedTx.SignedTransaction.RawTxnBytes);

                    Console.WriteLine($"SequenceNumber {rawTx.SequenceNumber}.");
                    Console.WriteLine($"MaxGasAmount {rawTx.MaxGasAmount}.");
                    Console.WriteLine($"GasUnitPrice {rawTx.GasUnitPrice}.");
                    Console.WriteLine($"ExpirationTime {rawTx.ExpirationTime}.");

                    var byteCode = rawTx.Program.Code.ToByteArray();

                    SHA512 sha512 = SHA512.Create();
                    var byteCodeHash = hex.EncodeData(sha512.ComputeHash(byteCode));
                    Console.WriteLine($"Program.Code hash {byteCodeHash}.");
                }
            }
            else
            {
                Console.WriteLine("GetTransaction did not return a result.");
            }
        }
    }
}
4 Likes

I hooked up to a local validator and the log messages report that the signature is invalid.

E0701 08:39:02.514312 139730349979392 language/vm/vm_runtime/src/process_txn/validate.rs:84] [VM] Invalid signature

Does the signature need to be done on the raw serialised tx or a hash of it?
The signature needs to be made on the SHA3-256 hash of the raw transaction.

Still unable to get my local validating node to accept a signed transaction. Any working samples or pointers greatly appreciated.

I’d suggest adding logging locally to the local libra swarm you are running to see what raw bytes the validator is seeing and compare those to the raw bytes that you are signing. Also print the keys used for signing/verification and make sure they match on both sides and see if you can notice what is off between the two

1 Like

Thanks for the reply. Your suggestion is exactly the same thought I had and what I ended up doing :slightly_smiling_face:.

I know my sha3 is good as I get the same results as the Libra hash unit tests. I know my ed25519 signature is good as if I log the hash in check_signature and sign it it gets accepted. My tx serialisation is good as it matches the input I log in check_signature.

I suspect there’s a nonce or seed being added to the serialised raw transaction. I’ll add some more log messages and track it down.

Hi,

I’m currently working with a java integration to libra. I’m currently able to get the signature correct but the values still show up funny in the chain explorer. But yes, you need to add a salt to the data that is signed.
I pushed my code to github, you can check if my example helps you:

Transfer: https://github.com/ketola/jlibra/blob/master/src/main/java/dev/jlibra/example/TransferExample.java

Signing:

4 Likes

Aha thx!

And the salt in your code is RawTransaction@@$$LIBRA$$@@. I’d tried those two strings separately. Hopefully that’s the trick I’m missing.

Update: Thanks again to @2kSiika I was able to successfully generate a payment transaction. For anyone else that comes along the two tricks I was missing were:

  1. Account ID’s are the sha3(<ed25519 public key>)
  2. The signature for a transaction needs to be of: sha3(sha3(“RawTransaction@@$$LIBRA$$@@”) + <protobuf serialised RawTrnsaction>)
1 Like

@2kSiika nice, your Java snippets are very impressive!
How did you come up with this, did you port some other language version, coded against the spec, by trail and error or a combination of these :sweat_smile:

Yes, mostly by trial and error :grinning:
I used the source code of libra as reference + setting up a local node to see errors on the logs helped a lot.

Thanks, I tried the examples against the official testnode and as you said, something seems to be wrong with the transactions (ie. they don’t show up in librabrowser.io though there is no error). So I will try the local node mode and look into the logs.
Did you try to include a proto generation step in the Maven build? Currently the generated files are copied into the src folder, generation would be nicer I think. If you don’t mind, I will do this and create a PR on your repo - but might take one week or two…

Yes, proto-generation as part of the maven build is something I’m hoping to have in the future.

I think the problem with the yesterday’s version was that the byte order resulted in a zero-value amount and that’s why it did not appear in the browser.
I changed the implementation for the long to bytearray conversion. It still shows the value incorrect in the browser but at least it should appear there now.

The commit changed other parts also but the significant change is ByteBuffer.allocate(Long.BYTES).putLong(amount).array()
for the conversion.

1 Like

Not sure about Move IR, but in Solidity/EVM there is no logging as you just don’t know where the log should go to, as all this is highly distributed and you would log on some arbitrary node. In Solidity, usually events are used for logging.

1 Like

Thanks! Makes total sense.
Btw, I removed my question because I didn’t feel that it was related to the issue of grpc calls.

Hi,

Were you also able to get the transaction amount to show correctly? If so, how did you convert the amount to byte array?
The results of my transactions look like the following and I guess the problem is with the long to bytes conversion: https://librabrowser.io/account/6674633c78e2e00c69fd6e027aa6d1db2abc2a6c80d78a3e129eaf33dd49ce1c

I did have some hassles with that transaction amount. I haven’t got back to tracking down a proper solution yet but what my hasty hypothesis was if the first byte had the most significant bit set it didn’t work correctly. For example:

0001 : 1 Libra ok
0011 : 3 Libra ok
1000 : 8 Libra bad

Maybe somewhere in he Libra code is using a sign bit.

My test transactions amounts are showing correctly but any attempts at an amount with the MSB set fail.

https://librabrowser.io/account/cc9af3ffc6952cccd475a1447132390a94168b851ea69837338a9132e0c83dc2

Update: Here’s my serialisation snippet.

            Types.RawTransaction rawTx = new Types.RawTransaction();
            rawTx.SenderAccount = Google.Protobuf.ByteString.CopyFrom(hex.DecodeData(senderHex));
            rawTx.SequenceNumber = seqNum;
            rawTx.Program = new Types.Program();
            rawTx.Program.Code = Google.Protobuf.ByteString.CopyFrom(Convert.FromBase64String("TElCUkFWTQoBAAcBSgAAAAQAAAADTgAAAAYAAAAMVAAAAAUAAAANWQAAAAQAAAAFXQAAACkAAAAEhgAAACAAAAAHpgAAAA0AAAAAAAABAAIAAQMAAgACBAIDAgQCBjxTRUxGPgxMaWJyYUFjY291bnQEbWFpbg9wYXlfZnJvbV9zZW5kZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgAEAAwADAERAQI="));

            var recipientArg = new Types.TransactionArgument { Type = Types.TransactionArgument.Types.ArgType.Address };
            recipientArg.Data = Google.Protobuf.ByteString.CopyFrom(hex.DecodeData(receipientHex));
            rawTx.Program.Arguments.Add(recipientArg);

            var amountArg = new Types.TransactionArgument { Type = Types.TransactionArgument.Types.ArgType.U64 };
            amountArg.Data = Google.Protobuf.ByteString.CopyFrom(BitConverter.GetBytes(recipientAmount));
            rawTx.Program.Arguments.Add(amountArg);

            rawTx.MaxGasAmount = maxGasAmount;
            rawTx.GasUnitPrice = maxGasUnitPrice;
            rawTx.ExpirationTime = expirationTime;

I got a Python client of Libra working, including transaction generation, sign, and broadcast. The code is available at https://github.com/bandprotocol/pylibra if you want to take a look!

3 Likes

I got the amount to show correctly now. There were to things:

  1. The amount is sent in micro libras (1 000 000 x amount),
  2. The amount had to be sent in little endian byte order
1 Like

Hello sipsorcery, I’m following your code example in Net Core, and I’m having the Invalid Signature Error.
Could you please add a code snippet of how you build the signature.
Thanks in advance!

Diego-

Here’s my sample https://gist.github.com/sipsorcery/9420892747104d72054e6504317d3b24.

Sorry there’s no project file etc.

Thankssss, now works like charm

One more question :slight_smile:

How do you encode the move scripts to send them in the request.

rawTx.Program.Code = Google.Protobuf.ByteString.CopyFrom(Convert.FromBase64String(ENCODED SCRIPT)

Because I’m trying other script and the response returns Unknown Script, y think that is probably because of the encoding im using is not the correct

Thanks in advance!

Regarding the script encoding I took a shortcut and copied the bytes from a payment transfer generated with the Rust client.

Compiling a custom script seems straight forward see How to execute and deploy Move script?.

Your problem could be that according to the documentation the initial version Libra deployment will only support 4 pre-defined scripts. I suspect they are the ones in this driectory https://github.com/libra/libra/tree/master/language/stdlib/transaction_scripts.