diff --git a/NBitcoin.Altcoins/Litecoin.cs b/NBitcoin.Altcoins/Litecoin.cs index 88261ec7e..2f7d42d22 100644 --- a/NBitcoin.Altcoins/Litecoin.cs +++ b/NBitcoin.Altcoins/Litecoin.cs @@ -219,6 +219,9 @@ public override void ReadWrite(BitcoinStream stream) } else { + if (Inputs.Count == 0 && !stream.AllowNoInputs) + throw new InvalidOperationException("The transaction must have at least one input"); + stream.ReadWrite(ref nVersion); if (witSupported) diff --git a/NBitcoin.Tests/ChainTests.cs b/NBitcoin.Tests/ChainTests.cs index b37446f0d..09e046788 100644 --- a/NBitcoin.Tests/ChainTests.cs +++ b/NBitcoin.Tests/ChainTests.cs @@ -608,7 +608,9 @@ public ChainedBlock AppendBlock(ChainedBlock previous, params ConcurrentChain[] var nonce = RandomUtils.GetUInt32(); foreach (var chain in chains) { - var block = TestUtils.CreateFakeBlock(Network.Main.CreateTransaction()); + var tx = Network.Main.CreateTransaction(); + tx.Inputs.Add(); + var block = TestUtils.CreateFakeBlock(tx); block.Header.HashPrevBlock = previous == null ? chain.Tip.HashBlock : previous.HashBlock; block.Header.Nonce = nonce; if (!chain.TrySetTip(block.Header, out last)) diff --git a/NBitcoin.Tests/ColoredCoinsTests.cs b/NBitcoin.Tests/ColoredCoinsTests.cs index 65fe59d34..0dbb37388 100644 --- a/NBitcoin.Tests/ColoredCoinsTests.cs +++ b/NBitcoin.Tests/ColoredCoinsTests.cs @@ -151,6 +151,7 @@ public void CanColorizeSpecScenario() Assert.True(destroyed[0].Id == a2.Id); var prior = Network.Main.CreateTransaction(); + prior.Inputs.Add(); prior.Outputs.Add(new TxOut(dust, a1.ScriptPubKey)); prior.Outputs.Add(new TxOut(dust, a2.ScriptPubKey)); prior.Outputs.Add(new TxOut(dust, h.ScriptPubKey)); diff --git a/NBitcoin.Tests/Generators/PSBTGenerator.cs b/NBitcoin.Tests/Generators/PSBTGenerator.cs index e749d736d..bea300588 100644 --- a/NBitcoin.Tests/Generators/PSBTGenerator.cs +++ b/NBitcoin.Tests/Generators/PSBTGenerator.cs @@ -38,7 +38,7 @@ from psbt in SanePSBT(network) /// /// public static Gen SanePSBT(Network network) => - from inputN in Gen.Choose(0, 8) + from inputN in Gen.Choose(1, 8) from scripts in Gen.ListOf(inputN, ScriptGenerator.RandomScriptSig()) from txOuts in Gen.Sequence(scripts.Select(sc => OutputFromRedeem(sc))) from prevN in Gen.Choose(0, 5) diff --git a/NBitcoin.Tests/PSBTTests.cs b/NBitcoin.Tests/PSBTTests.cs index 1ae6a5503..bd8f8752d 100644 --- a/NBitcoin.Tests/PSBTTests.cs +++ b/NBitcoin.Tests/PSBTTests.cs @@ -56,6 +56,7 @@ public static void ShouldCalculateBalanceOfHDKey(PSBTVersion version) var bob = bobMaster.Derive(new KeyPath("4/5/6")); var funding = network.CreateTransaction(); + funding.Inputs.Add(); funding.Outputs.Add(Money.Coins(1.0m), alice); funding.Outputs.Add(Money.Coins(1.5m), bob); @@ -642,8 +643,9 @@ public void CanRebaseKeypathInPSBT() var accountExtKey = masterExtkey.Derive(new KeyPath("0'/0'/0'")); var accountRootedKeyPath = new KeyPath("0'/0'/0'").ToRootedKeyPath(masterExtkey); uint hardenedFlag = 0x80000000U; - retry: + retry: Transaction funding = masterExtkey.Network.CreateTransaction(); + funding.Inputs.Add(); funding.Outputs.Add(Money.Coins(2.0m), accountExtKey.Derive(0 | hardenedFlag).ScriptPubKey); funding.Outputs.Add(Money.Coins(2.0m), accountExtKey.Derive(1 | hardenedFlag).ScriptPubKey); #if HAS_SPAN diff --git a/NBitcoin.Tests/ProtocolTests.cs b/NBitcoin.Tests/ProtocolTests.cs index 121dcf372..bf76c54a7 100644 --- a/NBitcoin.Tests/ProtocolTests.cs +++ b/NBitcoin.Tests/ProtocolTests.cs @@ -1102,7 +1102,9 @@ public void CanConnectMultipleTimeToServer() public void CanRoundtripCmpctBlock() { Block block = Network.Main.Consensus.ConsensusFactory.CreateBlock(); - block.Transactions.Add(Network.Main.Consensus.ConsensusFactory.CreateTransaction()); + var tx = Network.Main.Consensus.ConsensusFactory.CreateTransaction(); + tx.Inputs.Add(); + block.Transactions.Add(tx); var cmpct = new CmpctBlockPayload(block); cmpct.Clone(); } diff --git a/NBitcoin.Tests/TestUtils.cs b/NBitcoin.Tests/TestUtils.cs index c5f9de5b5..7ed95e920 100644 --- a/NBitcoin.Tests/TestUtils.cs +++ b/NBitcoin.Tests/TestUtils.cs @@ -47,7 +47,9 @@ public static Block CreateFakeBlock(Transaction tx) public static Block CreateFakeBlock() { - var block = TestUtils.CreateFakeBlock(Network.Main.CreateTransaction()); + var tx = Network.Main.CreateTransaction(); + tx.Inputs.Add(); + var block = TestUtils.CreateFakeBlock(tx); block.Header.HashPrevBlock = new uint256(RandomUtils.GetBytes(32)); block.Header.Nonce = RandomUtils.GetUInt32(); return block; diff --git a/NBitcoin.Tests/script_tests.cs b/NBitcoin.Tests/script_tests.cs index bf5d7ef7d..577896a4a 100644 --- a/NBitcoin.Tests/script_tests.cs +++ b/NBitcoin.Tests/script_tests.cs @@ -149,6 +149,7 @@ public void BIP65_tests() private void BIP65_testsCore(LockTime target, LockTime now, bool expectedResult) { Transaction tx = Network.CreateTransaction(); + tx.Inputs.Add(); tx.Outputs.Add(new TxOut() { ScriptPubKey = new Script(Op.GetPushOp(target.Value), OpcodeType.OP_CHECKLOCKTIMEVERIFY) @@ -736,6 +737,7 @@ public void script_CHECKMULTISIG12() ); Transaction txFrom12 = Network.CreateTransaction(); + txFrom12.Inputs.Add(); txFrom12.Outputs.Add(new TxOut()); txFrom12.Outputs[0].ScriptPubKey = scriptPubKey12; @@ -780,6 +782,7 @@ public void script_CHECKMULTISIG23() var txFrom23 = Network.CreateTransaction(); + txFrom23.Inputs.Add(); txFrom23.Outputs.Add(new TxOut()); txFrom23.Outputs[0].ScriptPubKey = scriptPubKey23; diff --git a/NBitcoin.Tests/transaction_tests.cs b/NBitcoin.Tests/transaction_tests.cs index f31f18056..51fa8aa7c 100644 --- a/NBitcoin.Tests/transaction_tests.cs +++ b/NBitcoin.Tests/transaction_tests.cs @@ -988,6 +988,7 @@ public void CanBuildShuffleColoredTransaction() var repo = new NoSqlColoredTransactionRepository(new NoSqlTransactionRepository(), new InMemoryNoSqlRepository()); var init = Network.CreateTransaction(); + init.Inputs.Add(); init.Outputs.Add("1.0", gold.PubKey); init.Outputs.Add("1.0", silver.PubKey); init.Outputs.Add("1.0", satoshi.PubKey); @@ -1279,6 +1280,7 @@ public void CanBuildColoredTransaction() var repo = new NoSqlColoredTransactionRepository(); var init = Network.CreateTransaction(); + init.Inputs.Add(); init.Outputs.Add("1.0", gold.PubKey); init.Outputs.Add("1.0", silver.PubKey); init.Outputs.Add("1.0", satoshi.PubKey); @@ -1375,6 +1377,7 @@ public void CanBuildColoredTransaction() //Gold receive 2.5 BTC tx = txBuilder.Network.Consensus.ConsensusFactory.CreateTransaction(); + tx.Inputs.Add(); tx.Outputs.Add("2.5", gold.PubKey); repo.Transactions.Put(tx.GetHash(), tx); @@ -1762,6 +1765,7 @@ public void CanEstimateFees() builder.SendEstimatedFees(rate); signed = builder.BuildTransaction(true); Assert.True(builder.Verify(signed, estimatedFees)); + Assert.Equal(1174, builder.EstimateSize(signed)); } private Coin RandomCoin(Money amount, IDestination dest, bool p2sh) @@ -1924,23 +1928,6 @@ void BitcoinStreamCoverageCore(TItem[] input, BitcoinStreamCoverageCoreDe AssertEx.CollectionEquals(before, input); } - [Fact] - [Trait("UnitTest", "UnitTest")] - public void CanSerializeInvalidTransactionsBackAndForth() - { - Transaction before = Network.CreateTransaction(); - var versionBefore = before.Version; - before.Outputs.Add(new TxOut()); - Transaction after = AssertClone(before); - Assert.Equal(before.Version, after.Version); - Assert.Equal(versionBefore, after.Version); - Assert.True(after.Outputs.Count == 1); - - before = Network.CreateTransaction(); - after = AssertClone(before); - Assert.Equal(before.Version, versionBefore); - } - private Transaction AssertClone(Transaction before) { Transaction after = before.Clone(); @@ -2100,6 +2087,7 @@ public void CanFilterUneconomicalCoins() var bob = new Key(); //P2SH(P2WSH) var previousTx = Network.CreateTransaction(); + previousTx.Inputs.Add(); previousTx.Outputs.Add(new TxOut(Money.Coins(1.0m), alice.PubKey.ScriptPubKey.WitHash.ScriptPubKey.Hash)); var previousCoin = previousTx.Outputs.AsCoins().First(); @@ -2658,6 +2646,7 @@ public void CanBuildTransactionWithDustPrevention() var bob = new Key(); var alice = new Key(); var tx = Network.CreateTransaction(); + tx.Inputs.Add(); tx.Outputs.Add(Money.Coins(1.0m), bob); var coins = tx.Outputs.AsCoins().ToArray(); @@ -2852,6 +2841,7 @@ public void CanMutateSignature() public void CanUseLockTime() { var tx = Network.CreateTransaction(); + tx.Inputs.Add(); tx.LockTime = new LockTime(4); var clone = tx.Clone(); Assert.Equal(tx.LockTime, clone.LockTime); @@ -3082,6 +3072,7 @@ public void witnessHasPushSizeLimit() { var bob = new Key().GetWif(Network.RegTest); Transaction tx = Network.CreateTransaction(); + tx.Inputs.Add(); tx.Outputs.Add(new TxOut(Money.Coins(1.0m), bob.PubKey.ScriptPubKey.WitHash)); ScriptCoin coin = new ScriptCoin(tx.Outputs.AsCoins().First(), bob.PubKey.ScriptPubKey); diff --git a/NBitcoin/BitcoinStream.cs b/NBitcoin/BitcoinStream.cs index 0c5f68743..8b80af49f 100644 --- a/NBitcoin/BitcoinStream.cs +++ b/NBitcoin/BitcoinStream.cs @@ -592,6 +592,7 @@ public void CopyParameters(BitcoinStream from) IsBigEndian = from.IsBigEndian; MaxArraySize = from.MaxArraySize; Type = from.Type; + AllowNoInputs = from.AllowNoInputs; } public SerializationType Type @@ -630,6 +631,13 @@ public System.Threading.CancellationToken ReadCancellationToken set; } + /// + /// Allows serialization of transactions with no inputs. + /// Such transactions are not valid for deserialization, but may still be useful, + /// for example, when computing a transaction hash or estimating size. + /// + public bool AllowNoInputs { get; set; } + public void ReadWriteAsVarInt(ref uint val) { if (Serializing) diff --git a/NBitcoin/IBitcoinSerializable.cs b/NBitcoin/IBitcoinSerializable.cs index 682910a2f..42b31e61b 100644 --- a/NBitcoin/IBitcoinSerializable.cs +++ b/NBitcoin/IBitcoinSerializable.cs @@ -32,6 +32,7 @@ public static void ReadWrite(this IBitcoinSerializable serializable, Stream stre public static int GetSerializedSize(this IBitcoinSerializable serializable, uint? version, SerializationType serializationType) { BitcoinStream s = new BitcoinStream(Stream.Null, true); + s.AllowNoInputs = true; s.Type = serializationType; s.ProtocolVersion = version; s.ReadWrite(serializable); @@ -40,6 +41,7 @@ public static int GetSerializedSize(this IBitcoinSerializable serializable, uint public static int GetSerializedSize(this IBitcoinSerializable serializable, TransactionOptions options) { var bms = new BitcoinStream(Stream.Null, true); + bms.AllowNoInputs = true; bms.TransactionOptions = options; serializable.ReadWrite(bms); return (int)bms.Counter.WrittenBytes; diff --git a/NBitcoin/RPC/RPCClient.Wallet.cs b/NBitcoin/RPC/RPCClient.Wallet.cs index 838603949..272ce5e0e 100644 --- a/NBitcoin/RPC/RPCClient.Wallet.cs +++ b/NBitcoin/RPC/RPCClient.Wallet.cs @@ -380,8 +380,12 @@ private string ToHex(Transaction tx) // if there is inputs, then it can't be confusing if (tx.Inputs.Count > 0) return tx.ToHex(); - // if there is, do this ACK so that NBitcoin does not change the version number - return Encoders.Hex.EncodeData(tx.ToBytes(70012 - 1)); + + var ms = new MemoryStream(); + BitcoinStream bs = new BitcoinStream(ms, true); + bs.AllowNoInputs = true; + tx.ReadWrite(bs); + return Encoders.Hex.EncodeData(ms.ToArrayEfficient()); } diff --git a/NBitcoin/Transaction.cs b/NBitcoin/Transaction.cs index 975f36b2d..01a5f50ac 100644 --- a/NBitcoin/Transaction.cs +++ b/NBitcoin/Transaction.cs @@ -1574,11 +1574,8 @@ public virtual void ReadWrite(BitcoinStream stream) { /* The witness flag is present, and we support witnesses. */ flags ^= 1; - if (Inputs.Count != 0) - { - Witness wit = new Witness(Inputs); - wit.ReadWrite(stream); - } + Witness wit = new Witness(Inputs); + wit.ReadWrite(stream); } if (flags != 0) { @@ -1588,6 +1585,8 @@ public virtual void ReadWrite(BitcoinStream stream) } else { + if (Inputs.Count == 0 && !stream.AllowNoInputs) + throw new InvalidOperationException("The transaction must have at least one input"); stream.ReadWrite(ref nVersion); if (witSupported) @@ -1637,6 +1636,7 @@ public uint256 GetHash() { TransactionOptions = TransactionOptions.None, ConsensusFactory = GetConsensusFactory(), + AllowNoInputs = true }; stream.SerializationTypeScope(SerializationType.Hash); this.ReadWrite(stream); diff --git a/NBitcoin/TransactionBuilder.cs b/NBitcoin/TransactionBuilder.cs index c445bb6fa..38f26b5ee 100644 --- a/NBitcoin/TransactionBuilder.cs +++ b/NBitcoin/TransactionBuilder.cs @@ -2443,10 +2443,9 @@ public void EstimateSizes(Transaction tx, out int witSize, out int baseSize) if (tx == null) throw new ArgumentNullException(nameof(tx)); var clone = tx.Clone(); - clone.Inputs.Clear(); - baseSize = clone.GetSerializedSize() - 1; - baseSize += new Protocol.VarInt((ulong)tx.Inputs.Count).GetSerializedSize(); - + clone.RemoveSignatures(); + baseSize = clone.GetSerializedSize(); + baseSize -= clone.Inputs.Count; // The varint to push scriptSig is accounted later witSize = 0; int nonWitnessCount = 0; bool hasWitness = tx.HasWitness; @@ -2460,10 +2459,8 @@ public void EstimateSizes(Transaction tx, out int witSize, out int baseSize) else nonWitnessCount++; EstimateScriptSigSize(coin, ref witSize, ref baseSize); - baseSize += (32 + 4) + 4; } - if (hasWitness) { witSize += 2; // 1 Dummy + 1 Flag