3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 06:44:16 +00:00

Introduce MultisigKeyBag to expose P2SH redeem data

For married wallets KeyChainGroup now keeps redeem scripts together with keys
This commit is contained in:
Kosta Korenkov 2014-08-04 19:44:08 +04:00 committed by Mike Hearn
parent 588e314b06
commit 0d2fb93536
10 changed files with 145 additions and 42 deletions

View File

@ -18,6 +18,7 @@ package com.google.bitcoin.core;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.wallet.KeyBag;
import com.google.bitcoin.wallet.RedeemData;
import javax.annotation.Nullable;
import java.io.IOException;
@ -134,7 +135,7 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
}
/**
* Returns the ECKey identified in the connected output, for either pay-to-address scripts or pay-to-key scripts.
* Returns the ECKey identified in the connected output, for either pay-to-address scripts, pay-to-key or P2SH scripts.
* If the script forms cannot be understood, throws ScriptException.
*
* @return an ECKey or null if the connected key cannot be found in the wallet.
@ -150,6 +151,12 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
} else if (connectedScript.isSentToRawPubKey()) {
byte[] pubkeyBytes = connectedScript.getPubKey();
return keyBag.findKeyFromPubKey(pubkeyBytes);
} else if (connectedScript.isPayToScriptHash()) {
byte[] scriptHash = connectedScript.getPubKeyHash();
RedeemData redeemData = keyBag.findRedeemDataFromScriptHash(scriptHash);
if (redeemData == null)
return null;
return redeemData.getFullKey();
} else {
throw new ScriptException("Could not understand form of connected output script: " + connectedScript);
}

View File

@ -784,24 +784,24 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
}
/**
* <p>Locates a script from the KeyChainGroup given the script hash. This is needed when finding out which
* script we need to use to redeem a transaction output.</p>
* Returns null if no such script found
* Locates a redeem data (redeem script and keys) from the keychain given the hash of the script.
* Returns RedeemData object or null if no such data was found.
*/
@Nullable
public Script findRedeemScriptFromPubHash(byte[] payToScriptHash) {
public RedeemData findRedeemDataFromScriptHash(byte[] payToScriptHash) {
lock.lock();
try {
return keychain.findRedeemScriptFromPubHash(payToScriptHash);
return keychain.findRedeemDataFromScriptHash(payToScriptHash);
} finally {
lock.unlock();
}
}
/**
* Returns true if this wallet knows the script corresponding to the given hash
*/
public boolean isPayToScriptHashMine(byte[] payToScriptHash) {
return findRedeemScriptFromPubHash(payToScriptHash) != null;
return findRedeemDataFromScriptHash(payToScriptHash) != null;
}
/**
@ -3964,7 +3964,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
key = findKeyFromPubHash(script.getPubKeyHash());
checkNotNull(key, "Coin selection includes unspendable outputs");
} else if (script.isPayToScriptHash()) {
redeemScript = keychain.findRedeemScriptFromPubHash(script.getPubKeyHash());
redeemScript = findRedeemDataFromScriptHash(script.getPubKeyHash()).redeemScript;
checkNotNull(redeemScript, "Coin selection includes unspendable outputs");
}
size += script.getNumberOfBytesRequiredToSpend(key, redeemScript);

View File

@ -193,7 +193,6 @@ public class BasicKeyChain implements EncryptableKeyChain {
}
}
@Override
public ECKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock();
try {
@ -203,7 +202,6 @@ public class BasicKeyChain implements EncryptableKeyChain {
}
}
@Override
public ECKey findKeyFromPubKey(byte[] pubkey) {
lock.lock();
try {

View File

@ -21,6 +21,9 @@ import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
/**
@ -52,4 +55,15 @@ public class DecryptingKeyBag implements KeyBag {
public ECKey findKeyFromPubKey(byte[] pubkey) {
return maybeDecrypt(target.findKeyFromPubKey(pubkey));
}
@Nullable
@Override
public RedeemData findRedeemDataFromScriptHash(byte[] scriptHash) {
RedeemData redeemData = target.findRedeemDataFromScriptHash(scriptHash);
List<ECKey> decryptedKeys = new ArrayList<ECKey>();
for (ECKey key : redeemData.keys) {
decryptedKeys.add(maybeDecrypt(key));
}
return RedeemData.of(decryptedKeys, redeemData.redeemScript);
}
}

View File

@ -388,7 +388,6 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
return k;
}
@Override
public DeterministicKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock();
try {
@ -398,7 +397,6 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
}
}
@Override
public DeterministicKey findKeyFromPubKey(byte[] pubkey) {
lock.lock();
try {

View File

@ -18,9 +18,11 @@ package com.google.bitcoin.wallet;
import com.google.bitcoin.core.ECKey;
import javax.annotation.Nullable;
/**
* A KeyBag is simply an object that can map public keys and their 160-bit hashes to ECKey objects. All
* {@link com.google.bitcoin.wallet.KeyChain}s are key bags.
* A KeyBag is simply an object that can map public keys, their 160-bit hashes and script hashes to ECKey
* and {@link RedeemData} objects.
*/
public interface KeyBag {
/**
@ -37,4 +39,15 @@ public interface KeyBag {
* @return ECKey or null if no such key was found.
*/
public ECKey findKeyFromPubKey(byte[] pubkey);
/**
* Locates a redeem data (redeem script and keys) from the keychain given the hash of the script.
* This is needed when finding out which key and script we need to use to locally sign a P2SH transaction input.
* It is assumed that wallet should not have more than one private key for a single P2SH tx for security reasons.
*
* Returns RedeemData object or null if no such data was found.
*/
@Nullable
public RedeemData findRedeemDataFromScriptHash(byte[] scriptHash);
}

View File

@ -34,7 +34,7 @@ import java.util.concurrent.Executor;
* restrictions is to support key chains that may be handled by external hardware or software, or which are derived
* deterministically from a seed (and thus the notion of importing a key is meaningless).</p>
*/
public interface KeyChain extends KeyBag {
public interface KeyChain {
/** Returns true if the given key is in the chain. */
public boolean hasKey(ECKey key);

View File

@ -63,7 +63,7 @@ import static com.google.common.base.Preconditions.*;
* <p>Deterministic key chains have a concept of a lookahead size and threshold. Please see the discussion in the
* class docs for {@link com.google.bitcoin.wallet.DeterministicKeyChain} for more information on this topic.</p>
*/
public class KeyChainGroup {
public class KeyChainGroup implements KeyBag {
private static final Logger log = LoggerFactory.getLogger(KeyChainGroup.class);
private BasicKeyChain basic;
@ -74,8 +74,9 @@ public class KeyChainGroup {
// The map keys are the watching keys of the followed chains and values are the following chains
private Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains;
// The map holds P2SH redeem scripts issued by this KeyChainGroup (including lookahead) mapped to their scriptPubKey hashes.
private LinkedHashMap<ByteString, Script> marriedKeysScripts;
// The map holds P2SH redeem script and corresponding ECKeys issued by this KeyChainGroup (including lookahead)
// mapped to redeem script hashes.
private LinkedHashMap<ByteString, RedeemData> marriedKeysRedeemData;
private EnumMap<KeyChain.KeyPurpose, Address> currentAddresses;
@Nullable private KeyCrypter keyCrypter;
@ -154,7 +155,7 @@ public class KeyChainGroup {
if (followingKeychains != null) {
this.followingKeychains.putAll(followingKeychains);
}
marriedKeysScripts = new LinkedHashMap<ByteString, Script>();
marriedKeysRedeemData = new LinkedHashMap<ByteString, RedeemData>();
maybeLookaheadScripts();
if (!this.followingKeychains.isEmpty()) {
@ -167,7 +168,7 @@ public class KeyChainGroup {
}
/**
* This keeps {@link #marriedKeysScripts} in sync with the number of keys issued
* This keeps {@link #marriedKeysRedeemData} in sync with the number of keys issued
*/
private void maybeLookaheadScripts() {
if (chains.isEmpty())
@ -178,17 +179,16 @@ public class KeyChainGroup {
numLeafKeys += chain.getLeafKeys().size();
}
checkState(marriedKeysScripts.size() <= numLeafKeys, "Number of scripts is greater than number of leaf keys");
if (marriedKeysScripts.size() == numLeafKeys)
checkState(marriedKeysRedeemData.size() <= numLeafKeys, "Number of scripts is greater than number of leaf keys");
if (marriedKeysRedeemData.size() == numLeafKeys)
return;
for (DeterministicKeyChain chain : chains) {
if (isMarried(chain)) {
for (DeterministicKey followedKey : chain.getLeafKeys()) {
Script redeemScript = makeRedeemScript(followedKey, chain.getWatchingKey());
Script scriptPubKey = ScriptBuilder.createP2SHOutputScript(redeemScript);
marriedKeysScripts.put(ByteString.copyFrom(scriptPubKey.getPubKeyHash()), redeemScript);
RedeemData redeemData = getRedeemData(followedKey, chain.getWatchingKey());
Script scriptPubKey = ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript);
marriedKeysRedeemData.put(ByteString.copyFrom(scriptPubKey.getPubKeyHash()), redeemData);
}
}
}
@ -425,16 +425,13 @@ public class KeyChainGroup {
return importKeys(encryptedKeys);
}
/**
* <p>Returns redeem script for the given scriptPubKey hash.
* Returns null if no such script found
*/
@Nullable
public Script findRedeemScriptFromPubHash(byte[] payToScriptHash) {
return marriedKeysScripts.get(ByteString.copyFrom(payToScriptHash));
public RedeemData findRedeemDataFromScriptHash(byte[] scriptHash) {
return marriedKeysRedeemData.get(ByteString.copyFrom(scriptHash));
}
@Nullable
@Override
public ECKey findKeyFromPubHash(byte[] pubkeyHash) {
ECKey result;
if ((result = basic.findKeyFromPubHash(pubkeyHash)) != null)
@ -482,6 +479,7 @@ public class KeyChainGroup {
}
@Nullable
@Override
public ECKey findKeyFromPubKey(byte[] pubkey) {
ECKey result;
if ((result = basic.findKeyFromPubKey(pubkey)) != null)
@ -626,9 +624,9 @@ public class KeyChainGroup {
if (basic.numKeys() > 0)
filter.merge(basic.getFilter(size, falsePositiveRate, nTweak));
for (Map.Entry<ByteString, Script> entry : marriedKeysScripts.entrySet()) {
for (Map.Entry<ByteString, RedeemData> entry : marriedKeysRedeemData.entrySet()) {
filter.insert(entry.getKey().toByteArray());
filter.insert(ScriptBuilder.createP2SHOutputScript(entry.getValue()).getProgram());
filter.insert(ScriptBuilder.createP2SHOutputScript(entry.getValue().redeemScript).getProgram());
}
for (DeterministicKeyChain chain : chains) {
@ -649,13 +647,13 @@ public class KeyChainGroup {
}
private Script makeP2SHOutputScript(DeterministicKey followedKey, DeterministicKey followedAccountKey) {
return ScriptBuilder.createP2SHOutputScript(makeRedeemScript(followedKey, followedAccountKey));
return ScriptBuilder.createP2SHOutputScript(getRedeemData(followedKey, followedAccountKey).redeemScript);
}
private Script makeRedeemScript(DeterministicKey followedKey, DeterministicKey followedAccountKey) {
private RedeemData getRedeemData(DeterministicKey followedKey, DeterministicKey followedAccountKey) {
Collection<DeterministicKeyChain> followingChains = followingKeychains.get(followedAccountKey);
List<ECKey> marriedKeys = getMarriedKeysWithFollowed(followedKey, followingChains);
return makeRedeemScript(marriedKeys);
return RedeemData.of(marriedKeys, makeRedeemScript(marriedKeys));
}
private Script makeRedeemScript(List<ECKey> marriedKeys) {
@ -868,8 +866,8 @@ public class KeyChainGroup {
builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58()));
}
builder2.append(String.format("%n"));
for (Script script : marriedKeysScripts.values())
formatScript(ScriptBuilder.createP2SHOutputScript(script), builder2);
for (RedeemData redeemData : marriedKeysRedeemData.values())
formatScript(ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript), builder2);
} else {
for (ECKey key : chain.getKeys())
formatKeyWithAddress(includePrivateKeys, key, builder2);

View File

@ -0,0 +1,60 @@
/**
* Copyright 2014 Kosta Korenkov
*
* 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.wallet;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.script.Script;
import javax.annotation.Nullable;
import java.util.List;
/**
* This class aggregates portion of data required to spend transaction output.
*
* For pay-to-address and pay-to-pubkey transactions it will have only a single key and no redeem script.
* For multisignature transactions there will be multiple keys one of which will be a full key and the rest are watch only.
* For P2SH transactions there also will be a redeem script.
*/
public class RedeemData {
@Nullable public final Script redeemScript;
public final List<ECKey> keys;
private RedeemData(List<ECKey> keys, @Nullable Script redeemScript) {
this.redeemScript = redeemScript;
this.keys = keys;
}
public static RedeemData of(List<ECKey> keys, @Nullable Script redeemScript) {
return new RedeemData(keys, redeemScript);
}
/**
* Returns the first key that has private bytes
*/
public ECKey getFullKey() {
for (ECKey key : keys) {
//TODO: don't use exception catching here to test. It's better to use hasPrivKey, but currently it's not working
// as expected for DeterministicKeys (it doesn't test if it's possible to derive private key)
try {
if (key.getPrivKey() != null)
return key;
} catch (IllegalStateException e) {
// no private bytes. Proceed to the next key
}
}
return null;
}
}

View File

@ -182,6 +182,21 @@ public class KeyChainGroupTest {
assertEquals(a2, a3);
}
@Test
public void findRedeemData() throws Exception {
group = createMarriedKeyChainGroup();
// test script hash that we don't have
assertNull(group.findRedeemDataFromScriptHash(new ECKey().getPubKey()));
// test our script hash
Address address = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
RedeemData redeemData = group.findRedeemDataFromScriptHash(address.getHash160());
assertNotNull(redeemData);
assertNotNull(redeemData.redeemScript);
assertEquals(2, redeemData.keys.size());
}
// Check encryption with and without a basic keychain.
@Test
@ -310,15 +325,15 @@ public class KeyChainGroupTest {
public void findRedeemScriptFromPubHash() throws Exception {
group = createMarriedKeyChainGroup();
Address address = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertTrue(group.findRedeemScriptFromPubHash(address.getHash160()) != null);
assertTrue(group.findRedeemDataFromScriptHash(address.getHash160()) != null);
KeyChainGroup group2 = createMarriedKeyChainGroup();
group2.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
// test address from lookahead zone and lookahead threshold zone
for (int i = 0; i < LOOKAHEAD_SIZE + group.getLookaheadThreshold(); i++) {
address = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertTrue(group2.findRedeemScriptFromPubHash(address.getHash160()) != null);
assertTrue(group2.findRedeemDataFromScriptHash(address.getHash160()) != null);
}
assertFalse(group2.findRedeemScriptFromPubHash(group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS).getHash160()) != null);
assertFalse(group2.findRedeemDataFromScriptHash(group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS).getHash160()) != null);
}
@Test