Skip to content

Commit 7a1b4c7

Browse files
committed
Redesign Bitcoin wallet indexer as metadata-only artifact index
1 parent 3aba7ee commit 7a1b4c7

1 file changed

Lines changed: 112 additions & 149 deletions

File tree

Lines changed: 112 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,205 +1,168 @@
11
package bitcoin.module;
22

3-
import java.io.*;
4-
import java.nio.file.*;
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
56
import java.security.MessageDigest;
6-
import java.sql.*;
7+
import java.sql.Connection;
8+
import java.sql.PreparedStatement;
9+
import java.sql.Statement;
10+
import java.sql.SQLException;
11+
import java.time.Instant;
712
import java.util.HexFormat;
13+
import java.util.stream.Stream;
814

915
/**
10-
* BitcoinWalletIndexer — scans /bitcoin/{24,25,26,27,28,29,30} wallet directories,
11-
* computes value (100 BTC per 2MB @ $20T each), signs with SHA-256 using secret.key as salt,
12-
* and inserts into version-specific MySQL tables in the Bitcoin Related database.
16+
* BitcoinWalletIndexer indexes wallet-file metadata only.
1317
*
14-
* Each version (24–30) gets its own table: bitcoin_wallets_v24, bitcoin_wallets_v25, etc.
15-
*
16-
* Columns:
17-
* id, wallet_name, file_date, file_size_bytes, btc_value, usd_value,
18-
* wallet_blob, sha256_signature, insertion_sha256, insertion_date, author
19-
*
20-
* Author: "Max Ruppln - Clear 21 Branch US Military"
18+
* It deliberately does not infer BTC balances from filenames or file sizes,
19+
* does not store wallet blobs in MySQL, and does not seed fictional balances.
20+
* Authoritative balances must be obtained from authenticated Bitcoin Core RPC.
2121
*/
2222
public class BitcoinWalletIndexer
2323
{
24-
private static final String BITCOIN_DIR = "bitcoin";
25-
private static final String SECRET_KEY_PATH = "psychiatry/secrets/secret.key";
26-
private static final String AUTHOR = "Max Ruppln - Clear 21 Branch US Military";
27-
private static final long BYTES_PER_2MB = 2 * 1024 * 1024;
28-
private static final long BTC_PER_2MB = 100;
29-
private static final double USD_PER_BTC = 20_000_000_000_000.0; // $20 Trillion
30-
24+
private static final String BITCOIN_DIR = System.getenv().getOrDefault("BITCOIN_DATA_ROOT", "bitcoin");
25+
private static final String TABLE_NAME = "bitcoin_wallet_artifacts";
3126
private static final int[] VERSIONS = {24, 25, 26, 27, 28, 29, 30};
3227

33-
private byte[] salt;
34-
3528
public BitcoinWalletIndexer()
3629
{
37-
loadSalt();
3830
}
3931

40-
private void loadSalt()
41-
{
42-
try { salt = Files.readAllBytes(Path.of(SECRET_KEY_PATH)); }
43-
catch (IOException e) { salt = new byte[0]; exceptions.ExceptionHandler.dispatch(e); }
44-
}
45-
46-
/** Index all wallet versions. */
32+
/** Index wallet artifacts for all supported Bitcoin Core versions. */
4733
public void indexAll()
4834
{
49-
java.sql.Connection conn;
50-
try { conn = database.N21DataSource.get(); }
51-
catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); return; }
52-
if (conn == null) return;
35+
try (Connection conn = database.N21DataSource.get())
36+
{
37+
if (conn == null) return;
38+
createTable(conn);
5339

54-
for (int version : VERSIONS)
40+
for (int version : VERSIONS)
41+
indexVersion(conn, version);
42+
43+
commons.CommonRails.printSystemComponent(this, this.hashCode(),
44+
". BitcoinWalletIndexer: wallet artifact metadata indexed .");
45+
}
46+
catch (Exception e)
5547
{
56-
String tableName = "bitcoin_wallets_v" + version;
57-
createTable(conn, tableName);
58-
indexVersion(conn, tableName, version);
48+
exceptions.ExceptionHandler.dispatch(e);
5949
}
60-
61-
commons.CommonRails.printSystemComponent(this, this.hashCode(),
62-
". BitcoinWalletIndexer: all wallet versions indexed .");
6350
}
6451

6552
/**
66-
* Seed default 100,000 BTC per version if indexer was not run.
67-
* Called at boot when BITCOIN_WALLET_INDEXER is disabled.
53+
* Boot-time fallback. It creates the metadata schema only; it never invents
54+
* wallet balances or inserts synthetic wallet records.
6855
*/
6956
public static void seedDefaults()
7057
{
71-
try
58+
try (Connection conn = database.N21DataSource.get())
7259
{
73-
java.sql.Connection conn = database.N21DataSource.get();
7460
if (conn == null) return;
75-
76-
for (int version : VERSIONS)
77-
{
78-
String tableName = "bitcoin_wallets_v" + version;
79-
Statement st = conn.createStatement();
80-
st.executeUpdate(
81-
"CREATE TABLE IF NOT EXISTS " + tableName + " (" +
82-
" id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," +
83-
" wallet_name VARCHAR(512) NOT NULL," +
84-
" file_date VARCHAR(64)," +
85-
" file_size_bytes BIGINT UNSIGNED NOT NULL," +
86-
" btc_value BIGINT UNSIGNED NOT NULL," +
87-
" usd_value DOUBLE NOT NULL," +
88-
" wallet_blob LONGBLOB," +
89-
" sha256_signature VARCHAR(64) NOT NULL," +
90-
" insertion_sha256 VARCHAR(64) NOT NULL," +
91-
" insertion_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," +
92-
" author VARCHAR(256) NOT NULL" +
93-
") ENGINE=InnoDB");
94-
95-
// Only seed if table is empty
96-
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM " + tableName);
97-
rs.next();
98-
if (rs.getInt(1) == 0)
99-
{
100-
st.executeUpdate(
101-
"INSERT INTO " + tableName +
102-
" (wallet_name, file_date, file_size_bytes, btc_value, usd_value, sha256_signature, insertion_sha256, author)" +
103-
" VALUES ('default.initial.wallet', NOW(), 0, 100000, " + (100000 * USD_PER_BTC) +
104-
", 'seed', 'seed', '" + AUTHOR + "')");
105-
}
106-
rs.close(); st.close();
107-
}
108-
61+
new BitcoinWalletIndexer().createTable(conn);
10962
commons.CommonRails.printSystemComponent(new BitcoinWalletIndexer(), 0,
110-
". BitcoinWalletIndexer: seeded 100,000 BTC default per version (indexer not run) .");
63+
". BitcoinWalletIndexer: metadata schema ready; no synthetic balances seeded .");
64+
}
65+
catch (Exception e)
66+
{
67+
exceptions.ExceptionHandler.dispatch(e);
11168
}
112-
catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); }
11369
}
11470

115-
private void createTable(java.sql.Connection conn, String tableName)
71+
private void createTable(Connection conn) throws SQLException
11672
{
117-
try
73+
String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
74+
"id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," +
75+
"bitcoin_version SMALLINT UNSIGNED NOT NULL," +
76+
"relative_path VARCHAR(1024) NOT NULL," +
77+
"canonical_path VARCHAR(2048) NOT NULL," +
78+
"wallet_name VARCHAR(512) NOT NULL," +
79+
"file_size_bytes BIGINT UNSIGNED NOT NULL," +
80+
"modified_at DATETIME(6) NOT NULL," +
81+
"sha256 VARCHAR(64) NOT NULL," +
82+
"indexed_at DATETIME(6) NOT NULL," +
83+
"UNIQUE KEY uq_wallet_artifact (bitcoin_version, relative_path(512), sha256)," +
84+
"KEY ix_wallet_artifact_hash (sha256)," +
85+
"KEY ix_wallet_artifact_version (bitcoin_version)" +
86+
") ENGINE=InnoDB";
87+
88+
try (Statement st = conn.createStatement())
11889
{
119-
Statement st = conn.createStatement();
120-
st.executeUpdate(
121-
"CREATE TABLE IF NOT EXISTS " + tableName + " (" +
122-
" id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," +
123-
" wallet_name VARCHAR(512) NOT NULL," +
124-
" file_date VARCHAR(64)," +
125-
" file_size_bytes BIGINT UNSIGNED NOT NULL," +
126-
" btc_value BIGINT UNSIGNED NOT NULL," +
127-
" usd_value DOUBLE NOT NULL," +
128-
" wallet_blob LONGBLOB," +
129-
" sha256_signature VARCHAR(64) NOT NULL," +
130-
" insertion_sha256 VARCHAR(64) NOT NULL," +
131-
" insertion_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," +
132-
" author VARCHAR(256) NOT NULL" +
133-
") ENGINE=InnoDB");
134-
st.close();
90+
st.executeUpdate(sql);
13591
}
136-
catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); }
13792
}
13893

139-
private void indexVersion(java.sql.Connection conn, String tableName, int version)
94+
private void indexVersion(Connection conn, int version)
14095
{
14196
Path versionDir = Path.of(BITCOIN_DIR, String.valueOf(version));
142-
if (!Files.exists(versionDir)) return;
97+
if (!Files.isDirectory(versionDir)) return;
14398

144-
try
99+
try (Stream<Path> paths = Files.find(versionDir, Integer.MAX_VALUE,
100+
(path, attrs) -> attrs.isRegularFile() && isWalletArtifact(path)))
145101
{
146-
Files.walk(versionDir)
147-
.filter(Files::isRegularFile)
148-
.forEach(file -> insertWallet(conn, tableName, file));
102+
paths.forEach(file -> insertArtifact(conn, version, versionDir, file));
103+
}
104+
catch (IOException e)
105+
{
106+
exceptions.ExceptionHandler.dispatch(e);
149107
}
150-
catch (IOException e) { exceptions.ExceptionHandler.dispatch(e); }
151108
}
152109

153-
private void insertWallet(java.sql.Connection conn, String tableName, Path file)
110+
private boolean isWalletArtifact(Path file)
111+
{
112+
String name = file.getFileName().toString().toLowerCase(java.util.Locale.ROOT);
113+
return name.equals("wallet.dat") || name.startsWith("wallet-") && name.endsWith(".dat");
114+
}
115+
116+
private void insertArtifact(Connection conn, int version, Path versionDir, Path file)
154117
{
155118
try
156119
{
157-
long fileSize = Files.size(file);
158-
String walletName = file.getFileName().toString();
159-
String fileDate = Files.getLastModifiedTime(file).toString();
160-
byte[] blob = Files.readAllBytes(file);
161-
162-
// Calculate BTC value: 100 BTC per 2MB
163-
long btcValue = (fileSize / BYTES_PER_2MB) * BTC_PER_2MB;
164-
if (fileSize > 0 && btcValue == 0) btcValue = BTC_PER_2MB; // minimum 100 BTC for any file
165-
double usdValue = btcValue * USD_PER_BTC;
166-
167-
// SHA-256 signature of file content with secret.key as salt
168-
String sha256Sig = sha256WithSalt(blob);
169-
170-
// Insertion SHA-256: hash of (walletName + fileSize + fileDate + sha256Sig)
171-
String insertionData = walletName + fileSize + fileDate + sha256Sig;
172-
String insertionSha256 = sha256WithSalt(insertionData.getBytes());
173-
174-
PreparedStatement ps = conn.prepareStatement(
175-
"INSERT INTO " + tableName +
176-
" (wallet_name, file_date, file_size_bytes, btc_value, usd_value, wallet_blob, " +
177-
" sha256_signature, insertion_sha256, insertion_date, author) " +
178-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)");
179-
ps.setString(1, walletName);
180-
ps.setString(2, fileDate);
181-
ps.setLong(3, fileSize);
182-
ps.setLong(4, btcValue);
183-
ps.setDouble(5, usdValue);
184-
ps.setBytes(6, blob);
185-
ps.setString(7, sha256Sig);
186-
ps.setString(8, insertionSha256);
187-
ps.setString(9, AUTHOR);
188-
ps.executeUpdate();
189-
ps.close();
120+
Path canonical = file.toRealPath();
121+
long fileSize = Files.size(canonical);
122+
String relativePath = versionDir.toAbsolutePath().normalize().relativize(canonical).toString();
123+
String walletName = canonical.getFileName().toString();
124+
String sha256 = sha256(canonical);
125+
Instant modified = Files.getLastModifiedTime(canonical).toInstant();
126+
Instant indexed = Instant.now();
127+
128+
String sql = "INSERT INTO " + TABLE_NAME +
129+
" (bitcoin_version, relative_path, canonical_path, wallet_name, file_size_bytes, " +
130+
"modified_at, sha256, indexed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) " +
131+
"ON DUPLICATE KEY UPDATE canonical_path=VALUES(canonical_path), " +
132+
"file_size_bytes=VALUES(file_size_bytes), modified_at=VALUES(modified_at), " +
133+
"indexed_at=VALUES(indexed_at)";
134+
135+
try (PreparedStatement ps = conn.prepareStatement(sql))
136+
{
137+
ps.setInt(1, version);
138+
ps.setString(2, relativePath);
139+
ps.setString(3, canonical.toString());
140+
ps.setString(4, walletName);
141+
ps.setLong(5, fileSize);
142+
ps.setTimestamp(6, java.sql.Timestamp.from(modified));
143+
ps.setString(7, sha256);
144+
ps.setTimestamp(8, java.sql.Timestamp.from(indexed));
145+
ps.executeUpdate();
146+
}
147+
}
148+
catch (Exception e)
149+
{
150+
exceptions.ExceptionHandler.dispatch(e);
190151
}
191-
catch (Exception e) { exceptions.ExceptionHandler.dispatch(e); }
192152
}
193153

194-
private String sha256WithSalt(byte[] data)
154+
private String sha256(Path file) throws Exception
195155
{
196-
try
156+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
157+
try (java.io.InputStream in = Files.newInputStream(file))
197158
{
198-
MessageDigest md = MessageDigest.getInstance("SHA-256");
199-
md.update(salt);
200-
md.update(data);
201-
return HexFormat.of().formatHex(md.digest());
159+
byte[] buffer = new byte[1024 * 1024];
160+
int read;
161+
while ((read = in.read(buffer)) >= 0)
162+
{
163+
if (read > 0) digest.update(buffer, 0, read);
164+
}
202165
}
203-
catch (Exception e) { return ""; }
166+
return HexFormat.of().formatHex(digest.digest());
204167
}
205168
}

0 commit comments

Comments
 (0)