Valid Signatures Fail Node Validation

Dear @fabrice, thank you for the Go example for txn signing. That helped me to create the same function in JS. Now my transactions and signatures are valid according to nacl.verify(txnBytes, sig, pk). However, when I actually send the txn to the node API, I get the error: At least one signature didn't pass verification. So my txn objects are fine, MsgPack formatting is correct, etc., but it appears that the node API signature validation spec does not conform to the nacl.verify spec.

I thought maybe the node API expects some additional bytes for padding or the SIGN_BYTES_PREFIX constant that is in main.js or the NUM_ADDL_BYTES_AFTER_SIGNING constant that is in transaction.js. But I don’t see those constants used in any of the primary signing functions in transaction.js (e.g., bytesToSign(), toByte(), rawSignTxn(sk), signTxn(sk)). So I’m not sure.

My app must load on memory-constrained devices; so I can’t rely on the SDK. I’ve already written all the rest of the code, which works fine, but I’m blocked by this signature validation problem. Can you (or somebody with enough knowledge) please help me figure out why my signatures validate with nacl.verify but fail when I send them to the node API?

Below is an example transaction (JSON and corresponding MsgPack formats) that validates with nacl.verify but fails the node API signature validation:

{
    "sig": [96,133,109,166,137,97,148,86,186,5,192,158,89,27,95,33,91,71,135,210,238,66,211,221,96,208,45,161,61,17,22,79,65,144,4,81,138,105,221,37,149,6,81,209,171,8,194,247,33,81,64,10,227,26,26,247,6,177,64,164,34,85,128,10],
    "txn": {
        "amt": 1,
        "fee": 1000,
        "fv": 22115091,
        "gen": "testnet-v1.0",
        "gh": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
        "lv": 22116091,
        "rcv": [136,26,30,107,207,78,46,98,173,182,97,243,58,204,213,81,197,212,1,15,94,255,30,84,19,58,147,231,218,67,49,212],
        "snd":[195,6,178,115,116,60,107,41,217,70,146,23,49,238,143,201,12,6,193,211,92,91,46,104,122,32,198,200,17,195,171,182],
        "type": "pay"
    }
}
[130,163,115,105,103,196,64,96,133,109,166,137,97,148,86,186,5,192,158,89,27,95,33,91,71,135,210,238,66,211,221,96,208,45,161,61,17,22,79,65,144,4,81,138,105,221,37,149,6,81,209,171,8,194,247,33,81,64,10,227,26,26,247,6,177,64,164,34,85,128,10,163,116,120,110,137,163,97,109,116,1,163,102,101,101,205,3,232,162,102,118,206,1,81,115,19,163,103,101,110,172,116,101,115,116,110,101,116,45,118,49,46,48,162,103,104,217,44,83,71,79,49,71,75,83,122,121,69,55,73,69,80,73,116,84,120,67,66,121,119,57,120,56,70,109,110,114,67,68,101,120,105,57,47,99,79,85,74,79,105,73,61,162,108,118,206,1,81,118,251,163,114,99,118,196,32,136,26,30,107,207,78,46,98,173,182,97,243,58,204,213,81,197,212,1,15,94,255,30,84,19,58,147,231,218,67,49,212,163,115,110,100,196,32,195,6,178,115,116,60,107,41,217,70,146,23,49,238,143,201,12,6,193,211,92,91,46,104,122,32,198,200,17,195,171,182,164,116,121,112,101,163,112,97,121]
1 Like

Welcome @Julia to Algorand!

Storing your msgpack in a file (with appropriate conversion) and then parsing it using msgpacktool (a tool that is provided when installing the Algorand software: Install a node - Algorand Developer Portal / you can also use something inside sandbox), I get:

$ msgpacktool -d -b32 < tx.tx
{
  "sig:b32": "MCCW3JUJMGKFNOQFYCPFSG27EFNUPB6S5ZBNHXLA2AW2CPIRCZHUDEAEKGFGTXJFSUDFDUNLBDBPOIKRIAFOGGQ264DLCQFEEJKYACQ=",
  "txn": {
    "amt": 1,
    "fee": 1000,
    "fv": 22115091,
    "gen": "testnet-v1.0",
    "gh": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
    "lv": 22116091,
    "rcv:b32": "RANB426PJYXGFLNWMHZTVTGVKHC5IAIPL37R4VATHKJ6PWSDGHKA====",
    "snd:b32": "YMDLE43UHRVSTWKGSILTD3UPZEGANQOTLRNS42D2EDDMQEODVO3A====",
    "type": "pay"
  }
}

Compare this to a similar transacition I generate using goal clerk send:

$ goal clerk send -o tx2.tx --from YMDLE43UHRVSTWKGSILTD3UPZEGANQOTLRNS42D2EDDMQEODVO3PCJOGLQ --to RANB426PJYXGFLNWMHZTVTGVKHC5IAIPL37R4VATHKJ6PWSDGHKDJH4HAU --firstvalid 22115091 --amount 1 --fee 1000

$ msgpacktool -d -b32 < tx2.tx
{
  "txn": {
    "amt": 1,
    "fee": 1000,
    "fv": 22115091,
    "gen": "testnet-v1.0",
    "gh:b32": "JBR3KGFEWPEE5SAQ6IWU6EEBZMHXD4CZU6WCBXWGF57XBZIJHIRA====",
    "lv": 22116091,
    "note:b32": "MFKWIIOBOPWUS===",
    "rcv:b32": "RANB426PJYXGFLNWMHZTVTGVKHC5IAIPL37R4VATHKJ6PWSDGHKA====",
    "snd:b32": "YMDLE43UHRVSTWKGSILTD3UPZEGANQOTLRNS42D2EDDMQEODVO3A====",
    "type": "pay"
  }
}

(it’s missing the signature because I don’t have the secret key.)

See in particular that in your case you have gh, while in my case I have gh:b32. gh:b32 means that msgpacktool read a byte array and had to convert it to base32. While in your case, you directly put the base64 of the genesis has gh, as a string.

Note: It is much easier if you provide the base64 of the msgpack, rather than a JSON array for msgpack. The latter can be handled much more easily with command line.

Thank you, @fabrice . Yes, I’ve used the msgpktool a lot during my testing to study the txn formats. The reason I don’t include the :b32 or :b64 suffixes (as applicable) and corresponding encoded values in my actual code and the reason I only used the raw Uint8Arrays for the PK/SK fields is because I always get a msgpack decode error if I try to submit b64 or b32 encoded values. For example, when I replace my txn object with your txn object (using my private key for the sig of course) and then MsgPack it, the node API throws this error:

msgpack decode error [pos 120]: no matching struct…ld found when decoding stream map with key gh:b32

I get the same error if I pass b64-encoded key suffixes and values. My interpretation of that error is that the node cannot parse the gh field because it’s not expecting a b32 (or b64)-encoded value or key suffix. But maybe that’s not a correct interpretation.

I’m using the exact same encode MsgPack function that is provided in the SDK and I don’t see any special parameters to feed it any b32 or b64 keys or values.

It feels like there is a lot of ambiguity about exactly what formatting the node is expecting. Is there a clear spec for how the raw transactions should be formatted and submitted to the node for people like me who can’t use the SDK?

You should not include :b32 suffix. This suffix is added by msgpacktool to indicate that they converted the field from a raw Uint8Array to a string.

The fact you don’t see this suffix for gh means that your gh is now a raw Uint8Array of the genesis hash, but instead a string of the corresponding base64 representation.
You change your gh field accordingly.

I find the best way to do it is read the Go struct code, such as:

The Go struct represent exactly the expected formatting.
For example, following Transaction, you go to https://github.com/algorand/go-algorand/blob/d012599b1b67de9a34184347ae7d31a54aa07063/data/transactions/transaction.go#L84
and then you follow Header (this is an anonymous field, you can learn more there Promoted Fields in Golang Structure - GeeksforGeeks - but you can just see it as a macro that expands for your purpose) and you see that in Header, the genesis hash is a crypto.Digest (https://github.com/algorand/go-algorand/blob/d012599b1b67de9a34184347ae7d31a54aa07063/crypto/util.go#L47), which is a 32-byte array (and not a string - this is very important).

Thank you, @fabrice. I’ve studied the Go source code and followed the steps that you suggested, but there’s still something causing valid signatures (valid according to Nacl.verify) to fail the node API’s signature validation check with the same error: At least one signature didn't pass verification. So I’m going step-by-step here to try to achieve more clarity…

Using the Go Header struct as our spec below, it contains the primary txn fields and their types.

type Header struct {
	_struct struct{} `codec:",omitempty,omitemptyarray"`

	Sender      basics.Address    `codec:"snd"`
	Fee         basics.MicroAlgos `codec:"fee"`
	FirstValid  basics.Round      `codec:"fv"`
	LastValid   basics.Round      `codec:"lv"`
	Note        []byte            `codec:"note,allocbound=config.MaxTxnNoteBytes"` // Uniqueness or app-level data about txn
	GenesisID   string            `codec:"gen"`
	GenesisHash crypto.Digest     `codec:"gh"`

The GH is defined as a crypto.Digest type, which is a 32-byte Uint8Array. The Sender (and Receiver) fields are defined in basics.Address as a crypto.Digest type, too; so they’re also Uint8Arrays. The Note field is also defined as an Uint8Array. So within the context of a pure Go environment, the node API is expecting the Uint8Array type (i.e., raw bytes) for all those fields; is that correct?

Assuming that’s correct, then the next step is to define the encoding format of the data for each field to protect the integrity of the binary array data when its packaged within a JSON txn object and transmitting the txns over the network. To accomplish this, the Uint8Array fields must be encoded in either B64 or B32; and based on the fact that the msgpacktool can parse both B64 and B32 encoded fields, I think the node API should be able to parse both encoding formats, too. Is that correct?

Assuming that’s correct, then the final step is to encode the entire tx object into a MsgPack payload and POST it to the node API. I’m using the exact same MsgPack encode function from the SDK and the node API clearly can parse the txn; so there should be nothing wrong with the final MsgPacked payload object. Is that correct?

Assuming my understanding is accurate so far, then the 2 txns below should pass the node signature validation checks, but they don’t pass. They still throw the error: At least one signature didn't pass verification.

Here’s the txn payload with the applicable fields encoded in B64 and corresponding msgpack:

{
    "sig": "f3oM1xLlqQzRq7CjBuoOSvdWOoE/15fyG4gx9of3kfCJ/Hk2KOyYLPbBb7II9adFXz7lQj/56GDjmjQZcx73Cg==",
    "txn": {
        "amt": 1,
        "fee": 1000,
        "fv": 22162791,
        "gen": "testnet-v1.0",
        "gh": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
        "lv": 22163791,
        "rcv": "iBoea89OLmKttmHzOszVUcXUAQ9e/x5UEzqT59pDMdQ=",
        "snd": "wwayc3Q8aynZRpIXMe6PyQwGwdNcWy5oeiDGyBHDq7Y=",
        "type": "pay"
    }
}

MsgPack:

[130,163,115,105,103,217,88,102,51,111,77,49,120,76,108,113,81,122,82,113,55,67,106,66,117,111,79,83,118,100,87,79,111,69,47,49,53,102,121,71,52,103,120,57,111,102,51,107,102,67,74,47,72,107,50,75,79,121,89,76,80,98,66,98,55,73,73,57,97,100,70,88,122,55,108,81,106,47,53,54,71,68,106,109,106,81,90,99,120,55,51,67,103,61,61,163,116,120,110,137,163,97,109,116,1,163,102,101,101,205,3,232,162,102,118,206,1,82,45,103,163,103,101,110,172,116,101,115,116,110,101,116,45,118,49,46,48,162,103,104,217,44,83,71,79,49,71,75,83,122,121,69,55,73,69,80,73,116,84,120,67,66,121,119,57,120,56,70,109,110,114,67,68,101,120,105,57,47,99,79,85,74,79,105,73,61,162,108,118,206,1,82,49,79,163,114,99,118,217,44,105,66,111,101,97,56,57,79,76,109,75,116,116,109,72,122,79,115,122,86,85,99,88,85,65,81,57,101,47,120,53,85,69,122,113,84,53,57,112,68,77,100,81,61,163,115,110,100,217,44,119,119,97,121,99,51,81,56,97,121,110,90,82,112,73,88,77,101,54,80,121,81,119,71,119,100,78,99,87,121,53,111,101,105,68,71,121,66,72,68,113,55,89,61,164,116,121,112,101,163,112,97,121]

Here’s the tx payload with the applicable fields encoded in B32:

{
    "sig": "RLDRAHYTTM3NKY7J2WFTI3HMVAPB5NNOSQMDPB4A2AUXSKNPB2W73ZD7AKVSKBJ4KQIC3YUGOHISBW3ZG2GEJMR4D3X6OT34HKUO6BA=",
    "txn": {
        "amt": 1,
        "fee": 1000,
        "fv": 22162827,
        "gen": "testnet-v1.0",
        "gh": "JBR3KGFEWPEE5SAQ6IWU6EEBZMHXD4CZU6WCBXWGF57XBZIJHIRA====",
        "lv": 22163827,
        "rcv": "RANB426PJYXGFLNWMHZTVTGVKHC5IAIPL37R4VATHKJ6PWSDGHKA====",
        "snd": "YMDLE43UHRVSTWKGSILTD3UPZEGANQOTLRNS42D2EDDMQEODVO3A====",
        "type": "pay"
    }
}

MsgPack:

[130,163,115,105,103,217,104,82,76,68,82,65,72,89,84,84,77,51,78,75,89,55,74,50,87,70,84,73,51,72,77,86,65,80,66,53,78,78,79,83,81,77,68,80,66,52,65,50,65,85,88,83,75,78,80,66,50,87,55,51,90,68,55,65,75,86,83,75,66,74,52,75,81,73,67,51,89,85,71,79,72,73,83,66,87,51,90,71,50,71,69,74,77,82,52,68,51,88,54,79,84,51,52,72,75,85,79,54,66,65,61,163,116,120,110,137,163,97,109,116,1,163,102,101,101,205,3,232,162,102,118,206,1,82,45,139,163,103,101,110,172,116,101,115,116,110,101,116,45,118,49,46,48,162,103,104,217,56,74,66,82,51,75,71,70,69,87,80,69,69,53,83,65,81,54,73,87,85,54,69,69,66,90,77,72,88,68,52,67,90,85,54,87,67,66,88,87,71,70,53,55,88,66,90,73,74,72,73,82,65,61,61,61,61,162,108,118,206,1,82,49,115,163,114,99,118,217,56,82,65,78,66,52,50,54,80,74,89,88,71,70,76,78,87,77,72,90,84,86,84,71,86,75,72,67,53,73,65,73,80,76,51,55,82,52,86,65,84,72,75,74,54,80,87,83,68,71,72,75,65,61,61,61,61,163,115,110,100,217,56,89,77,68,76,69,52,51,85,72,82,86,83,84,87,75,71,83,73,76,84,68,51,85,80,90,69,71,65,78,81,79,84,76,82,78,83,52,50,68,50,69,68,68,77,81,69,79,68,86,79,51,65,61,61,61,61,164,116,121,112,101,163,112,97,121]

What is wrong with those txn objects? Is my understanding of the txn object formatting correct? Please help.

I have looked at your msgpack: the issue is that you used (raw) base32/base64 string as fields instead of (raw) bytes/uint8array.

Yes, not only within Go but also in msgpack. And that is what is very important.

No, you should keep them as byte arrays, not convert them to base64 / base32.

msgpacktool converts them to base64/base32 (and change the key to ...:b32/...:b64) because JSON does not support byte array.
So it is the only way to display them properly.

But your code should not do that.

See https://github.com/msgpack/msgpack/blob/master/spec.md#type-system to understand msgpack.

Thank you, @fabrice. I wasn’t expecting a reply on the weekend, but I deeply appreciate it because this problem has been blocking me for nearly a week.

In my first post I used the raw Uint8Array arrays. The only reason I used the B64/B32 encoding in my 2nd round of test results above is because I thought that’s what you were telling me to do after my first post. I guess I misinterpreted you.

Anyway, I’ve reverted those fields back to the raw Uint8Arrays again and I’m still getting the same invalid signature error, even though nacl.verify says they are valid.

Maybe I’m missing something that seems obvious to you, but here’s my JS code that constructs my txn object, which is what is passed to MsgPack. This shows you exactly what txn field types and values are being MsgPacked and sent to the node API:

			let txn = {
				amt: txn.amt, // Number
				fee: txn.fee, // Number from API
				fv: txn.fv, // Number first round value from API
				gen: txn.gen, // String from API
				gh: Base64.toUint8Array(txn.gh), // Already b64 encoded from API; so I convert to Uint8Array
				lv: txn.lv, // Number; fv + 1000
				// note: textEncoder.encode(txn.note), disabled for testing
				rcv: receiverPublicKey.publicKey, // Raw 32-byte Uint8Array pubkey separate from the 4-byte checksum
				snd: senderPublicKey.publicKey, // Raw 32-byte Uint8Array pubkey separate from the 4-byte checksum
				type: 'pay' // String
			};

And the signature is created with the same Nacl library and function args that is in the SDK:

const sig: Uint8Array = await nacl.sign(txnBytes, secretKey);

I’m not sure what “appropriate conversion” means. When I save the MsgPack block below to a text file and execute msgpacktool -d -b32 < tx.tx or msgpacktool -d < tx.tx, it spits out the same MsgPack array numbers without the brackets. So the only way I’m able to inspect the correctness of my msgpack blocks is from within my code like this…

msgPack.decode([130,163,115,105,103,196,64,249,130,246,127,165,30,8,124,68,141,67,207,132,96,27,140,249,133,18,208,222,83,47,123,218,182,110,221,123,98,47,172,55,144,102,131,86,174,54,226,50,88,116,100,16,64,33,178,43,174,47,15,138,72,80,113,167,30,201,40,76,223,200,14,163,116,120,110,137,163,97,109,116,1,163,102,101,101,205,3,232,162,102,118,206,1,82,93,13,163,103,101,110,172,116,101,115,116,110,101,116,45,118,49,46,48,162,103,104,196,32,72,99,181,24,164,179,200,78,200,16,242,45,79,16,129,203,15,113,240,89,167,172,32,222,198,47,127,112,229,9,58,34,162,108,118,206,1,82,96,245,163,114,99,118,196,32,136,26,30,107,207,78,46,98,173,182,97,243,58,204,213,81,197,212,1,15,94,255,30,84,19,58,147,231,218,67,49,212,163,115,110,100,196,32,195,6,178,115,116,60,107,41,217,70,146,23,49,238,143,201,12,6,193,211,92,91,46,104,122,32,198,200,17,195,171,182,164,116,121,112,101,163,112,97,121])

… and the result of that inspection is a correct JSON representation of the raw txn fields, which you can see below.

{
    "sig": {
        "0": 249,"1": 130,"2": 246,"3": 127,"4": 165,"5": 30,"6": 8,"7": 124,"8": 68,"9": 141,"10": 67,"11": 207,"12": 132,"13": 96,"14": 27,"15": 140,"16": 249,"17": 133,"18": 18,"19": 208,"20": 222,"21": 83,"22": 47,"23": 123,"24": 218,"25": 182,"26": 110,"27": 221,"28": 123,"29": 98,"30": 47,"31": 172,"32": 55,"33": 144,"34": 102,"35": 131,"36": 86,"37": 174,"38": 54,"39": 226,"40": 50,"41": 88,"42": 116,"43": 100,"44": 16,"45": 64,"46": 33,"47": 178,"48": 43,"49": 174,"50": 47,"51": 15,"52": 138,"53": 72,"54": 80,"55": 113,"56": 167,"57": 30,"58": 201,"59": 40,"60": 76,"61": 223,"62": 200,"63": 14
    },
    "txn": {
        "amt": 1,"fee": 1000,"fv": 22174989,"gen": "testnet-v1.0","gh": {
            "0": 72,"1": 99,"2": 181,"3": 24,"4": 164,"5": 179,"6": 200,"7": 78,"8": 200,"9": 16,"10": 242,"11": 45,"12": 79,"13": 16,"14": 129,"15": 203,"16": 15,"17": 113,"18": 240,"19": 89,"20": 167,"21": 172,"22": 32,"23": 222,"24": 198,"25": 47,"26": 127,"27": 112,"28": 229,"29": 9,"30": 58,"31": 34
        },"lv": 22175989,"rcv": {
            "0": 136,"1": 26,"2": 30,"3": 107,"4": 207,"5": 78,"6": 46,"7": 98,"8": 173,"9": 182,"10": 97,"11": 243,"12": 58,"13": 204,"14": 213,"15": 81,"16": 197,"17": 212,"18": 1,"19": 15,"20": 94,"21": 255,"22": 30,"23": 84,"24": 19,"25": 58,"26": 147,"27": 231,"28": 218,"29": 67,"30": 49,"31": 212
        },"snd": {
            "0": 195,"1": 6,"2": 178,"3": 115,"4": 116,"5": 60,"6": 107,"7": 41,"8": 217,"9": 70,"10": 146,"11": 23,"12": 49,"13": 238,"14": 143,"15": 201,"16": 12,"17": 6,"18": 193,"19": 211,"20": 92,"21": 91,"22": 46,"23": 104,"24": 122,"25": 32,"26": 198,"27": 200,"28": 17,"29": 195,"30": 171,"31": 182
        },"type": "pay"
    }
}

Of course, the MsgPack code blocks above are rendered as JSON keys/values, but stripping out the keys yields the correct MsgPack byte values, which I’ve also decoded back to the original Uint8Array field values. That verifies that the encoding/decoding operations are producing the correct data, which is why nacl.verify says the signature is valid. I’ve provided the same JSON below with those fields rendered as the original Uint8Array arrays (in JSON) in case you want to visually confirm more easily.

{
    "sig": { [249,130,246,127,165,30,8,124,68,141,67,207,132,96,27,140,249,133,18,208,222,83,47,123,218,182,110,221,123,98,47,172,55,144,102,131,86,174,54,226,50,88,116,100,16,64,33,178,43,174,47,15,138,72,80,113,167,30,201,40,76,223,200,14]},
    "txn": {
        "amt": 1,
		"fee": 1000,
		"fv": 22174989,
		"gen": "testnet-v1.0",
		"gh": {[72,99,181,24,164,179,200,78,200,16,242,45,79,16,129,203,15,113,240,89,167,172,32,222,198,47,127,112,229,9,58,34]},
		"lv": 22175989,
		"rcv": {[136,26,30,107,207,78,46,98,173,182,97,243,58,204,213,81,197,212,1,15,94,255,30,84,19,58,147,231,218,67,49,212]},
		"snd": {[195,6,178,115,116,60,107,41,217,70,146,23,49,238,143,201,12,6,193,211,92,91,46,104,122,32,198,200,17,195,171,182]},
		"type": "pay"
    }
}

And here’s the same txn object decoded to Base64 (even easier to verify):

{
    "sig": "+YL2f6UeCHxEjUPPhGAbjPmFEtDeUy972rZu3XtiL6w3kGaDVq424jJYdGQQQCGyK64vD4pIUHGnHskoTN/IDg==",
    "txn": {
        "amt": 1,
        "fee": 1000,
        "fv": 22174989,
        "gen": "testnet-v1.0",
        "gh": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
        "lv": 22175989,
        "rcv": "iBoea89OLmKttmHzOszVUcXUAQ9e/x5UEzqT59pDMdQ=",
        "snd": "wwayc3Q8aynZRpIXMe6PyQwGwdNcWy5oeiDGyBHDq7Y=",
        "type": "pay"
    }
}

And finally, the same txn object with pubKeys correctly re-encoded back to the 58-byte addresses:

{
    "sig": "+YL2f6UeCHxEjUPPhGAbjPmFEtDeUy972rZu3XtiL6w3kGaDVq424jJYdGQQQCGyK64vD4pIUHGnHskoTN/IDg==",
    "txn": {
        "amt": 1,
        "fee": 1000,
        "fv": 22174989,
        "gen": "testnet-v1.0",
        "gh": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
        "lv": 22175989,
        "rcv": "RANB426PJYXGFLNWMHZTVTGVKHC5IAIPL37R4VATHKJ6PWSDGHKDJH4HAU",
        "snd": "YMDLE43UHRVSTWKGSILTD3UPZEGANQOTLRNS42D2EDDMQEODVO3PCJOGLQ",
        "type": "pay"
    }
}

After everything I’ve shown here, do you still think my txn objects are not correct even though nacl.verify says the signatures are valid? If so, then the problem probably exists in the JS txn object logic at the top of my post. Is there something wrong with it?

Your object looks good to me now.
But the issue may be that you do not sign it properly.
I would recommend you debugging the same code signing with the SDK and comparing, with the debbuger, what inputs is given to sign.
Check whether all the bytes are identical.

Thank you, @fabrice. I just resolved the issue. The sigs were missing the prefix. After I added the prefix, the txns now pass the node validation. Thank you again!

1 Like