I suspect it's considered tacky in programmer circles to post an answer to one's own question, but in this case my mistake was rather dangerous so I'd like to share the details with y'all.
RECAP:
My question stated that the public 'push tx' APIs weren't giving viable error messages after refusing my hex-formatted raw transactions. The answer from Mr. Chow indicated that the problem was a signature error, which he received after attempting to push my malformed TX through his personal node. That pointed me in the right direction.
His suggestion was that perhaps I wasn't serializing my sighash properly, since that often leads to the error message he referenced. But the code that formulates that sighash has been working fine for years so I figured the problem was elsewhere. Indeed, it was. The short story is that my logic used to derive a public key from a private key, which had worked flawlessly forever, was now failing after an upgrade of the BouncyCastle .NET encryption component.
Here's an excerpt of my code that converts a private key to public key:
public class BTCUtils2{
public static byte[] DerivePublicKey(byte[] privKey,bool isCompr){
ECPoint pubPt=ComputePublicECPoint(privKey);
//byte[] xCoord = pubPt.X.ToBigInteger().ToByteArrayUnsigned(), //2015: worked properly in with BouncyCastle 1.7.4114.6375
// yCoord = pubPt.Y.ToBigInteger().ToByteArrayUnsigned();
//byte[] xCoord = pubPt.XCoord.GetEncoded(), //2019-07-01: upgraded to 1.8.5; generates bad data
// yCoord = pubPt.YCoord.GetEncoded();
ECPublicKeyParameters publicParams = new ECPublicKeyParameters(pubPt,ECParams); //2019-07-16; proper usage of 1.8.5
byte[] xCoord=publicParams.Q.XCoord.GetEncoded(),
yCoord=publicParams.Q.YCoord.GetEncoded();
if(xCoord.Length!=32 || yCoord.Length!=32){throw new ApplicationException("SANITY CHECK: Expected 32 bytes for X/Y coords");}
byte[] bytes=new byte[isCompr?33:65]; //public key consists of a one byte prefix (0x04=uncompress,0x02=compressed w\ y=even, 0x03=compressed w\ y=odd) prior to the payload
if(isCompr){
bytes[0]=((yCoord[31] & 0x01)==0)?(byte)0x02:(byte)0x03; //compressed; set prefix based on whether Y is even or odd
Array.Copy(xCoord,0,bytes,1,32); //x coord
}
else{
bytes[0]=0x04; //uncompressed; set prefix to 0x04
Array.Copy(xCoord,0,bytes,1,32); //x coord
Array.Copy(yCoord,0,bytes,33,32); //y coord
}
return bytes;
}
private static ECPoint ComputePublicECPoint(byte[] privKey){return ECParams.G.Multiply(new BigInteger(1,privKey));}
private static readonly ECDomainParameters ECParams=null;
static BTCUtils2(){
X9ECParameters crv = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1");
ECParams=new ECDomainParameters(crv.Curve, crv.G, crv.N, crv.H);
}
}
The problem occurs at the top, where I'm converting a private key, expressed as a byte array, to a public key, expressed in terms of its x/y coords. See my comments. The 2015 code worked just fine for years with the 2011 edition of BouncyCastle.
On July of 2019-07-01, I had to upgrade to version 1.8.5 of bouncy castle. The code no longer compiled. I figured hey, they must have renamed a few things or whatever so I applied the two lines denoted as 2019-07-01, figuring I was accomplishing the same thing. MAJOR MISTAKE. I'm not sure exactly what those function calls return, but they aren't the x/y coord of the public key. They're something else entirely.
As you can imagine, having a public key that doesn't properly correspond to your private key leads to all sorts of problems including malformed Bitcoin addresses, signing errors, etc.
Today, I replaced those bad lines with the ones labeled 2019-07-16. That did the trick. I verified that those lines were in fact generating a valid public key. Transactions now submit without error.
Hope this is value to someone.
Best,
Festus