Skip to content

Commit 591a968

Browse files
authored
Bitcoin Transaction Monitor (#45)
This PR adds a scatterplot (entry time x feerate) of recent mempool entries.
2 parents 5b1322d + dee946e commit 591a968

55 files changed

Lines changed: 3053 additions & 523 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,18 @@
88
Seemingly stuck and longtime-unconfirmed transactions can be quite annoying for users transacting on the Bitcoin network.
99
The idea of mempool.observer is to provide users with information about unconfirmed transactions and transaction fees.
1010

11-
> This is v2-master of memo.
12-
> A full project refresh.
13-
1411
## Project Structure
1512

1613
Folder Structure
1714
```
1815
memo/
19-
├── api/ # Go module functioning as an API returning JSON
20-
├── database/ # Database creation scripts
21-
├── memod/ # Go module functioning as a worker deamon wirting data to database
16+
├── api/ # Go code that compiles to a binary API returning JSON from a Redis instance
17+
├── memod/ # Go code that compiles to a binary worker daemon writing data to a Redis instance
2218
└── www/ # Statically served HTML, JS and CSS files
2319
```
2420

2521
There exists a overview of my [infrastructure setup](https://github.com/0xB10C/memo/wiki/Infrastructure-memo-v2) for mempool.observer.
2622

27-
2823
## Project History
2924

3025
I've started building the first version of mempool.observer mid 2017 as my first Bitcoin related project.
@@ -36,14 +31,19 @@ Later this year the bitcoin transaction fees rose and I had quite some traffic.
3631
The high fees were caused by a huge transaction flood as the price rose to $20k.
3732
I regularly had problems with long running scripts due to querying and processing the huge mempool on a low end VPS.
3833
Due to time constrains I wasn't able to improve the performance.
39-
This resulted in mempool.observer v1 dieing the not-maintained-death sometime in 2018.
34+
This resulted in mempool.observer version 1 dieing the not-maintained-death sometime in 2018.
4035

41-
I've focused full time on Bitcoin in spring 2019 and spend a part of that time to work on v2.
42-
V2 is a full rewrite of mempool.observer - only the idea, license and the quote from Maxwell remained.
43-
The goal is to offer way more than v1 did, but build on a foundation with performance and maintainability in mind.
36+
I've focused full time on Bitcoin in spring 2019 and spend a part of that time to work on version 2.
37+
Version 2 is a full rewrite of mempool.observer - only the idea, license and the quote from Maxwell remained.
38+
The goal is to offer way more than version 1 did, but build on a foundation with performance and maintainability in mind.
4439
I'm open for ideas and feedback.
4540

41+
## Self-Hosting
42+
43+
Self-hosting memo is possible, but there is no detailed setup and maintenance documentation written yet.
44+
You might need to do some exploration on your own to get everything working.
45+
Keep in mind, that you need a customized Bitcoin Core version to run the Bitcoin Transaction Monitor.
4646

47-
## Licencse
47+
## Licence
4848
This project and all it's files are licensed under a GNU Affero General Public License.
4949

api/api.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import (
66
"net/http"
77
"strconv"
88

9+
"github.com/0xb10c/memo/api/cache"
910
"github.com/0xb10c/memo/api/config"
1011
"github.com/0xb10c/memo/api/database"
1112
"github.com/gin-contrib/cors"
13+
"github.com/gin-contrib/gzip"
1214
"github.com/gin-gonic/gin"
1315
)
1416

@@ -25,20 +27,25 @@ func main() {
2527
} else {
2628
corsConfig.AllowOrigins = []string{"*"}
2729
}
30+
2831
router.Use(cors.New(corsConfig))
32+
router.Use(gzip.Gzip(gzip.DefaultCompression))
2933

3034
err := database.SetupDatabase()
3135
if err != nil {
3236
panic(fmt.Errorf("Failed to setup database: %v", err))
3337
}
3438

39+
go cache.SetupCache()
40+
3541
api := router.Group("/api")
3642
{
3743
api.GET("/mempool", getMempool)
3844
api.GET("/recentBlocks", getRecentBlocks)
3945
api.GET("/historicalMempool/:timeframe/:by", getHistoricalMempool)
40-
api.GET("/timeInMempool", getTimeInMempool)
4146
api.GET("/transactionStats", getTransactionStats)
47+
api.GET("/getMempoolEntries", getCachedMempoolEntries)
48+
api.GET("/getRecentFeerateAPIData", getRecentFeerateAPIEntries)
4249
}
4350

4451
portString := ":" + config.GetString("api.port")
@@ -117,9 +124,21 @@ func getHistoricalMempool(c *gin.Context) {
117124
c.JSON(http.StatusOK, mempoolStates)
118125
}
119126

120-
func getTimeInMempool(c *gin.Context) {
127+
func getTransactionStats(c *gin.Context) {
128+
tss, err := database.GetTransactionStats()
129+
if err != nil {
130+
fmt.Println(err.Error())
131+
c.JSON(http.StatusInternalServerError, gin.H{
132+
"error": "Database error",
133+
})
134+
return
135+
}
121136

122-
timestamp, timeAxis, feerateAxis, err := database.GetTimeInMempool()
137+
c.JSON(http.StatusOK, tss)
138+
}
139+
140+
func getMempoolEntries(c *gin.Context) {
141+
mes, err := database.GetMempoolEntries()
123142
if err != nil {
124143
fmt.Println(err.Error())
125144
c.JSON(http.StatusInternalServerError, gin.H{
@@ -128,15 +147,12 @@ func getTimeInMempool(c *gin.Context) {
128147
return
129148
}
130149

131-
c.JSON(http.StatusOK, gin.H{
132-
"timestamp": timestamp,
133-
"feerateAxis": feerateAxis,
134-
"timeAxis": timeAxis,
135-
})
150+
c.JSON(http.StatusOK, mes)
136151
}
137152

138-
func getTransactionStats(c *gin.Context) {
139-
tss, err := database.GetTransactionStats()
153+
func getRecentFeerateAPIEntries(c *gin.Context) {
154+
155+
entries, err := database.GetRecentFeerateAPIEntries()
140156
if err != nil {
141157
fmt.Println(err.Error())
142158
c.JSON(http.StatusInternalServerError, gin.H{
@@ -145,5 +161,18 @@ func getTransactionStats(c *gin.Context) {
145161
return
146162
}
147163

148-
c.JSON(http.StatusOK, tss)
164+
c.JSON(http.StatusOK, entries)
165+
}
166+
167+
func getCachedMempoolEntries(c *gin.Context) {
168+
entries, err := database.GetMempoolEntriesCache()
169+
if err != nil {
170+
fmt.Println(err.Error())
171+
c.JSON(http.StatusInternalServerError, gin.H{
172+
"error": "Database error",
173+
})
174+
return
175+
}
176+
c.Header("Content-Type", "application/json; charset=utf-8")
177+
c.String(http.StatusOK, entries)
149178
}

api/cache/cache.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cache
2+
3+
import (
4+
"encoding/json"
5+
"log"
6+
7+
"github.com/0xb10c/memo/api/database"
8+
"github.com/jasonlvhit/gocron"
9+
)
10+
11+
// SetupCache sets up caching scripts
12+
func SetupCache() {
13+
SetupMempoolEntriesCacher()
14+
}
15+
16+
// SetupMempoolEntriesCacher sets up a periodic GetMempoolEntries() fetch job
17+
func SetupMempoolEntriesCacher() {
18+
cacheMempoolEntries()
19+
20+
fetchInterval := uint64(30)
21+
s := gocron.NewScheduler()
22+
s.Every(fetchInterval).Seconds().Do(cacheMempoolEntries)
23+
log.Printf("Setup GetMempoolEntries() cacher to run every %d seconds.\n", fetchInterval)
24+
<-s.Start()
25+
defer s.Clear()
26+
}
27+
28+
func cacheMempoolEntries() {
29+
log.Printf("Caching mempool entries.\n")
30+
mes, err := database.GetMempoolEntries()
31+
if err != nil {
32+
log.Printf("Error getting mempool entries %v.\n", err)
33+
}
34+
35+
mesJSON, err := json.Marshal(mes)
36+
if err != nil {
37+
log.Printf("Error marshalling mempool entries %v.\n", err)
38+
}
39+
40+
err = database.SetMempoolEntriesCache(string(mesJSON))
41+
if err != nil {
42+
log.Printf("Could not cache mempool entries %v.\n", err)
43+
}
44+
45+
}

api/database/database.go

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package database
22

33
import (
4-
"encoding/json"
54
"errors"
65
"strconv"
76
"time"
87

98
"github.com/0xb10c/memo/api/config"
9+
"github.com/0xb10c/memo/api/types"
1010
"github.com/gomodule/redigo/redis"
11+
jsoniter "github.com/json-iterator/go"
1112
)
1213

13-
var (
14-
Pool *redis.Pool
15-
)
14+
var Pool *redis.Pool
15+
var json = jsoniter.ConfigCompatibleWithStandardLibrary
1616

1717
func newPool() *redis.Pool {
1818
dbUser := config.GetString("redis.user")
@@ -81,7 +81,7 @@ func GetMempool() (timestamp time.Time, feerateMapJSON string, megabyteMarkersJS
8181
return
8282
}
8383

84-
func GetRecentBlocks() (blocks []RecentBlock, err error) {
84+
func GetRecentBlocks() (blocks []types.RecentBlock, err error) {
8585
c := Pool.Get()
8686
defer c.Close()
8787

@@ -90,9 +90,9 @@ func GetRecentBlocks() (blocks []RecentBlock, err error) {
9090
return
9191
}
9292

93-
blocks = make([]RecentBlock, 0)
93+
blocks = make([]types.RecentBlock, 0)
9494
for index := range reJSON {
95-
block := RecentBlock{}
95+
block := types.RecentBlock{}
9696
err = json.Unmarshal([]byte(reJSON[index]), &block)
9797
if err != nil {
9898
return
@@ -110,7 +110,7 @@ type MempoolState struct {
110110
DataInBuckets []float64 `json:"dataInBuckets"`
111111
}
112112

113-
func GetHistorical(timeframe int, by string) (hmds []HistoricalMempoolData, err error) {
113+
func GetHistorical(timeframe int, by string) (hmds []types.HistoricalMempoolData, err error) {
114114
c := Pool.Get()
115115
defer c.Close()
116116

@@ -149,9 +149,9 @@ func GetHistorical(timeframe int, by string) (hmds []HistoricalMempoolData, err
149149
return
150150
}
151151

152-
hmds = make([]HistoricalMempoolData, 0)
152+
hmds = make([]types.HistoricalMempoolData, 0)
153153
for index := range hmdsJSON {
154-
hmd := HistoricalMempoolData{}
154+
hmd := types.HistoricalMempoolData{}
155155
err = json.Unmarshal([]byte(hmdsJSON[index]), &hmd)
156156
if err != nil {
157157
return
@@ -162,61 +162,102 @@ func GetHistorical(timeframe int, by string) (hmds []HistoricalMempoolData, err
162162
return
163163
}
164164

165-
// GetTimeInMempool gets the TimeInMempool data from the database
166-
func GetTimeInMempool() (timestamp int64, timeAxis []int, feerateAxis []float64, err error) {
165+
// GetTransactionStats gets the Transaction Stats data from the database
166+
func GetTransactionStats() (tss []types.TransactionStat, err error) {
167167
c := Pool.Get()
168168
defer c.Close()
169169

170-
prefix := "timeInMempool"
171-
172-
response, err := redis.Strings(c.Do("MGET", prefix+":timeAxis", prefix+":feerateAxis", prefix+":utcTimestamp"))
170+
tssJSON, err := redis.Strings(c.Do("LRANGE", "transactionStats", 0, 180))
173171
if err != nil {
174172
return
175173
}
176174

177-
err = json.Unmarshal([]byte(response[0]), &timeAxis)
178-
if err != nil {
179-
return
175+
type transactionStat struct {
176+
SegwitCount int `json:"segwitCount"`
177+
RbfCount int `json:"rbfCount"`
178+
TxCount int `json:"txCount"`
179+
Timestamp int64 `json:"timestamp"`
180180
}
181181

182-
err = json.Unmarshal([]byte(response[1]), &feerateAxis)
183-
if err != nil {
184-
return
182+
tss = make([]types.TransactionStat, 0)
183+
for index := range tssJSON {
184+
ts := types.TransactionStat{}
185+
err = json.Unmarshal([]byte(tssJSON[index]), &ts)
186+
if err != nil {
187+
return
188+
}
189+
tss = append(tss, ts)
185190
}
186191

187-
err = json.Unmarshal([]byte(response[2]), &timestamp)
192+
return
193+
}
194+
195+
// GetMempoolEntries gets the last x mempool Entries from the database
196+
func GetMempoolEntries() (mes []types.MempoolEntry, err error) {
197+
c := Pool.Get()
198+
defer c.Close()
199+
200+
// gets recent entries from 0 to 19999 (20k)
201+
mesJSON, err := redis.Strings(c.Do("ZREVRANGE", "mempoolEntries", 0, 29999))
188202
if err != nil {
189203
return
190204
}
191205

206+
mes = make([]types.MempoolEntry, 0)
207+
for index := range mesJSON {
208+
me := types.MempoolEntry{}
209+
err = json.Unmarshal([]byte(mesJSON[index]), &me)
210+
if err != nil {
211+
return
212+
}
213+
mes = append(mes, me)
214+
}
215+
192216
return
193217
}
194218

195-
// GetTransactionStats gets the Transaction Stats data from the database
196-
func GetTransactionStats() (tss []TransactionStat, err error) {
219+
// SetMempoolEntriesCache SETs the response of a recent GetMempoolEntries() as a cache
220+
func SetMempoolEntriesCache(mesJSON string) (err error) {
197221
c := Pool.Get()
198222
defer c.Close()
199223

200-
tssJSON, err := redis.Strings(c.Do("LRANGE", "transactionStats", 0, 180))
224+
_, err = c.Do("SET", "cache:mempoolEntries", mesJSON)
225+
if err != nil {
226+
return err
227+
}
228+
return nil
229+
}
230+
231+
// GetMempoolEntriesCache GET the cached response of a recent GetMempoolEntries() call
232+
func GetMempoolEntriesCache() (mesJSON string, err error) {
233+
c := Pool.Get()
234+
defer c.Close()
235+
236+
mesJSON, err = redis.String(c.Do("GET", "cache:mempoolEntries"))
201237
if err != nil {
202238
return
203239
}
240+
return
241+
}
204242

205-
type transactionStat struct {
206-
SegwitCount int `json:"segwitCount"`
207-
RbfCount int `json:"rbfCount"`
208-
TxCount int `json:"txCount"`
209-
Timestamp int64 `json:"timestamp"`
243+
// GetRecentFeerateAPIEntries returns the recent feeRate API entrys from Redis
244+
func GetRecentFeerateAPIEntries() (entries []types.FeeRateAPIEntry, err error) {
245+
c := Pool.Get()
246+
defer c.Close()
247+
248+
entrysJSON, err := redis.Strings(c.Do("LRANGE", "feerateAPIEntries", 0, 100))
249+
if err != nil {
250+
return
210251
}
211252

212-
tss = make([]TransactionStat, 0)
213-
for index := range tssJSON {
214-
ts := TransactionStat{}
215-
err = json.Unmarshal([]byte(tssJSON[index]), &ts)
253+
entries = make([]types.FeeRateAPIEntry, 0)
254+
for index := range entrysJSON {
255+
entry := types.FeeRateAPIEntry{}
256+
err = json.Unmarshal([]byte(entrysJSON[index]), &entry)
216257
if err != nil {
217258
return
218259
}
219-
tss = append(tss, ts)
260+
entries = append(entries, entry)
220261
}
221262

222263
return

0 commit comments

Comments
 (0)