Skip to content

Commit 9e8541f

Browse files
committed
Bitcoin: enforce RPC allowlist, exact amounts, address validation, and process timeouts
1 parent e189842 commit 9e8541f

1 file changed

Lines changed: 84 additions & 112 deletions

File tree

source/bitcoin/base/BitcoinBase.java

Lines changed: 84 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -9,50 +9,38 @@
99

1010
import java.io.BufferedReader;
1111
import java.io.InputStreamReader;
12+
import java.math.BigDecimal;
13+
import java.time.Duration;
14+
import java.util.ArrayList;
15+
import java.util.List;
1216
import java.util.stream.Collectors;
1317

1418
/**
15-
* BitcoinBase — wraps a local bitcoind instance.
19+
* BitcoinBase — controlled wrapper around a local bitcoind instance.
1620
*
17-
* RPC config mirrors /bitcoin/bash/btc24-query.sh:
18-
* port 2222
19-
* authentication: Bitcoin Core cookie authentication (no password in source)
20-
* network regtest
21-
* wallet "United States"
22-
*
23-
* All mutating operations (start, stop, load/unload wallet, send) persist a
24-
* trade/action record to the MySQL N21 instance via db.N21Store.storeBitcoinTrade().
25-
*
26-
* @author Max Rupplin
27-
* @date June 08 2026
21+
* RPC authentication is delegated to Bitcoin Core cookie authentication.
22+
* The server never accepts arbitrary bitcoin-cli method names and never places
23+
* RPC credentials in source code or command arguments.
2824
*/
2925
public class BitcoinBase
3026
{
3127
protected String hash = "0xDA717018470E213F";
32-
3328
protected NitroWebExpress.Aspect ASPECT;
3429

35-
// ── RPC constants (from btc24-query.sh) ──────────────────────────────────
36-
protected static final String BITCOIN_CLI = "bitcoin-cli";
37-
protected static final String BITCOIND = "bitcoind";
38-
protected static final String RPC_PORT = "2222";
39-
protected static final String NETWORK = "-regtest";
40-
protected static final String WALLET_NAME = "United States";
41-
42-
// ── Shared RPC flag array (prepended to every bitcoin-cli call) ───────────
43-
private static final String[] RPC_FLAGS = {
44-
NETWORK,
45-
"-rpcport=" + RPC_PORT,
46-
};
30+
protected static final String BITCOIN_CLI = "bitcoin-cli";
31+
protected static final String BITCOIND = "bitcoind";
32+
protected static final String RPC_PORT = configuredRpcPort();
33+
protected static final String NETWORK = "-regtest";
34+
protected static final String WALLET_NAME = "United States";
35+
private static final Duration RPC_TIMEOUT = Duration.ofSeconds(30);
4736

4837
protected MessageOrderer bitcoin_message_orderer = new MessageOrderer(this);
4938

5039
public BitcoinBase(final NitroWebExpress.Aspect ASPECT)
5140
{
5241
this.ASPECT = ASPECT;
53-
54-
BitcoinAsiaAndTokyoDate JAPANDate = new BitcoinAsiaAndTokyoDate();
55-
BitcoinAmericaAndNewYorkDate ESTDate = new BitcoinAmericaAndNewYorkDate();
42+
BitcoinAsiaAndTokyoDate JAPANDate = new BitcoinAsiaAndTokyoDate();
43+
BitcoinAmericaAndNewYorkDate ESTDate = new BitcoinAmericaAndNewYorkDate();
5644

5745
CommonRails.printSystemComponent(this, this.hashCode(),
5846
". WebExpress Bitcoin >> opens in North Carolina on Date " + ESTDate.EST_Time + " . ");
@@ -62,154 +50,138 @@ public BitcoinBase(final NitroWebExpress.Aspect ASPECT)
6250
database.N21Store.createBitcoinTradesTable();
6351
}
6452

65-
// ── Daemon lifecycle ──────────────────────────────────────────────────────
66-
67-
/** Start local bitcoind in regtest+daemon mode. */
6853
public String start_bitcoind()
6954
{
70-
String result = exec(new String[]{ BITCOIND, NETWORK, "-daemon",
71-
"-rpcport=" + RPC_PORT,
72-
});
55+
String result = exec(List.of(BITCOIND, NETWORK, "-daemon", "-rpcport=" + RPC_PORT), false);
7356
database.N21Store.storeBitcoinTrade("start_bitcoind", "", "", result);
7457
return result;
7558
}
7659

77-
/** Stop local bitcoind via RPC stop. */
7860
public String stop_bitcoind()
7961
{
8062
String result = cli("stop");
8163
database.N21Store.storeBitcoinTrade("stop_bitcoind", "", "", result);
8264
return result;
8365
}
8466

85-
// ── Wallet management ─────────────────────────────────────────────────────
86-
8767
public String load_wallet()
8868
{
69+
BitcoinRpcPolicy.requireWalletName(WALLET_NAME);
8970
String result = cli("loadwallet", WALLET_NAME);
9071
database.N21Store.storeBitcoinTrade("load_wallet", WALLET_NAME, "", result);
9172
return result;
9273
}
9374

9475
public String unload_wallet()
9576
{
77+
BitcoinRpcPolicy.requireWalletName(WALLET_NAME);
9678
String result = cli("unloadwallet", WALLET_NAME);
9779
database.N21Store.storeBitcoinTrade("unload_wallet", WALLET_NAME, "", result);
9880
return result;
9981
}
10082

10183
public String create_wallet(final String name)
10284
{
85+
BitcoinRpcPolicy.requireWalletName(name);
10386
String result = cli("createwallet", name);
10487
database.N21Store.storeBitcoinTrade("create_wallet", name, "", result);
10588
return result;
10689
}
10790

108-
/** Returns raw JSON from getwalletinfo for the default wallet. */
109-
public String get_wallet_info()
110-
{
111-
return walletCli("getwalletinfo");
112-
}
113-
114-
/** Returns raw balance string for the default wallet. */
115-
public String get_balance()
116-
{
117-
return walletCli("getbalance");
118-
}
119-
120-
/** Returns a new address for the default wallet. */
121-
public String get_new_address()
122-
{
123-
return walletCli("getnewaddress");
124-
}
125-
126-
// ── Node status ───────────────────────────────────────────────────────────
127-
128-
public String get_blockchain_info()
129-
{
130-
return cli("getblockchaininfo");
131-
}
132-
133-
public String get_block_count()
134-
{
135-
return cli("getblockcount");
136-
}
137-
138-
// ── Trade / send ──────────────────────────────────────────────────────────
91+
public String get_wallet_info() { return walletCli("getwalletinfo"); }
92+
public String get_balance() { return walletCli("getbalance"); }
93+
public String get_new_address() { return walletCli("getnewaddress"); }
94+
public String get_blockchain_info() { return cli("getblockchaininfo"); }
95+
public String get_block_count() { return cli("getblockcount"); }
13996

14097
/**
141-
* Send BTC from the default wallet to a destination address.
142-
* Records the trade to MySQL regardless of outcome.
143-
*
144-
* @param toAddress destination Bitcoin address
145-
* @param amount amount in BTC (e.g. "0.001")
146-
* @return txid on success, error string on failure
98+
* Broadcast a transaction only after strict address and amount validation.
99+
* Bitcoin Core remains the authority for transaction acceptance.
147100
*/
148101
public String send(final String toAddress, final String amount)
149102
{
150-
String result = walletCli("sendtoaddress", toAddress, amount);
151-
database.N21Store.storeBitcoinTrade("send", WALLET_NAME, toAddress + " " + amount + " BTC", result);
103+
BitcoinRpcPolicy.requireAddress(toAddress);
104+
long satoshis = BitcoinRpcPolicy.requireSatoshis(amount);
105+
String normalizedAmount = BigDecimal.valueOf(satoshis, 8).toPlainString();
106+
String result = walletCli("sendtoaddress", toAddress, normalizedAmount);
107+
database.N21Store.storeBitcoinTrade("send", WALLET_NAME,
108+
"address=" + toAddress + " satoshis=" + satoshis, result);
152109
return result;
153110
}
154111

155-
// ── Message pass-through ──────────────────────────────────────────────────
156-
157112
public void send_message(final StringBuffer BUFFER) {}
158-
public void send_message(final String MESSAGE) {}
113+
public void send_message(final String MESSAGE) {}
159114

160-
// ── Process helpers ───────────────────────────────────────────────────────
161-
162-
/**
163-
* Run bitcoin-cli with the shared RPC flags, no wallet suffix.
164-
* Additional args are appended after the RPC flags.
165-
*/
166115
protected String cli(final String... args)
167116
{
168-
String[] cmd = buildCmd(false, args);
169-
return exec(cmd);
117+
if (args.length == 0) throw new IllegalArgumentException("Bitcoin RPC method is required");
118+
BitcoinRpcPolicy.requireAllowed(args[0]);
119+
return exec(buildCmd(false, args), true);
170120
}
171121

172-
/**
173-
* Run bitcoin-cli with -rpcwallet=WALLET_NAME prepended to args.
174-
*/
175122
protected String walletCli(final String... args)
176123
{
177-
String[] cmd = buildCmd(true, args);
178-
return exec(cmd);
124+
if (args.length == 0) throw new IllegalArgumentException("Bitcoin RPC method is required");
125+
BitcoinRpcPolicy.requireAllowed(args[0]);
126+
return exec(buildCmd(true, args), true);
179127
}
180128

181-
private String[] buildCmd(final boolean withWallet, final String... args)
129+
private List<String> buildCmd(final boolean withWallet, final String... args)
182130
{
183-
int base = 1 + RPC_FLAGS.length + (withWallet ? 1 : 0);
184-
String[] cmd = new String[base + args.length];
185-
cmd[0] = BITCOIN_CLI;
186-
System.arraycopy(RPC_FLAGS, 0, cmd, 1, RPC_FLAGS.length);
187-
int off = 1 + RPC_FLAGS.length;
188-
if (withWallet) { cmd[off] = "-rpcwallet=" + WALLET_NAME; off++; }
189-
System.arraycopy(args, 0, cmd, off, args.length);
131+
List<String> cmd = new ArrayList<>();
132+
cmd.add(BITCOIN_CLI);
133+
cmd.add(NETWORK);
134+
cmd.add("-rpcport=" + RPC_PORT);
135+
if (withWallet) cmd.add("-rpcwallet=" + WALLET_NAME);
136+
for (String arg : args) cmd.add(arg);
190137
return cmd;
191138
}
192139

193-
/** Execute a command, capture stdout+stderr, return combined output. */
194-
private String exec(final String[] cmd)
140+
private String exec(final List<String> cmd, final boolean rpc)
195141
{
196142
try
197143
{
198-
Process p = Runtime.getRuntime().exec(cmd);
199-
String out = new BufferedReader(new InputStreamReader(p.getInputStream()))
200-
.lines().collect(Collectors.joining("\n"));
201-
String err = new BufferedReader(new InputStreamReader(p.getErrorStream()))
202-
.lines().collect(Collectors.joining("\n"));
203-
p.waitFor();
204-
String result = out.isBlank() ? err : out;
144+
ProcessBuilder builder = new ProcessBuilder(cmd);
145+
builder.redirectErrorStream(true);
146+
Process process = builder.start();
147+
148+
String result;
149+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())))
150+
{
151+
result = reader.lines().collect(Collectors.joining("\n"));
152+
}
153+
154+
if (!process.waitFor(RPC_TIMEOUT.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS))
155+
{
156+
process.destroyForcibly();
157+
return "ERROR: Bitcoin process timed out";
158+
}
159+
160+
int exit = process.exitValue();
161+
String method = cmd.size() > 3 ? cmd.get(cmd.size() - (rpc ? Math.min(1, cmd.size() - 1) : 1)) : "process";
205162
CommonRails.printSystemComponent(this, this.hashCode(),
206-
". BitcoinBase >> " + cmd[0] + " " + (cmd.length > 1 ? cmd[cmd.length - 1] : "") + " >> exit=" + p.exitValue() + " .");
207-
return result;
163+
". BitcoinBase >> controlled RPC invocation exit=" + exit + " method=" + (cmd.size() > 3 ? cmd.get(3) : method) + " .");
164+
return result == null ? "" : result;
208165
}
209166
catch (Exception e)
210167
{
211168
ExceptionHandler.dispatch(e);
212-
return "ERROR: " + e.getMessage();
169+
return "ERROR: Bitcoin RPC operation failed";
170+
}
171+
}
172+
173+
private static String configuredRpcPort()
174+
{
175+
String value = System.getenv().getOrDefault("BITCOIN_RPC_PORT", "2222");
176+
try
177+
{
178+
int port = Integer.parseInt(value);
179+
if (port < 1 || port > 65535) throw new NumberFormatException();
180+
return Integer.toString(port);
181+
}
182+
catch (NumberFormatException e)
183+
{
184+
return "2222";
213185
}
214186
}
215187
}

0 commit comments

Comments
 (0)