From 48535f9a9c17deec946a4f78daa0a0559301cafe Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Sun, 10 Jul 2011 15:52:06 +0000 Subject: [PATCH] Optimize chain download further by skipping merkle root verification unless there are transactions relevant to a wallet in the block. Refactor some code out of WalletTest into a new static TestUtils class. --- src/com/google/bitcoin/core/Block.java | 33 ++++-- src/com/google/bitcoin/core/BlockChain.java | 109 ++++++++++++------ .../google/bitcoin/store/DiskBlockStore.java | 2 +- .../google/bitcoin/core/BlockChainTest.java | 51 ++++++-- tests/com/google/bitcoin/core/TestUtils.java | 64 ++++++++++ tests/com/google/bitcoin/core/WalletTest.java | 73 +++--------- 6 files changed, 221 insertions(+), 111 deletions(-) create mode 100644 tests/com/google/bitcoin/core/TestUtils.java diff --git a/src/com/google/bitcoin/core/Block.java b/src/com/google/bitcoin/core/Block.java index 3d81a56f..0245edef 100644 --- a/src/com/google/bitcoin/core/Block.java +++ b/src/com/google/bitcoin/core/Block.java @@ -348,13 +348,13 @@ public class Block extends Message { /** * Checks the block data to ensure it follows the rules laid out in the network parameters. Specifically, throws - * an exception if the proof of work is invalid, if the timestamp is too far from what it should be, or if the - * transactions don't hash to the value in the merkle root field. This is not everything that is required - * for a block to be valid, only what is checkable independent of the chain. + * an exception if the proof of work is invalid, or if the timestamp is too far from what it should be. This is + * not everything that is required for a block to be valid, only what is checkable independent of the + * chain and without a transaction index. * * @throws VerificationException */ - public void verify() throws VerificationException { + public void verifyHeader() throws VerificationException { // Prove that this block is OK. It might seem that we can just ignore most of these checks given that the // network is also verifying the blocks, but we cannot as it'd open us to a variety of obscure attacks. // @@ -362,15 +362,28 @@ public class Block extends Message { // enough, it's probably been done by the network. checkProofOfWork(true); checkTimestamp(); + } + + /** + * Checks the block contents + * @throws VerificationException + */ + public void verifyTransactions() throws VerificationException { // Now we need to check that the body of the block actually matches the headers. The network won't generate // an invalid block, but if we didn't validate this then an untrusted man-in-the-middle could obtain the next // valid block from the network and simply replace the transactions in it with their own fictional // transactions that reference spent or non-existant inputs. - if (transactions != null) { - assert transactions.size() > 0; - checkTransactions(); - checkMerkleRoot(); - } + assert transactions.size() > 0; + checkTransactions(); + checkMerkleRoot(); + } + + /** + * Verifies both the header and that the transactions hash to the merkle root. + */ + public void verify() throws VerificationException { + verifyHeader(); + verifyTransactions(); } @Override @@ -490,7 +503,7 @@ public class Block extends Message { b.setTime(time); b.solve(); try { - b.verify(); + b.verifyHeader(); } catch (VerificationException e) { throw new RuntimeException(e); // Cannot happen. } diff --git a/src/com/google/bitcoin/core/BlockChain.java b/src/com/google/bitcoin/core/BlockChain.java index 375efacd..27e91342 100644 --- a/src/com/google/bitcoin/core/BlockChain.java +++ b/src/com/google/bitcoin/core/BlockChain.java @@ -149,12 +149,27 @@ public class BlockChain { return true; } - // Prove the block is internally valid: hash is lower than target, merkle root is correct and so on. + // Does this block contain any transactions we might care about? Check this up front before verifying the + // blocks validity so we can skip the merkle root verification if the contents aren't interesting. This saves + // a lot of time for big blocks. + boolean contentsImportant = false; + HashMap> walletToTxMap = new HashMap>();; + if (block.transactions != null) { + scanTransactions(block, walletToTxMap); + contentsImportant = walletToTxMap.size() > 0; + } + + // Prove the block is internally valid: hash is lower than target, etc. This only checks the block contents + // if there is a tx sending or receiving coins using an address in one of our wallets. And those transactions + // are only lightly verified: presence in a valid connecting block is taken as proof of validity. See the + // article here for more details: http://code.google.com/p/bitcoinj/wiki/SecurityModel try { - block.verify(); + block.verifyHeader(); + if (contentsImportant) + block.verifyTransactions(); } catch (VerificationException e) { - log.error("Failed to verify block:", e); - log.error(block.toString()); + log.error("Failed to verify block: ", e); + log.error(block.getHashAsString()); throw e; } @@ -176,9 +191,7 @@ public class BlockChain { StoredBlock newStoredBlock = storedPrev.build(block); checkDifficultyTransitions(storedPrev, newStoredBlock); blockStore.put(newStoredBlock); - // block.transactions may be null here if we received only a header and not a full block. This does not - // happen currently but might in future if getheaders is implemented. - connectBlock(newStoredBlock, storedPrev, block.transactions); + connectBlock(newStoredBlock, storedPrev, walletToTxMap); } if (tryConnecting) @@ -188,7 +201,8 @@ public class BlockChain { return true; } - private void connectBlock(StoredBlock newStoredBlock, StoredBlock storedPrev, List newTransactions) + private void connectBlock(StoredBlock newStoredBlock, StoredBlock storedPrev, + HashMap> newTransactions) throws BlockStoreException, VerificationException { if (storedPrev.equals(chainHead)) { // This block connects to the best known block, it is a normal continuation of the system. @@ -296,14 +310,16 @@ public class BlockChain { } private void sendTransactionsToWallet(StoredBlock block, NewBlockType blockType, - List newTransactions) throws VerificationException { - // Scan the transactions to find out if any mention addresses we own. - for (Transaction tx : newTransactions) { + HashMap> newTransactions) throws VerificationException { + for (Wallet wallet : newTransactions.keySet()) { try { - scanTransaction(block, tx, blockType); + List txns = newTransactions.get(wallet); + for (Transaction tx : txns) { + wallet.receive(tx, block, blockType); + } } catch (ScriptException e) { - // We don't want scripts we don't understand to break the block chain, - // so just note that this tx was not scanned here and continue. + // We don't want scripts we don't understand to break the block chain so just note that this tx was + // not scanned here and continue. log.warn("Failed to parse a script: " + e.toString()); } } @@ -409,33 +425,50 @@ public class BlockChain { receivedDifficulty.toString(16) + " vs " + newDifficulty.toString(16)); } - private void scanTransaction(StoredBlock block, Transaction tx, NewBlockType blockType) - throws ScriptException, VerificationException { - for (Wallet wallet : wallets) { - boolean shouldReceive = false; - for (TransactionOutput output : tx.outputs) { - // TODO: Handle more types of outputs, not just regular to address outputs. - if (output.getScriptPubKey().isSentToIP()) return; - // This is not thread safe as a key could be removed between the call to isMine and receive. - if (output.isMine(wallet)) { - shouldReceive = true; - } - } - - // Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air). - if (!tx.isCoinBase()) { - for (TransactionInput i : tx.inputs) { - byte[] pubkey = i.getScriptSig().getPubKey(); - // This is not thread safe as a key could be removed between the call to isPubKeyMine and receive. - if (wallet.isPubKeyMine(pubkey)) { - shouldReceive = true; + /** + * For the transactions in the given block, update the txToWalletMap such that each wallet maps to a list of + * transactions for which it is relevant. + */ + private void scanTransactions(Block block, HashMap> walletToTxMap) + throws VerificationException { + for (Transaction tx : block.transactions) { + try { + for (Wallet wallet : wallets) { + boolean shouldReceive = false; + for (TransactionOutput output : tx.outputs) { + // TODO: Handle more types of outputs, not just regular to address outputs. + if (output.getScriptPubKey().isSentToIP()) return; + // This is not thread safe as a key could be removed between the call to isMine and receive. + if (output.isMine(wallet)) { + shouldReceive = true; + } } + + // Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air). + if (!shouldReceive && !tx.isCoinBase()) { + for (TransactionInput i : tx.inputs) { + byte[] pubkey = i.getScriptSig().getPubKey(); + // This is not thread safe as a key could be removed between the call to isPubKeyMine and receive. + if (wallet.isPubKeyMine(pubkey)) { + shouldReceive = true; + } + } + } + + if (!shouldReceive) continue; + List txList = walletToTxMap.get(wallet); + if (txList == null) { + txList = new LinkedList(); + walletToTxMap.put(wallet, txList); + } + txList.add(tx); } + } catch (ScriptException e) { + // We don't want scripts we don't understand to break the block chain so just note that this tx was + // not scanned here and continue. + log.warn("Failed to parse a script: " + e.toString()); } - - if (shouldReceive) - wallet.receive(tx, block, blockType); - } + } } /** diff --git a/src/com/google/bitcoin/store/DiskBlockStore.java b/src/com/google/bitcoin/store/DiskBlockStore.java index c3fd9db1..9401dd51 100644 --- a/src/com/google/bitcoin/store/DiskBlockStore.java +++ b/src/com/google/bitcoin/store/DiskBlockStore.java @@ -115,7 +115,7 @@ public class DiskBlockStore implements BlockStore { } } else { // Don't try to verify the genesis block to avoid upsetting the unit tests. - b.verify(); + b.verifyHeader(); // Calculate its height and total chain work. s = prev.build(b); } diff --git a/tests/com/google/bitcoin/core/BlockChainTest.java b/tests/com/google/bitcoin/core/BlockChainTest.java index 1f0bc5c8..4aad9880 100644 --- a/tests/com/google/bitcoin/core/BlockChainTest.java +++ b/tests/com/google/bitcoin/core/BlockChainTest.java @@ -16,16 +16,18 @@ package com.google.bitcoin.core; -import com.google.bitcoin.bouncycastle.util.encoders.Hex; +import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.MemoryBlockStore; import org.junit.Before; import org.junit.Test; import java.math.BigInteger; +import static com.google.bitcoin.core.TestUtils.createFakeBlock; +import static com.google.bitcoin.core.TestUtils.createFakeTx; import static org.junit.Assert.*; -// NOTE: Handling of chain splits/reorgs are in ChainSplitTests. +// Handling of chain splits/reorgs are in ChainSplitTests. public class BlockChainTest { private static final NetworkParameters testNet = NetworkParameters.testNet(); @@ -33,21 +35,27 @@ public class BlockChainTest { private Wallet wallet; private BlockChain chain; + private BlockStore blockStore; private Address coinbaseTo; private NetworkParameters unitTestParams; - private Address someOtherGuy; + + private void resetBlockStore() { + blockStore = new MemoryBlockStore(unitTestParams); + } @Before public void setUp() { + testNetChain = new BlockChain(testNet, new Wallet(testNet), new MemoryBlockStore(testNet)); unitTestParams = NetworkParameters.unitTests(); wallet = new Wallet(unitTestParams); wallet.addKey(new ECKey()); - chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams)); + + resetBlockStore(); + chain = new BlockChain(unitTestParams, wallet, blockStore); coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams); - someOtherGuy = new ECKey().toAddress(unitTestParams); } @Test @@ -68,10 +76,39 @@ public class BlockChainTest { } catch (VerificationException e) { b2.setNonce(n); } + // Now it works because we reset the nonce. assertTrue(testNetChain.add(b2)); } + @Test + public void merkleRoots() throws Exception { + // Test that merkle root verification takes place when a relevant transaction is present and doesn't when + // there isn't any such tx present (as an optimization). + Transaction tx1 = createFakeTx(unitTestParams, + Utils.toNanoCoins(1, 0), + wallet.keychain.get(0).toAddress(unitTestParams)); + Block b1 = createFakeBlock(unitTestParams, blockStore, tx1).block; + chain.add(b1); + resetBlockStore(); + Sha256Hash hash = b1.getMerkleRoot(); + b1.setMerkleRoot(Sha256Hash.ZERO_HASH); + try { + chain.add(b1); + fail(); + } catch (VerificationException e) { + // Expected. + b1.setMerkleRoot(hash); + } + // Now add a second block with no relevant transactions and then break it. + Transaction tx2 = createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), + new ECKey().toAddress(unitTestParams)); + Block b2 = createFakeBlock(unitTestParams, blockStore, tx2).block; + hash = b2.getMerkleRoot(); + b2.setMerkleRoot(Sha256Hash.ZERO_HASH); + chain.add(b2); // Broken block is accepted because its contents don't matter to us. + } + @Test public void testUnconnectedBlocks() throws Exception { Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); @@ -163,7 +200,7 @@ public class BlockChainTest { b2.setTime(1296734343L); b2.setPrevBlockHash(new Sha256Hash("000000033cc282bc1fa9dcae7a533263fd7fe66490f550d80076433340831604")); assertEquals("000000037b21cac5d30fc6fda2581cf7b2612908aed2abbcc429c45b0557a15f", b2.getHashAsString()); - b2.verify(); + b2.verifyHeader(); return b2; } @@ -174,7 +211,7 @@ public class BlockChainTest { b1.setTime(1296734340); b1.setPrevBlockHash(new Sha256Hash("00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008")); assertEquals("000000033cc282bc1fa9dcae7a533263fd7fe66490f550d80076433340831604", b1.getHashAsString()); - b1.verify(); + b1.verifyHeader(); return b1; } } diff --git a/tests/com/google/bitcoin/core/TestUtils.java b/tests/com/google/bitcoin/core/TestUtils.java new file mode 100644 index 00000000..4a631000 --- /dev/null +++ b/tests/com/google/bitcoin/core/TestUtils.java @@ -0,0 +1,64 @@ +/** + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import com.google.bitcoin.store.BlockStore; +import com.google.bitcoin.store.BlockStoreException; + +import java.math.BigInteger; + +public class TestUtils { + public static Transaction createFakeTx(NetworkParameters params, BigInteger nanocoins, Address to) { + Transaction t = new Transaction(params); + TransactionOutput o1 = new TransactionOutput(params, t, nanocoins, to); + t.addOutput(o1); + // Make a previous tx simply to send us sufficient coins. This prev tx is not really valid but it doesn't + // matter for our purposes. + Transaction prevTx = new Transaction(params); + TransactionOutput prevOut = new TransactionOutput(params, prevTx, nanocoins, to); + prevTx.addOutput(prevOut); + // Connect it. + t.addInput(prevOut); + return t; + } + + public static class BlockPair { + StoredBlock storedBlock; + Block block; + } + + // Emulates receiving a valid block that builds on top of the chain. + public static BlockPair createFakeBlock(NetworkParameters params, BlockStore blockStore, + Transaction... transactions) { + try { + Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params)); + for (Transaction tx : transactions) + b.addTransaction(tx); + b.solve(); + BlockPair pair = new BlockPair(); + pair.block = b; + pair.storedBlock = blockStore.getChainHead().build(b); + blockStore.put(pair.storedBlock); + blockStore.setChainHead(pair.storedBlock); + return pair; + } catch (VerificationException e) { + throw new RuntimeException(e); // Cannot happen. + } catch (BlockStoreException e) { + throw new RuntimeException(e); // Cannot happen. + } + } +} diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 911fd3cc..761a7435 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -24,6 +24,8 @@ import org.junit.Test; import java.math.BigInteger; +import static com.google.bitcoin.core.TestUtils.createFakeBlock; +import static com.google.bitcoin.core.TestUtils.createFakeTx; import static com.google.bitcoin.core.Utils.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -46,50 +48,11 @@ public class WalletTest { blockStore = new MemoryBlockStore(params); } - private Transaction createFakeTx(BigInteger nanocoins, Address to) { - Transaction t = new Transaction(params); - TransactionOutput o1 = new TransactionOutput(params, t, nanocoins, to); - t.addOutput(o1); - // Make a previous tx simply to send us sufficient coins. This prev tx is not really valid but it doesn't - // matter for our purposes. - Transaction prevTx = new Transaction(params); - TransactionOutput prevOut = new TransactionOutput(params, prevTx, nanocoins, to); - prevTx.addOutput(prevOut); - // Connect it. - t.addInput(prevOut); - return t; - } - - class BlockPair { - StoredBlock storedBlock; - Block block; - } - - // Emulates receiving a valid block that builds on top of the chain. - private BlockPair createFakeBlock(Transaction... transactions) { - try { - Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params)); - for (Transaction tx : transactions) - b.addTransaction(tx); - b.solve(); - BlockPair pair = new BlockPair(); - pair.block = b; - pair.storedBlock = blockStore.getChainHead().build(b); - blockStore.put(pair.storedBlock); - blockStore.setChainHead(pair.storedBlock); - return pair; - } catch (VerificationException e) { - throw new RuntimeException(e); // Cannot happen. - } catch (BlockStoreException e) { - throw new RuntimeException(e); // Cannot happen. - } - } - @Test public void basicSpending() throws Exception { // We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. BigInteger v1 = Utils.toNanoCoins(1, 0); - Transaction t1 = createFakeTx(v1, myAddress); + Transaction t1 = createFakeTx(params, v1, myAddress); wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); @@ -109,13 +72,13 @@ public class WalletTest { public void sideChain() throws Exception { // The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance. BigInteger v1 = Utils.toNanoCoins(1, 0); - Transaction t1 = createFakeTx(v1, myAddress); + Transaction t1 = createFakeTx(params, v1, myAddress); wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); BigInteger v2 = toNanoCoins(0, 50); - Transaction t2 = createFakeTx(v2, myAddress); + Transaction t2 = createFakeTx(params, v2, myAddress); wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN); assertEquals(v1, wallet.getBalance()); @@ -123,7 +86,7 @@ public class WalletTest { @Test public void listeners() throws Exception { - final Transaction fakeTx = createFakeTx(Utils.toNanoCoins(1, 0), myAddress); + final Transaction fakeTx = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress); final boolean[] didRun = new boolean[1]; WalletEventListener listener = new WalletEventListener() { public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { @@ -144,10 +107,10 @@ public class WalletTest { // Receive 5 coins then half a coin. BigInteger v1 = toNanoCoins(5, 0); BigInteger v2 = toNanoCoins(0, 50); - Transaction t1 = createFakeTx(v1, myAddress); - Transaction t2 = createFakeTx(v2, myAddress); - StoredBlock b1 = createFakeBlock(t1).storedBlock; - StoredBlock b2 = createFakeBlock(t2).storedBlock; + Transaction t1 = createFakeTx(params, v1, myAddress); + Transaction t2 = createFakeTx(params, v2, myAddress); + StoredBlock b1 = createFakeBlock(params, blockStore, t1).storedBlock; + StoredBlock b2 = createFakeBlock(params, blockStore, t2).storedBlock; BigInteger expected = toNanoCoins(5, 50); wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN); @@ -165,7 +128,7 @@ public class WalletTest { wallet.getBalance(Wallet.BalanceType.ESTIMATED))); // Now confirm the transaction by including it into a block. - StoredBlock b3 = createFakeBlock(spend).storedBlock; + StoredBlock b3 = createFakeBlock(params, blockStore, spend).storedBlock; wallet.receive(spend, b3, BlockChain.NewBlockType.BEST_CHAIN); // Change is confirmed. We started with 5.50 so we should have 4.50 left. @@ -180,22 +143,22 @@ public class WalletTest { @Test public void blockChainCatchup() throws Exception { - Transaction tx1 = createFakeTx(Utils.toNanoCoins(1, 0), myAddress); - StoredBlock b1 = createFakeBlock(tx1).storedBlock; + Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress); + StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock; wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN); // Send 0.10 to somebody else. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // Pretend it makes it into the block chain, our wallet state is cleared but we still have the keys, and we // want to get back to our previous state. We can do this by just not confirming the transaction as // createSend is stateless. - StoredBlock b2 = createFakeBlock(send1).storedBlock; + StoredBlock b2 = createFakeBlock(params, blockStore, send1).storedBlock; wallet.receive(send1, b2, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.90"); // And we do it again after the catchup. Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // What we'd really like to do is prove the official client would accept it .... no such luck unfortunately. wallet.confirmSend(send2); - StoredBlock b3 = createFakeBlock(send2).storedBlock; + StoredBlock b3 = createFakeBlock(params, blockStore, send2).storedBlock; wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80"); } @@ -203,7 +166,7 @@ public class WalletTest { @Test public void balances() throws Exception { BigInteger nanos = Utils.toNanoCoins(1, 0); - Transaction tx1 = createFakeTx(nanos, myAddress); + Transaction tx1 = createFakeTx(params, nanos, myAddress); wallet.receive(tx1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(nanos, tx1.getValueSentToMe(wallet, true)); // Send 0.10 to somebody else. @@ -216,7 +179,7 @@ public class WalletTest { @Test public void transactions() throws Exception { // This test covers a bug in which Transaction.getValueSentFromMe was calculating incorrectly. - Transaction tx = createFakeTx(Utils.toNanoCoins(1, 0), myAddress); + Transaction tx = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress); // Now add another output (ie, change) that goes to some other address. Address someOtherGuy = new ECKey().toAddress(params); TransactionOutput output = new TransactionOutput(params, tx, Utils.toNanoCoins(0, 5), someOtherGuy); @@ -255,7 +218,7 @@ public class WalletTest { // Receive 1 BTC. BigInteger nanos = Utils.toNanoCoins(1, 0); - Transaction t1 = createFakeTx(nanos, myAddress); + Transaction t1 = createFakeTx(params, nanos, myAddress); wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); // Create a send to a merchant. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));