Support sending to P2SH addresses. Thanks to Mike Belshe.

Resolves issue 461.
This commit is contained in:
Mike Hearn 2013-11-30 15:14:52 +01:00
parent 0044c8d269
commit 98081f0568
11 changed files with 126 additions and 29 deletions

View File

@ -38,15 +38,26 @@ public class Address extends VersionedChecksummedBytes {
*/
public static final int LENGTH = 20;
/**
* Construct an address from parameters, the address version, and the hash160 form. Example:<p>
*
* <pre>new Address(NetworkParameters.prodNet(), NetworkParameters.getAddressHeader(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));</pre>
*/
public Address(NetworkParameters params, int version, byte[] hash160) {
super(version, hash160);
if (!isAcceptableVersion(params, version))
throw new RuntimeException("Unrecognized Address version");
if (hash160.length != 20) // 160 = 8 * 20
throw new RuntimeException("Addresses are 160-bit hashes, so you must provide 20 bytes");
}
/**
* Construct an address from parameters and the hash160 form. Example:<p>
*
* <pre>new Address(NetworkParameters.prodNet(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));</pre>
*/
public Address(NetworkParameters params, byte[] hash160) {
super(params.getAddressHeader(), hash160);
if (hash160.length != 20) // 160 = 8 * 20
throw new RuntimeException("Addresses are 160-bit hashes, so you must provide 20 bytes");
this(params, params.getAddressHeader(), hash160);
}
/**
@ -62,14 +73,7 @@ public class Address extends VersionedChecksummedBytes {
public Address(@Nullable NetworkParameters params, String address) throws AddressFormatException, WrongNetworkException {
super(address);
if (params != null) {
boolean found = false;
for (int v : params.getAcceptableAddressCodes()) {
if (version == v) {
found = true;
break;
}
}
if (!found) {
if (!isAcceptableVersion(params, version)) {
throw new WrongNetworkException(version, params.getAcceptableAddressCodes());
}
}
@ -80,6 +84,14 @@ public class Address extends VersionedChecksummedBytes {
return bytes;
}
/*
* Returns true if this address is a Pay-To-Script-Hash (P2SH) address.
* See also https://en.bitcoin.it/wiki/BIP_0013: Address Format for pay-to-script-hash
*/
public boolean isP2SHAddress() {
return this.version == getParameters().p2shHeader;
}
/**
* Examines the version byte of the address and attempts to find a matching NetworkParameters. If you aren't sure
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
@ -92,10 +104,8 @@ public class Address extends VersionedChecksummedBytes {
// TODO: There should be a more generic way to get all supported networks.
NetworkParameters[] networks = { TestNet3Params.get(), MainNetParams.get() };
for (NetworkParameters params : networks) {
for (int code : params.getAcceptableAddressCodes()) {
if (code == version) {
return params;
}
if (isAcceptableVersion(params, version)) {
return params;
}
}
return null;
@ -114,4 +124,16 @@ public class Address extends VersionedChecksummedBytes {
throw new RuntimeException(e);
}
}
/**
* Check if a given address version is valid given the NetworkParameters.
*/
private boolean isAcceptableVersion(NetworkParameters params, int version) {
for (int v : params.getAcceptableAddressCodes()) {
if (version == v) {
return true;
}
}
return false;
}
}

View File

@ -66,6 +66,7 @@ public abstract class NetworkParameters implements Serializable {
protected int port;
protected long packetMagic;
protected int addressHeader;
protected int p2shHeader;
protected int dumpedPrivateKeyHeader;
protected int interval;
protected int targetTimespan;
@ -256,6 +257,13 @@ public abstract class NetworkParameters implements Serializable {
return addressHeader;
}
/**
* First byte of a base58 encoded P2SH address. P2SH addresses are defined as part of BIP0013.
*/
public int getP2SHHeader() {
return p2shHeader;
}
/** First byte of a base58 encoded dumped private key. See {@link com.google.bitcoin.core.DumpedPrivateKey}. */
public int getDumpedPrivateKeyHeader() {
return dumpedPrivateKeyHeader;

View File

@ -31,9 +31,10 @@ public class MainNetParams extends NetworkParameters {
interval = INTERVAL;
targetTimespan = TARGET_TIMESPAN;
proofOfWorkLimit = Utils.decodeCompactBits(0x1d00ffffL);
acceptableAddressCodes = new int[] { 0 };
dumpedPrivateKeyHeader = 128;
addressHeader = 0;
p2shHeader = 5;
acceptableAddressCodes = new int[] { addressHeader, p2shHeader };
port = 8333;
packetMagic = 0xf9beb4d9L;
genesisBlock.setDifficultyTarget(0x1d00ffffL);

View File

@ -32,10 +32,11 @@ public class TestNet2Params extends NetworkParameters {
packetMagic = 0xfabfb5daL;
port = 18333;
addressHeader = 111;
p2shHeader = 196;
acceptableAddressCodes = new int[] { addressHeader, p2shHeader };
interval = INTERVAL;
targetTimespan = TARGET_TIMESPAN;
proofOfWorkLimit = Utils.decodeCompactBits(0x1d0fffffL);
acceptableAddressCodes = new int[] { 111 };
dumpedPrivateKeyHeader = 239;
genesisBlock.setTime(1296688602L);
genesisBlock.setDifficultyTarget(0x1d07fff8L);

View File

@ -37,7 +37,8 @@ public class TestNet3Params extends NetworkParameters {
proofOfWorkLimit = Utils.decodeCompactBits(0x1d00ffffL);
port = 18333;
addressHeader = 111;
acceptableAddressCodes = new int[] { 111 };
p2shHeader = 196;
acceptableAddressCodes = new int[] { addressHeader, p2shHeader };
dumpedPrivateKeyHeader = 239;
genesisBlock.setTime(1296688602L);
genesisBlock.setDifficultyTarget(0x1d00ffffL);

View File

@ -31,6 +31,8 @@ public class UnitTestParams extends NetworkParameters {
id = ID_UNITTESTNET;
packetMagic = 0x0b110907;
addressHeader = 111;
p2shHeader = 196;
acceptableAddressCodes = new int[] { addressHeader, p2shHeader };
proofOfWorkLimit = new BigInteger("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16);
genesisBlock.setTime(System.currentTimeMillis() / 1000);
genesisBlock.setDifficultyTarget(Block.EASIEST_DIFFICULTY_TARGET);
@ -40,7 +42,6 @@ public class UnitTestParams extends NetworkParameters {
dumpedPrivateKeyHeader = 239;
targetTimespan = 200000000; // 6 years. Just a very big number.
spendableCoinbaseDepth = 5;
acceptableAddressCodes = new int[] { 111 };
subsidyDecreaseBlockCount = 100;
dnsSeeds = null;
}

View File

@ -194,6 +194,18 @@ public class Script {
chunks.get(4).equalsOpCode(OP_CHECKSIG);
}
/**
* Returns true if this script is of the form OP_HASH160 <scriptHash> OP_EQUAL, ie, payment to an
* address like 35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU. This form was codified as part of BIP13 and BIP16,
* for pay to script hash type addresses.
*/
public boolean isSentToP2SH() {
return chunks.size() == 3 &&
chunks.get(0).equalsOpCode(OP_HASH160) &&
chunks.get(1).data.length == Address.LENGTH &&
chunks.get(2).equalsOpCode(OP_EQUAL);
}
/**
* If a program matches the standard template DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG
* then this function retrieves the third element, otherwise it throws a ScriptException.<p>
@ -201,10 +213,12 @@ public class Script {
* This is useful for fetching the destination address of a transaction.
*/
public byte[] getPubKeyHash() throws ScriptException {
if (!isSentToAddress())
if (isSentToAddress())
return chunks.get(2).data;
else if (isSentToP2SH())
return chunks.get(1).data;
else
throw new ScriptException("Script not in the standard scriptPubKey form");
// Otherwise, the third element is the hash of the public key, ie the bitcoin address.
return chunks.get(2).data;
}
/**

View File

@ -64,14 +64,23 @@ public class ScriptBuilder {
/** Creates a scriptPubKey that encodes payment to the given address. */
public static Script createOutputScript(Address to) {
// OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
return new ScriptBuilder()
if (to.isP2SHAddress()) {
// OP_HASH160 <scriptHash> OP_EQUAL
return new ScriptBuilder()
.op(OP_HASH160)
.data(to.getHash160())
.op(OP_EQUAL)
.build();
} else {
// OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
return new ScriptBuilder()
.op(OP_DUP)
.op(OP_HASH160)
.data(to.getHash160())
.op(OP_EQUALVERIFY)
.op(OP_CHECKSIG)
.build();
}
}
/** Creates a scriptPubKey that encodes payment to the given raw public key. */

View File

@ -34,9 +34,11 @@ public class AddressTest {
// Test a testnet address.
Address a = new Address(testParams, Hex.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
assertEquals("n4eA2nbYqErp7H6jebchxAN59DmNpksexv", a.toString());
assertFalse(a.isP2SHAddress());
Address b = new Address(mainParams, Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));
assertEquals("17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL", b.toString());
assertFalse(b.isP2SHAddress());
}
@Test
@ -90,4 +92,27 @@ public class AddressTest {
params = Address.getParametersFromAddress("n4eA2nbYqErp7H6jebchxAN59DmNpksexv");
assertEquals(TestNet3Params.get().getId(), params.getId());
}
@Test
public void p2shAddress() throws Exception {
// Test that we can construct P2SH addresses
Address mainNetP2SHAddress = new Address(MainNetParams.get(), "35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU");
assertEquals(mainNetP2SHAddress.version, MainNetParams.get().p2shHeader);
assertTrue(mainNetP2SHAddress.isP2SHAddress());
Address testNetP2SHAddress = new Address(TestNet3Params.get(), "2MuVSxtfivPKJe93EC1Tb9UhJtGhsoWEHCe");
assertEquals(testNetP2SHAddress.version, TestNet3Params.get().p2shHeader);
assertTrue(testNetP2SHAddress.isP2SHAddress());
// Test that we can determine what network a P2SH address belongs to
NetworkParameters mainNetParams = Address.getParametersFromAddress("35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU");
assertEquals(MainNetParams.get().getId(), mainNetParams.getId());
NetworkParameters testNetParams = Address.getParametersFromAddress("2MuVSxtfivPKJe93EC1Tb9UhJtGhsoWEHCe");
assertEquals(TestNet3Params.get().getId(), testNetParams.getId());
// Test that we can convert them from hashes
Address a = new Address(mainParams, MainNetParams.get().p2shHeader, Hex.decode("2ac4b0b501117cc8119c5797b519538d4942e90e"));
assertEquals("35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU", a.toString());
Address b = new Address(testParams, TestNet3Params.get().p2shHeader, Hex.decode("18a0e827269b5211eb51a4af1b2fa69333efa722"));
assertEquals("2MuVSxtfivPKJe93EC1Tb9UhJtGhsoWEHCe", b.toString());
}
}

View File

@ -33,6 +33,7 @@ import com.google.bitcoin.wallet.WalletFiles;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
@ -42,6 +43,7 @@ import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.Hex;
import java.io.File;
import java.math.BigInteger;
@ -106,20 +108,26 @@ public class WalletTest extends TestWithWallet {
@Test
public void basicSpending() throws Exception {
basicSpendingCommon(wallet, myAddress, false);
basicSpendingCommon(wallet, myAddress, new ECKey().toAddress(params), false);
}
@Test
public void basicSpendingToP2SH() throws Exception {
Address destination = new Address(params, params.getP2SHHeader(), Hex.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));
basicSpendingCommon(wallet, myAddress, destination, false);
}
@Test
public void basicSpendingWithEncryptedWallet() throws Exception {
basicSpendingCommon(encryptedWallet, myEncryptedAddress, true);
basicSpendingCommon(encryptedWallet, myEncryptedAddress, new ECKey().toAddress(params), true);
}
@Test
public void basicSpendingWithEncryptedMixedWallet() throws Exception {
basicSpendingCommon(encryptedMixedWallet, myEncryptedAddress2, true);
basicSpendingCommon(encryptedMixedWallet, myEncryptedAddress2, new ECKey().toAddress(params), true);
}
private void basicSpendingCommon(Wallet wallet, Address toAddress, boolean testEncryption) throws Exception {
private void basicSpendingCommon(Wallet wallet, Address toAddress, Address destination, boolean testEncryption) throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. We
// will attach a small fee. Because the Bitcoin protocol makes it difficult to determine the fee of an
// arbitrary transaction in isolation, we'll check that the fee was set by examining the size of the change.
@ -128,7 +136,6 @@ public class WalletTest extends TestWithWallet {
receiveATransaction(wallet, toAddress);
// Try to send too much and fail.
Address destination = new ECKey().toAddress(params);
BigInteger vHuge = toNanoCoins(10, 0);
Wallet.SendRequest req = Wallet.SendRequest.to(destination, vHuge);
try {

View File

@ -17,8 +17,10 @@
package com.google.bitcoin.script;
import com.google.bitcoin.core.*;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.TestNet3Params;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
@ -85,6 +87,12 @@ public class ScriptTest {
// Actual execution is tested by the data driven tests.
}
@Test
public void testP2SHOutputScript() throws Exception {
Address p2shAddress = new Address(MainNetParams.get(), "35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU");
assertTrue(ScriptBuilder.createOutputScript(p2shAddress).isSentToP2SH());
}
@Test
public void testIp() throws Exception {
byte[] bytes = Hex.decode("41043e96222332ea7848323c08116dddafbfa917b8e37f0bdf63841628267148588a09a43540942d58d49717ad3fabfe14978cf4f0a8b84d2435dad16e9aa4d7f935ac");