• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bernardladenthin / BitcoinAddressFinder / #331

07 Jun 2025 02:05PM UTC coverage: 65.684% (+0.1%) from 65.576%
#331

push

bernardladenthin
feat: add witness version constants and refactor Bech32 handling

- Introduce constants for WITNESS_VERSION_0 and WITNESS_VERSION_1
- Refactor Bech32 decoding using reflection utility method
- Handle unsupported witness versions explicitly (e.g. version 2)
- Add unit test for invalid witness version
- Add randomized test data for P2WPKH, P2WSH, and P2TR addresses

18 of 18 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

1248 of 1900 relevant lines covered (65.68%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.61
/src/main/java/net/ladenthin/bitcoinaddressfinder/AddressTxtLine.java
1
// @formatter:off
2
/**
3
 * Copyright 2020 Bernard Ladenthin bernard.ladenthin@gmail.com
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *    http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
// @formatter:on
19
package net.ladenthin.bitcoinaddressfinder;
20

21
import com.github.kiulian.converter.AddressConverter;
22
import java.lang.reflect.Method;
23
import java.nio.ByteBuffer;
24
import org.bitcoinj.base.Base58;
25
import org.bitcoinj.base.Bech32;
26
import org.bitcoinj.base.Coin;
27
import org.bitcoinj.base.SegwitAddress;
28
import org.bitcoinj.base.exceptions.AddressFormatException;
29
import org.jspecify.annotations.Nullable;
30

31

32
/**
33
 * Most txt files have a common format which uses Base58 address and separated
34
 * anmount.
35
 */
36
public class AddressTxtLine {
1✔
37

38
    /**
39
     * Should not be {@link Coin#ZERO} because it can't be written to LMDB.
40
     */
41
    public static final Coin DEFAULT_COIN = Coin.SATOSHI;
1✔
42

43
    public static final String IGNORE_LINE_PREFIX = "#";
44
    public static final String ADDRESS_HEADER = "address";
45
    
46
    
47
    private final static int VERSION_BYTES_REGULAR = 1;
48
    private final static int VERSION_BYTES_ZCASH = 2;
49
    
50
    /**
51
    * Witness version 0, used for SegWit v0 addresses such as:
52
    * <ul>
53
    *   <li><b>P2WPKH</b> – Pay to Witness Public Key Hash</li>
54
    *   <li><b>P2WSH</b> – Pay to Witness Script Hash</li>
55
    * </ul>
56
    * Defined in <a href="https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki">BIP-173</a>.
57
    */
58
   private static final int WITNESS_VERSION_0 = 0;
59

60
   /**
61
    * Witness version 1, introduced with Taproot (SegWit v1):
62
    * <ul>
63
    *   <li><b>P2TR</b> – Pay to Taproot</li>
64
    * </ul>
65
    * Defined in <a href="https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki">BIP-341</a> and
66
    * encoded using Bech32m as specified in <a href="https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki">BIP-350</a>.
67
    */
68
   private static final int WITNESS_VERSION_1 = 1;
69
    
70
    /**
71
     * Parses a line containing an address and optional amount.
72
     * Returns {@code null} if the address is unsupported, malformed, or marked as ignored.
73
     * <p>
74
     * If no coin amount is specified in the line, {@link #DEFAULT_COIN} is used as a fallback.
75
     *
76
     * @param line the line to parse
77
     * @param keyUtility the {@link KeyUtility} used for conversions
78
     * @return an {@link AddressToCoin} instance, or {@code null} if the address is invalid or unsupported
79
     */
80
    @Nullable
81
    public AddressToCoin fromLine(String line, KeyUtility keyUtility) {
82
        String[] lineSplitted = SeparatorFormat.split(line);
1✔
83
        String address = lineSplitted[0];
1✔
84
        Coin amount = getCoinIfPossible(lineSplitted, DEFAULT_COIN);
1✔
85
        address = address.trim();
1✔
86
        if (address.isEmpty() || address.startsWith(IGNORE_LINE_PREFIX) || address.startsWith(ADDRESS_HEADER)) {
1✔
87
            return null;
1✔
88
        }
89
        
90
        // Riecoin: ScriptPubKey-style encoded address (hex with OP codes)
91
        {
92
            final String OP_DUP = "76";
1✔
93
            final String OP_HASH160 = "a9";
1✔
94
            final String OP_PUSH_20_BYTES = "14";
1✔
95
            final int length20Bytes = PublicKeyBytes.RIPEMD160_HASH_NUM_BYTES;
1✔
96
            final String riecoinP2SHPrefix = OP_DUP + OP_HASH160 + OP_PUSH_20_BYTES;
1✔
97
            final int riecoinScriptPubKeyLengthHex = length20Bytes * 2 + riecoinP2SHPrefix.length();
1✔
98
            if (address.length() >= riecoinScriptPubKeyLengthHex && address.startsWith(riecoinP2SHPrefix)) {
1✔
99
                final String hash160Hex = address.substring(riecoinP2SHPrefix.length(), length20Bytes*2+riecoinP2SHPrefix.length());
1✔
100
                final ByteBuffer hash160 = keyUtility.byteBufferUtility.getByteBufferFromHex(hash160Hex);
1✔
101
                return new AddressToCoin(hash160, amount);
1✔
102
            }
103
        }
104

105
        // Blockchair Multisig (P2MS) format is not supported
106
        if (
1✔
107
               address.startsWith("d-")
1✔
108
            || address.startsWith("m-")
1✔
109
            || address.startsWith("s-")
1✔
110
        ) {
111
            return null;
1✔
112
        }
113
        
114
        // BitCore WKH format (Base36-encoded hash160)
115
        if (address.startsWith("wkh_")) {
1✔
116
            // BitCore (WKH) is base36 encoded hash160
117
            String addressWKH = address.substring("wkh_".length());
1✔
118
            byte[] hash160 = new Base36Decoder().decodeBase36ToFixedLengthBytes(addressWKH, PublicKeyBytes.RIPEMD160_HASH_NUM_BYTES);
1✔
119
            ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
120
            return new AddressToCoin(hash160AsByteBuffer, amount);
1✔
121
        }
122
        
123
        // Bech32 decoding (P2WPKH, P2WSH, P2TR)
124
        try {
125
            Bech32.Bech32Data bechData = Bech32.decode(address);
1✔
126
            
127
            // protected: bechData.witnessVersion();
128
            short witnessVersion = invokeProtectedMethod(bechData, "witnessVersion", Short.class);
1✔
129
            // protected: bechData.witnessProgram();
130
            byte[] witnessProgram = invokeProtectedMethod(bechData, "witnessProgram", byte[].class);
1✔
131
            
132
            switch (witnessVersion) {
1✔
133
                case WITNESS_VERSION_0:
134
                    if (witnessProgram.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_PKH) {
1✔
135
                        ByteBuffer hash160 = keyUtility.byteBufferUtility.byteArrayToByteBuffer(witnessProgram);
1✔
136
                        return new AddressToCoin(hash160, amount); // P2WPKH supported
1✔
137
                    } else if (witnessProgram.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_SH) {
1✔
138
                        byte[] scriptHash = witnessProgram;
1✔
139
                        return null; // P2WSH not supported
1✔
140
                    }
141
                    break;
142
                case WITNESS_VERSION_1:
143
                    if (witnessProgram.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_TR) {
1✔
144
                        byte[] tweakedPublicKey = witnessProgram;
1✔
145
                        return null; // P2TR not supported
1✔
146
                    }
147
                    break;
148
                default:
149
                    // not supported
150
                    return null;
1✔
151
            }
152
        } catch (AddressFormatException | ReflectiveOperationException  e) {
1✔
153
            // Bech32 parsing or reflection failed; continue to next format
UNCOV
154
        }
×
155
        
156
        // ZCash or Peercoin with 2-byte version
157
        if (address.startsWith("p") || address.startsWith("t")) {
1✔
158
            // p: bitcoin cash / CashAddr (P2SH), this is a unique format and does not work
159
            // p: peercoin possible
160
            // t: ZCash has two version bytes
161
            try {
162
                ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_ZCASH);
1✔
163
                return new AddressToCoin(hash160, amount);
1✔
164
            } catch (RuntimeException e) {
1✔
165
                // Fall through to other format checks
166
            }
167
        }
168
        
169
        // Bitcoin Cash 'q' prefix: convert to legacy address
170
        if (address.startsWith("q")) {
1✔
171
            address = AddressConverter.toLegacyAddress(address);
1✔
172
        }
173
        
174
        // Fallback: assume Base58 with 1-byte version prefix
175
        try {
176
            ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_REGULAR);
1✔
177
            return new AddressToCoin(hash160, amount);
1✔
178
        } catch (AddressFormatException e) {
1✔
179
            return null;
1✔
180
        }
181
    }
182
    
183
    @SuppressWarnings("unchecked")
184
    private <T> T invokeProtectedMethod(Bech32.Bech32Bytes bech32Bytes, String methodName, Class<T> returnType) throws ReflectiveOperationException  {
185
        Class<?> clazz = Bech32.Bech32Bytes.class;
1✔
186
        Method method = clazz.getDeclaredMethod(methodName);
1✔
187
        method.setAccessible(true);
1✔
188
        return (T) method.invoke(bech32Bytes);
1✔
189
    }
190

191
    private ByteBuffer getHash160AsByteBufferFromBase58AddressUnchecked(String base58, KeyUtility keyUtility, int srcPos) {
192
        byte[] hash160 = getHash160fromBase58AddressUnchecked(base58, srcPos);
1✔
193
        ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
194
        return hash160AsByteBuffer;
1✔
195
    }
196

197
    byte[] getHash160fromBase58AddressUnchecked(String base58, int srcPos) {
198
        byte[] decoded = Base58.decode(base58);
1✔
199
        byte[] hash160 = new byte[20];
1✔
200
        int toCopy = Math.min(decoded.length - srcPos, hash160.length);
1✔
201
        System.arraycopy(decoded, srcPos, hash160, 0, toCopy);
1✔
202
        return hash160;
1✔
203
    }
204

205
    @Nullable
206
    private Coin getCoinIfPossible(String[] lineSplitted, Coin defaultValue) throws NumberFormatException {
207
        if (lineSplitted.length > 1) {
1✔
208
            String amountString = lineSplitted[1];
1✔
209
            try {
210
                return Coin.valueOf(Long.valueOf(amountString));
1✔
211
            } catch (NumberFormatException e) {
1✔
212
                return defaultValue;
1✔
213
            }
214
        } else {
215
            return defaultValue;
1✔
216
        }
217
    }
218
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc