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:
parent
588e314b06
commit
0d2fb93536
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
60
core/src/main/java/com/google/bitcoin/wallet/RedeemData.java
Normal file
60
core/src/main/java/com/google/bitcoin/wallet/RedeemData.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user